chromium/ios/chrome/browser/bookmarks/ui_bundled/bookmark_earl_grey_app_interface.mm

// Copyright 2019 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/bookmark_earl_grey_app_interface.h"

#import <vector>

#import "base/format_macros.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/bookmarks/browser/bookmark_node.h"
#import "components/bookmarks/browser/bookmark_utils.h"
#import "components/bookmarks/browser/titled_url_match.h"
#import "components/bookmarks/common/bookmark_metrics.h"
#import "components/prefs/pref_service.h"
#import "components/query_parser/query_parser.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_model_factory.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_storage_type.h"
#import "ios/chrome/browser/bookmarks/model/bookmarks_utils.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/bookmark_path_cache.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/bookmark_utils_ios.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/test/app/chrome_test_util.h"
#import "ios/testing/nserror_util.h"
#import "ui/base/models/tree_node_iterator.h"
#import "url/gurl.h"

namespace {

bookmarks::BookmarkModel* GetBookmarkModel() {
  return ios::BookmarkModelFactory::GetForBrowserState(
      chrome_test_util::GetOriginalBrowserState());
}

std::vector<const bookmarks::BookmarkNode*> GetBookmarksWithTitle(
    NSString* title,
    bookmarks::BookmarkModel* bookmark_model,
    BookmarkStorageType type) {
  int const kMaxCountOfBookmarks = 50;

  bookmarks::QueryFields query;
  query.title =
      std::make_unique<std::u16string>(base::SysNSStringToUTF16(title));

  std::vector<const bookmarks::BookmarkNode*> nodes =
      bookmarks::GetBookmarksMatchingProperties(bookmark_model, query,
                                                kMaxCountOfBookmarks);

  base::ranges::remove_if(nodes, [=](const bookmarks::BookmarkNode* node) {
    return type !=
           bookmark_utils_ios::GetBookmarkStorageType(node, bookmark_model);
  });

  return nodes;
}

const bookmarks::BookmarkNode* GetFirstBookmarkWithTitle(
    NSString* title,
    bookmarks::BookmarkModel* bookmark_model,
    BookmarkStorageType type) {
  std::vector<const bookmarks::BookmarkNode*> nodes =
      GetBookmarksWithTitle(title, bookmark_model, type);
  return nodes.empty() ? nullptr : nodes[0];
}

const bookmarks::BookmarkNode* GetMobileNodeWithType(
    BookmarkStorageType type,
    const bookmarks::BookmarkModel* model) {
  CHECK(model);
  switch (type) {
    case BookmarkStorageType::kLocalOrSyncable:
      return model->mobile_node();
    case BookmarkStorageType::kAccount:
      return model->account_mobile_node();
  }
  NOTREACHED();
}

}  // namespace

@implementation BookmarkEarlGreyAppInterface

#pragma mark - Public Interface

+ (NSError*)clearBookmarks {
  NSError* bookmarkModelLoadedError =
      [BookmarkEarlGreyAppInterface waitForBookmarkModelLoaded];
  if (bookmarkModelLoadedError) {
    return bookmarkModelLoadedError;
  }

  bookmarks::BookmarkModel* bookmarkModel = GetBookmarkModel();
  ChromeBrowserState* browserState =
      chrome_test_util::GetOriginalBrowserState();
  [BookmarkPathCache
      clearBookmarkTopMostRowCacheWithPrefService:browserState->GetPrefs()];

  bookmarkModel->RemoveAllUserBookmarks(FROM_HERE);
  ResetLastUsedBookmarkFolder(browserState->GetPrefs());

  // Checking whether managed bookmarks remain, in which case return false.
  if (bookmarkModel->HasBookmarks()) {
    return testing::NSErrorWithLocalizedDescription(
        @"Bookmark model is not empty. May have managed bookmarks.");
  }
  return nil;
}

+ (void)clearBookmarksPositionCache {
  ChromeBrowserState* browser_state =
      chrome_test_util::GetOriginalBrowserState();
  [BookmarkPathCache
      clearBookmarkTopMostRowCacheWithPrefService:browser_state->GetPrefs()];
}

