chromium/ios/chrome/browser/bookmarks/ui_bundled/folder_chooser/bookmarks_folder_chooser_sub_data_source_impl_unittest.mm

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/bookmarks/ui_bundled/folder_chooser/bookmarks_folder_chooser_sub_data_source_impl.h"

#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>

#import "base/containers/contains.h"
#import "base/memory/raw_ptr.h"
#import "base/notreached.h"
#import "base/strings/sys_string_conversions.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/bookmarks/browser/bookmark_node.h"
#import "components/bookmarks/common/bookmark_features.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_ios_unit_test_support.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_storage_type.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/folder_chooser/bookmarks_folder_chooser_consumer.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"

using bookmarks::BookmarkNode;

@interface FakeBookmarksFolderChooserParentDataSource
    : NSObject <BookmarksFolderChooserParentDataSource>

// The argument provided when `bookmarkNodeDeleted:` was called.
@property(nonatomic, assign) const BookmarkNode* bookmarkNodeDeletedArg;

- (instancetype)initWithNodes:(const std::set<const BookmarkNode*>&)nodes;

@end

@implementation FakeBookmarksFolderChooserParentDataSource {
  std::set<const BookmarkNode*> _editedNodes;
}

- (instancetype)initWithNodes:(const std::set<const BookmarkNode*>&)nodes {
  if ((self = [super init])) {
    _editedNodes = nodes;
  }
  return self;
}

- (void)bookmarkNodeDeleted:(const BookmarkNode*)bookmarkNode {
  if (base::Contains(_editedNodes, bookmarkNode)) {
    _editedNodes.erase(bookmarkNode);
  }
  _bookmarkNodeDeletedArg = bookmarkNode;
}

- (void)bookmarkModelWillRemoveAllNodes {
  _editedNodes.clear();
}

- (const std::set<const BookmarkNode*>&)editedNodes {
  return _editedNodes;
}

@end

class BookmarksFolderChooserSubDataSourceImplTest
    : public BookmarkIOSUnitTestSupport,
      public testing::WithParamInterface<BookmarkStorageType> {
 protected:
  BookmarksFolderChooserSubDataSourceImplTest() {
    mock_consumer_ =
        OCMStrictProtocolMock(@protocol(BookmarksFolderChooserConsumer));
  }

  ~BookmarksFolderChooserSubDataSourceImplTest() override {
    [sub_data_source_ disconnect];
    sub_data_source_.consumer = nil;
    sub_data_source_ = nil;
    mock_consumer_ = nil;
    fake_parent_data_source_ = nil;
  }

  void SetUp() override {
    BookmarkIOSUnitTestSupport::SetUp();
    edited_nodes_.insert(AddURL(mobile_node(), @"Test URL"));
  }

  const bookmarks::BookmarkNode* mobile_node() {
    switch (GetParam()) {
      case BookmarkStorageType::kLocalOrSyncable:
        return bookmark_model_->mobile_node();
      case BookmarkStorageType::kAccount:
        return bookmark_model_->account_mobile_node();
    }
    NOTREACHED();
  }

  void CreateSubDataSource() {
    fake_parent_data_source_ =
        [[FakeBookmarksFolderChooserParentDataSource alloc]
            initWithNodes:edited_nodes_];
    sub_data_source_ = [[BookmarksFolderChooserSubDataSourceImpl alloc]
        initWithBookmarkModel:bookmark_model_
                         type:GetParam()
             parentDataSource:fake_parent_data_source_];
    sub_data_source_.consumer = mock_consumer_;
  }

  const BookmarkNode* AddURL(const BookmarkNode* parent, NSString* title) {
    std::u16string c_title = base::SysNSStringToUTF16(title);
    GURL url(base::SysNSStringToUTF16(@"http://example.com/bookmark") +
             c_title);
    return bookmark_model_->AddURL(parent, parent->children().size(), c_title,
                                   url);
  }

  const BookmarkNode* AddFolder(const BookmarkNode* parent, NSString* title) {
    std::u16string c_title = base::SysNSStringToUTF16(title);
    return bookmark_model_->AddFolder(parent, parent->children().size(),
                                      c_title);
  }

  void ChangeTitle(const BookmarkNode* node, NSString* title) {
    std::u16string c_title = base::SysNSStringToUTF16(title);
    bookmark_model_->SetTitle(node, c_title,
                              bookmarks::metrics::BookmarkEditSource::kUser);
  }

  void RemoveNode(const BookmarkNode* node) {
    bookmark_model_->Remove(node, bookmarks::metrics::BookmarkEditSource::kUser,
                            FROM_HERE);
  }

  void RemoveAllNodes() { bookmark_model_->RemoveAllUserBookmarks(FROM_HERE); }

  void MoveNode(const BookmarkNode* node, const BookmarkNode* new_parent) {
    bookmark_model_->Move(node, new_parent, new_parent->children().size());
  }

  BookmarksFolderChooserSubDataSourceImpl* sub_data_source_;
  id mock_consumer_;
  FakeBookmarksFolderChooserParentDataSource* fake_parent_data_source_;
  NSString* test_folder_title_1 = @"Test Folder 1";
  NSString* test_folder_title_2 = @"Test Folder 2";
  std::set<const BookmarkNode*> edited_nodes_;
};