+ (NSError*)setupStandardBookmarksUsingFirstURL:(NSString*)firstURL
                                      secondURL:(NSString*)secondURL
                                       thirdURL:(NSString*)thirdURL
                                      fourthURL:(NSString*)fourthURL
                                      inStorage:
                                          (BookmarkStorageType)storageType {
  NSError* bookmarkModelLoadedError =
      [BookmarkEarlGreyAppInterface waitForBookmarkModelLoaded];
  if (bookmarkModelLoadedError) {
    return bookmarkModelLoadedError;
  }
  bookmarks::BookmarkModel* bookmarkModel = GetBookmarkModel();

  NSString* firstTitle = @"First URL";
  const GURL firstGURL = GURL(base::SysNSStringToUTF8(firstURL));
  bookmarkModel->AddURL(GetMobileNodeWithType(storageType, bookmarkModel), 0,
                        base::SysNSStringToUTF16(firstTitle), firstGURL);

  NSString* secondTitle = @"Second URL";
  const GURL secondGURL = GURL(base::SysNSStringToUTF8(secondURL));
  bookmarkModel->AddURL(GetMobileNodeWithType(storageType, bookmarkModel), 0,
                        base::SysNSStringToUTF16(secondTitle), secondGURL);

  NSString* frenchTitle = @"French URL";
  const GURL thirdGURL = GURL(base::SysNSStringToUTF8(thirdURL));
  bookmarkModel->AddURL(GetMobileNodeWithType(storageType, bookmarkModel), 0,
                        base::SysNSStringToUTF16(frenchTitle), thirdGURL);

  NSString* folderTitle = @"Folder 1";
  const bookmarks::BookmarkNode* folder1 = bookmarkModel->AddFolder(
      GetMobileNodeWithType(storageType, bookmarkModel), 0,
      base::SysNSStringToUTF16(folderTitle));
  folderTitle = @"Folder 1.1";
  bookmarkModel->AddFolder(GetMobileNodeWithType(storageType, bookmarkModel), 0,
                           base::SysNSStringToUTF16(folderTitle));

  folderTitle = @"Folder 2";
  const bookmarks::BookmarkNode* folder2 = bookmarkModel->AddFolder(
      folder1, 0, base::SysNSStringToUTF16(folderTitle));

  folderTitle = @"Folder 3";
  const bookmarks::BookmarkNode* folder3 = bookmarkModel->AddFolder(
      folder2, 0, base::SysNSStringToUTF16(folderTitle));

  NSString* thirdTitle = @"Third URL";
  const GURL fourthGURL = GURL(base::SysNSStringToUTF8(fourthURL));
  bookmarkModel->AddURL(folder3, 0, base::SysNSStringToUTF16(thirdTitle),
                        fourthGURL);
  return nil;
}

+ (NSError*)setupBookmarksWhichExceedsScreenHeightUsingURL:(NSString*)URL
                                                 inStorage:(BookmarkStorageType)
                                                               storageType {
  NSError* waitForBookmarkModelLoadedError =
      [BookmarkEarlGreyAppInterface waitForBookmarkModelLoaded];
  if (waitForBookmarkModelLoadedError) {
    return waitForBookmarkModelLoadedError;
  }
  bookmarks::BookmarkModel* bookmarkModel = GetBookmarkModel();

  const GURL dummyURL = GURL(base::SysNSStringToUTF8(URL));
  bookmarkModel->AddURL(GetMobileNodeWithType(storageType, bookmarkModel), 0,
                        base::SysNSStringToUTF16(@"Bottom URL"), dummyURL);

  NSString* dummyTitle = @"Dummy URL";
  for (int i = 0; i < 20; i++) {
    bookmarkModel->AddURL(GetMobileNodeWithType(storageType, bookmarkModel), 0,
                          base::SysNSStringToUTF16(dummyTitle), dummyURL);
  }
  NSString* folderTitle = @"Folder 1";
  const bookmarks::BookmarkNode* folder1 = bookmarkModel->AddFolder(
      GetMobileNodeWithType(storageType, bookmarkModel), 0,
      base::SysNSStringToUTF16(folderTitle));
  bookmarkModel->AddURL(GetMobileNodeWithType(storageType, bookmarkModel), 0,
                        base::SysNSStringToUTF16(@"Top URL"), dummyURL);

  // Add URLs to Folder 1.
  bookmarkModel->AddURL(folder1, 0, base::SysNSStringToUTF16(dummyTitle),
                        dummyURL);
  bookmarkModel->AddURL(folder1, 0, base::SysNSStringToUTF16(@"Bottom 1"),
                        dummyURL);
  for (int i = 0; i < 20; i++) {
    bookmarkModel->AddURL(folder1, 0, base::SysNSStringToUTF16(dummyTitle),
                          dummyURL);
  }
  return nil;
}

+ (NSError*)waitForBookmarkModelLoaded {
  bookmarks::BookmarkModel* bookmarkModel = GetBookmarkModel();

  if (!base::test::ios::WaitUntilConditionOrTimeout(
          base::test::ios::kWaitForUIElementTimeout, ^{
            return bookmarkModel->loaded();
          })) {
    return testing::NSErrorWithLocalizedDescription(
        @"Bookmark model did not load");
  }
  return nil;
}

+ (void)commitPendingWrite {
  bookmarks::BookmarkModel* bookmarkModel = GetBookmarkModel();
  bookmarkModel->CommitPendingWriteForTest();
}

+ (void)setLastUsedBookmarkFolderToMobileBookmarksInStorageType:
    (BookmarkStorageType)storageType {
  bookmarks::BookmarkModel* bookmarkModel = GetBookmarkModel();
  const bookmarks::BookmarkNode* folder =
      GetMobileNodeWithType(storageType, bookmarkModel);
  SetLastUsedBookmarkFolder(
      chrome_test_util::GetOriginalBrowserState()->GetPrefs(), folder,
      storageType);
}

+ (NSError*)verifyBookmarksWithTitle:(NSString*)title
                       expectedCount:(NSUInteger)expectedCount
                           inStorage:(BookmarkStorageType)storageType {
  bookmarks::BookmarkModel* bookmarkModel = GetBookmarkModel();

  std::vector<const bookmarks::BookmarkNode*> nodes =
      GetBookmarksWithTitle(title, bookmarkModel, storageType);

  if (nodes.size() != expectedCount) {
    return testing::NSErrorWithLocalizedDescription(
        @"Unexpected number of bookmarks");
  }

  return nil;
}

+ (NSError*)addBookmarkWithTitle:(NSString*)title
                             URL:(NSString*)url
                       inStorage:(BookmarkStorageType)storageType {
  NSError* waitForBookmarkModelLoadedError =
      [BookmarkEarlGreyAppInterface waitForBookmarkModelLoaded];
  if (waitForBookmarkModelLoadedError) {
    return waitForBookmarkModelLoadedError;
  }

  GURL bookmarkURL = GURL(base::SysNSStringToUTF8(url));
  bookmarks::BookmarkModel* bookmarkModel = GetBookmarkModel();
  bookmarkModel->AddNewURL(GetMobileNodeWithType(storageType, bookmarkModel), 0,
                           base::SysNSStringToUTF16(title), bookmarkURL);

  return nil;
}

+ (NSError*)removeBookmarkWithTitle:(NSString*)title
                          inStorage:(BookmarkStorageType)storageType {
  bookmarks::BookmarkModel* bookmarkModel = GetBookmarkModel();
  const bookmarks::BookmarkNode* bookmark =
      GetFirstBookmarkWithTitle(title, bookmarkModel, storageType);
  if (!bookmark) {
    return testing::NSErrorWithLocalizedDescription([NSString
        stringWithFormat:@"Could not remove bookmark with name %@", title]);
  }

  bookmarkModel->Remove(bookmark, bookmarks::metrics::BookmarkEditSource::kUser,
                        FROM_HERE);
  return nil;
}

+ (NSError*)moveBookmarkWithTitle:(NSString*)bookmarkTitle
                toFolderWithTitle:(NSString*)newFolderTitle
                        inStorage:(BookmarkStorageType)storageType {
  bookmarks::BookmarkModel* bookmarkModel = GetBookmarkModel();

  const bookmarks::BookmarkNode* bookmark =
      GetFirstBookmarkWithTitle(bookmarkTitle, bookmarkModel, storageType);
  if (!bookmark) {
    return testing::NSErrorWithLocalizedDescription([NSString
        stringWithFormat:@"Could not move inexistent bookmark with title %@",
                         bookmarkTitle]);
  }

  const bookmarks::BookmarkNode* newFolder =
      GetFirstBookmarkWithTitle(newFolderTitle, bookmarkModel, storageType);
  if (!newFolder || !newFolder->is_folder()) {
    return testing::NSErrorWithLocalizedDescription([NSString
        stringWithFormat:
            @"Could not move bookmark to inexistent folder with title %@",
            newFolderTitle]);
  }

  std::vector<const bookmarks::BookmarkNode*> toMove{bookmark};
  if (!bookmark_utils_ios::MoveBookmarks(toMove, bookmarkModel, newFolder)) {
    return testing::NSErrorWithLocalizedDescription(
        [NSString stringWithFormat:@"Could not move bookmark with name %@",
                                   bookmarkTitle]);
  }

  return nil;
}