// Tests that the sub data source correctly fetches visible folders.
TEST_P(BookmarksFolderChooserSubDataSourceImplTest, TestVisibleFolderNodes) {
  const BookmarkNode* test_folder_node_1 =
      AddFolder(mobile_node(), test_folder_title_1);
  const BookmarkNode* test_folder_node_2 =
      AddFolder(test_folder_node_1, test_folder_title_2);
  edited_nodes_.insert(test_folder_node_2);
  CreateSubDataSource();

  std::vector<const BookmarkNode*> visible_folder_nodes =
      [sub_data_source_ visibleFolderNodes];
  ASSERT_EQ(2u, visible_folder_nodes.size());
  EXPECT_NSEQ(base::SysUTF16ToNSString(visible_folder_nodes[0]->GetTitle()),
              @"Mobile Bookmarks");
  EXPECT_NSEQ(base::SysUTF16ToNSString(visible_folder_nodes[1]->GetTitle()),
              test_folder_title_1);
}

// Tests that changing title of bookmarked folder node updates the UI.
TEST_P(BookmarksFolderChooserSubDataSourceImplTest, TestFolderTitleChange) {
  const BookmarkNode* test_folder_node =
      AddFolder(mobile_node(), test_folder_title_1);
  CreateSubDataSource();

  [[mock_consumer_ expect] notifyModelUpdated];
  ChangeTitle(test_folder_node, test_folder_title_2);

  [mock_consumer_ verify];
  std::vector<const BookmarkNode*> visible_folder_nodes =
      [sub_data_source_ visibleFolderNodes];
  ASSERT_EQ(2u, visible_folder_nodes.size());
  EXPECT_NSEQ(base::SysUTF16ToNSString(visible_folder_nodes[0]->GetTitle()),
              @"Mobile Bookmarks");
  EXPECT_NSEQ(base::SysUTF16ToNSString(visible_folder_nodes[1]->GetTitle()),
              test_folder_title_2);
}

// Tests that adding a folder node in the bookmark model updates the UI.
TEST_P(BookmarksFolderChooserSubDataSourceImplTest, TestFolderAdded) {
  const BookmarkNode* test_folder_node_1 =
      AddFolder(mobile_node(), test_folder_title_1);
  CreateSubDataSource();

  [[mock_consumer_ expect] notifyModelUpdated];
  AddFolder(test_folder_node_1, test_folder_title_2);

  [mock_consumer_ verify];
  std::vector<const BookmarkNode*> visible_folder_nodes =
      [sub_data_source_ visibleFolderNodes];
  ASSERT_EQ(3u, visible_folder_nodes.size());
  EXPECT_NSEQ(base::SysUTF16ToNSString(visible_folder_nodes[0]->GetTitle()),
              @"Mobile Bookmarks");
  EXPECT_NSEQ(base::SysUTF16ToNSString(visible_folder_nodes[1]->GetTitle()),
              test_folder_title_1);
  EXPECT_NSEQ(base::SysUTF16ToNSString(visible_folder_nodes[2]->GetTitle()),
              test_folder_title_2);
}

// Tests that removing a folder node from the bookmark model updates the UI.
TEST_P(BookmarksFolderChooserSubDataSourceImplTest, TestFolderRemoved) {
  const BookmarkNode* test_folder_node_1 =
      AddFolder(mobile_node(), test_folder_title_1);
  const BookmarkNode* test_folder_node_2 =
      AddFolder(test_folder_node_1, test_folder_title_2);
  CreateSubDataSource();

  // `mock_consumer_` gets notified twice in `bookmarkNodeChildrenChanged:` and
  // `bookmarkNodeDeleted:fromFolder:` method calls from `BookmarkModel`
  // observer.
  [[mock_consumer_ expect] notifyModelUpdated];
  [[mock_consumer_ expect] notifyModelUpdated];
  RemoveNode(test_folder_node_2);

  [mock_consumer_ verify];
  ASSERT_EQ(test_folder_node_2,
            fake_parent_data_source_.bookmarkNodeDeletedArg);
  std::vector<const BookmarkNode*> visible_folder_nodes =
      [sub_data_source_ visibleFolderNodes];
  ASSERT_EQ(2u, visible_folder_nodes.size());
  EXPECT_NSEQ(base::SysUTF16ToNSString(visible_folder_nodes[0]->GetTitle()),
              @"Mobile Bookmarks");
  EXPECT_NSEQ(base::SysUTF16ToNSString(visible_folder_nodes[1]->GetTitle()),
              test_folder_title_1);
}

// Tests that removing all nodes in the bookmark model updates the UI.
TEST_P(BookmarksFolderChooserSubDataSourceImplTest, TestAllFoldersRemoved) {
  const BookmarkNode* test_folder_node_1 =
      AddFolder(mobile_node(), test_folder_title_1);
  AddFolder(test_folder_node_1, test_folder_title_2);
  CreateSubDataSource();

  [[mock_consumer_ expect] notifyModelUpdated];

  RemoveAllNodes();

  [mock_consumer_ verify];
  std::vector<const BookmarkNode*> visible_folder_nodes =
      [sub_data_source_ visibleFolderNodes];
  ASSERT_EQ(1u, visible_folder_nodes.size());
  // "Mobile Bookmarks" is a permanent node and thus always exists.
  EXPECT_NSEQ(base::SysUTF16ToNSString(visible_folder_nodes[0]->GetTitle()),
              @"Mobile Bookmarks");
}

// Tests that moving a node in the bookmark model updates the UI.
TEST_P(BookmarksFolderChooserSubDataSourceImplTest, TestFolderMoved) {
  const BookmarkNode* test_folder_node_1 =
      AddFolder(mobile_node(), test_folder_title_1);
  const BookmarkNode* test_folder_node_2 =
      AddFolder(test_folder_node_1, test_folder_title_2);
  CreateSubDataSource();

  [[mock_consumer_ expect] notifyModelUpdated];
  MoveNode(test_folder_node_2, mobile_node());

  [mock_consumer_ verify];
  std::vector<const BookmarkNode*> visible_folder_nodes =
      [sub_data_source_ visibleFolderNodes];
  ASSERT_EQ(3u, visible_folder_nodes.size());
  EXPECT_NSEQ(base::SysUTF16ToNSString(visible_folder_nodes[0]->GetTitle()),
              @"Mobile Bookmarks");
  EXPECT_NSEQ(base::SysUTF16ToNSString(visible_folder_nodes[1]->GetTitle()),
              test_folder_title_1);
  EXPECT_NSEQ(base::SysUTF16ToNSString(visible_folder_nodes[2]->GetTitle()),
              test_folder_title_2);
}

INSTANTIATE_TEST_SUITE_P(
    /* No InstantionName*/,
    BookmarksFolderChooserSubDataSourceImplTest,
    testing::Values(BookmarkStorageType::kAccount,
                    BookmarkStorageType::kLocalOrSyncable));