+ (NSError*)verifyChildCount:(size_t)count
            inFolderWithName:(NSString*)name
                   inStorage:(BookmarkStorageType)storageType {
  bookmarks::BookmarkModel* bookmarkModel = GetBookmarkModel();

  const bookmarks::BookmarkNode* folder =
      GetFirstBookmarkWithTitle(name, bookmarkModel, storageType);
  if (!folder || !folder->is_folder()) {
    return testing::NSErrorWithLocalizedDescription(
        [NSString stringWithFormat:@"No folder named %@", name]);
  }

  if (folder->children().size() != count) {
    return testing::NSErrorWithLocalizedDescription(
        [NSString stringWithFormat:
                      @"Unexpected number of children in folder '%@': %" PRIuS
                       " instead of %" PRIuS,
                      name, folder->children().size(), count]);
  }

  return nil;
}

+ (NSError*)verifyExistenceOfBookmarkWithURL:(NSString*)URL
                                        name:(NSString*)name
                                   inStorage:(BookmarkStorageType)storageType {
  bookmarks::BookmarkModel* bookmarkModel = GetBookmarkModel();

  for (const bookmarks::BookmarkNode* bookmark :
       bookmarkModel->GetNodesByURL(GURL(base::SysNSStringToUTF16(URL)))) {
    if (bookmark_utils_ios::GetBookmarkStorageType(bookmark, bookmarkModel) ==
            storageType &&
        bookmark->GetTitle().compare(base::SysNSStringToUTF16(name)) == 0) {
      return nil;
    }
  }

  return testing::NSErrorWithLocalizedDescription([NSString
      stringWithFormat:@"Could not find bookmark named %@ for %@", name, URL]);
}

+ (NSError*)verifyAbsenceOfBookmarkWithURL:(NSString*)URL
                                 inStorage:(BookmarkStorageType)storageType {
  bookmarks::BookmarkModel* bookmarkModel = GetBookmarkModel();

  for (const bookmarks::BookmarkNode* bookmark :
       bookmarkModel->GetNodesByURL(GURL(base::SysNSStringToUTF16(URL)))) {
    if (bookmark_utils_ios::GetBookmarkStorageType(bookmark, bookmarkModel) ==
        storageType) {
      return testing::NSErrorWithLocalizedDescription(
          [NSString stringWithFormat:@"There is a bookmark for %@", URL]);
    }
  }

  return nil;
}

+ (NSError*)verifyExistenceOfFolderWithTitle:(NSString*)title
                                   inStorage:(BookmarkStorageType)storageType {
  bookmarks::BookmarkModel* bookmarkModel = GetBookmarkModel();
  const bookmarks::BookmarkNode* folder =
      GetFirstBookmarkWithTitle(title, bookmarkModel, storageType);
  if (!folder || !folder->is_folder()) {
    return testing::NSErrorWithLocalizedDescription(
        [NSString stringWithFormat:@"Folder %@ doesn't exist", title]);
  }

  return nil;
}

+ (NSError*)verifyPromoAlreadySeen:(BOOL)seen {
  ChromeBrowserState* browserState =
      chrome_test_util::GetOriginalBrowserState();
  PrefService* prefs = browserState->GetPrefs();
  if (prefs->GetBoolean(prefs::kIosBookmarkPromoAlreadySeen) == seen) {
    return nil;
  }
  NSString* errorDescription =
      seen ? @"Expected promo already seen, but it wasn't."
           : @"Expected promo not already seen, but it was.";
  return testing::NSErrorWithLocalizedDescription(errorDescription);
}

+ (void)setPromoAlreadySeen:(BOOL)seen {
  PrefService* prefs = chrome_test_util::GetOriginalBrowserState()->GetPrefs();
  prefs->SetBoolean(prefs::kIosBookmarkPromoAlreadySeen, seen);
}

+ (void)setPromoAlreadySeenNumberOfTimes:(int)times {
  PrefService* prefs = chrome_test_util::GetOriginalBrowserState()->GetPrefs();
  prefs->SetInteger(prefs::kIosBookmarkSigninPromoDisplayedCount, times);
}

+ (int)numberOfTimesPromoAlreadySeen {
  PrefService* prefs = chrome_test_util::GetOriginalBrowserState()->GetPrefs();
  return prefs->GetInteger(prefs::kIosBookmarkSigninPromoDisplayedCount);
}

@end