chromium/ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_egtest.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 "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "components/sync/base/features.h"
#import "components/url_formatter/elide_url.h"
#import "components/visited_url_ranking/public/features.h"
#import "ios/chrome/browser/ntp_tiles/model/tab_resumption/tab_resumption_prefs.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/browser/start_surface/ui_bundled/start_surface_features.h"
#import "ios/chrome/browser/tabs/ui_bundled/tests/distant_tabs_app_interface.h"
#import "ios/chrome/browser/tabs/ui_bundled/tests/fake_distant_tab.h"
#import "ios/chrome/browser/ui/authentication/signin_earl_grey.h"
#import "ios/chrome/browser/ui/authentication/signin_earl_grey_ui_test_util.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_constants.h"
#import "ios/chrome/browser/ui/content_suggestions/magic_stack/magic_stack_constants.h"
#import "ios/chrome/browser/ui/content_suggestions/new_tab_page_app_interface.h"
#import "ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_constants.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_constants.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/testing/earl_grey/app_launch_manager.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "net/test/embedded_test_server/request_handler_util.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

// Timeout in seconds to wait for asynchronous sync operations.
constexpr base::TimeDelta kSyncOperationTimeout = base::Seconds(10);

// Sign in and enable history/tab sync using a fake identity.
void SignInAndEnableHistorySync() {
  FakeSystemIdentity* fake_identity = [FakeSystemIdentity fakeIdentity1];
  [SigninEarlGrey addFakeIdentity:fake_identity];
  [SigninEarlGreyUI signinWithFakeIdentity:fake_identity enableHistorySync:YES];
  [ChromeEarlGrey
      waitForSyncTransportStateActiveWithTimeout:kSyncOperationTimeout];
}

// Checks that the visibility of the tab resumption tile matches `should_show`.
void WaitUntilTabResumptionTileVisibleOrTimeout(bool should_show) {
  id<GREYMatcher> matcher =
      should_show ? grey_sufficientlyVisible() : grey_notVisible();
  ConditionBlock condition = ^{
    NSError* error = nil;
    [[EarlGrey
        selectElementWithMatcher:
            grey_allOf(
                grey_accessibilityID(
                    kMagicStackContentSuggestionsModuleTabResumptionAccessibilityIdentifier),
                grey_sufficientlyVisible(), nil)] assertWithMatcher:matcher
                                                              error:&error];
    return error == nil;
  };

  NSString* failure_reason = @"Tile visible.";
  if (should_show) {
    failure_reason = @"Tile did not appear.";
  }
  GREYAssert(
      base::test::ios::WaitUntilConditionOrTimeout(base::Seconds(6), condition),
      failure_reason);
}

// Returns a GREYMatcher for the given label.
id<GREYMatcher> TabResumptionLabelMatcher(NSString* label) {
  return grey_allOf(
      grey_ancestor(grey_accessibilityID(kTabResumptionViewIdentifier)),
      grey_text(label), nil);
}

// Returns the hostname from the given `URL`.
NSString* HostnameFromGURL(GURL URL) {
  return base::SysUTF16ToNSString(
      url_formatter::
          FormatUrlForDisplayOmitSchemePathTrivialSubdomainsAndMobilePrefix(
              URL));
}

}  // namespace

// Test case for the tab reusmption tile.
@interface TabResumptionTestCase : ChromeTestCase

@end

@implementation TabResumptionTestCase

- (BOOL)isUsingTabResumption15 {
  return [self.name containsString:@"TR15"];
}

- (BOOL)isUsingTabResumption2 {
  return [self.name containsString:@"TR2"];
}

- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config;
  if ([self isUsingTabResumption15]) {
    config.features_enabled.push_back(kTabResumption1_5);
  } else {
    config.features_disabled.push_back(kTabResumption1_5);
  }
  if ([self isUsingTabResumption2]) {
    config.features_enabled.push_back(kTabResumption2);
  } else {
    config.features_disabled.push_back(kTabResumption2);
  }
  config.additional_args.push_back(std::string("--") +
                                   kTabResumptionShowItemImmediately);
  config.additional_args.push_back("--test-ios-module-ranker=tab_resumption");
  // kVisitedURLRankingHistoryVisibilityScoreFilter require the network, keep
  // it disabled for tests.
  config.features_disabled.push_back(
      visited_url_ranking::features::
          kVisitedURLRankingHistoryVisibilityScoreFilter);
  return config;
}

// Relaunches the app with start surface enabled.
- (void)relaunchAppWithStartSurfaceEnabled {
  AppLaunchConfiguration config = [self appConfigurationForTestCase];
  config.relaunch_policy = ForceRelaunchByCleanShutdown;
  config.additional_args.push_back(
      "--enable-features=" + std::string(kStartSurface.name) + "<" +
      std::string(kStartSurface.name));
  config.additional_args.push_back(
      "--force-fieldtrials=" + std::string(kStartSurface.name) + "/Test");
  config.additional_args.push_back(
      "--force-fieldtrial-params=" + std::string(kStartSurface.name) +
      ".Test:" + std::string(kReturnToStartSurfaceInactiveDurationInSeconds) +
      "/" + "0");
  [[AppLaunchManager sharedManager] ensureAppLaunchedWithConfiguration:config];
}

- (void)setUp {
  [super setUp];
  [ChromeEarlGrey clearBrowsingHistory];
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  SignInAndEnableHistorySync();
  [NewTabPageAppInterface disableSetUpList];
  [[self class] closeAllTabs];
  [ChromeEarlGrey openNewTab];
}

- (void)tearDown {
  [SigninEarlGrey signOut];
  [ChromeEarlGrey waitForSyncEngineInitialized:NO
                                   syncTimeout:kSyncOperationTimeout];
  [ChromeEarlGrey clearFakeSyncServerData];
  [ChromeEarlGrey resetDataForLocalStatePref:tab_resumption_prefs::
                                                 kTabResumptioDisabledPref];
  [ChromeEarlGrey clearUserPrefWithName:tab_resumption_prefs::
                                            kTabResumptionLastOpenedTabURLPref];
  [super tearDown];
}

// Tests that the tab resumption tile is correctly displayed for a distant tab.
- (void)testTabResumptionTileDisplayedForDistantTab {
  // Check that the tile is not displayed when there is no distant tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(false);

  // Create a distant session with 4 tabs.
  [DistantTabsAppInterface
      addSessionToFakeSyncServer:@"Desktop"
               modifiedTimeDelta:base::Minutes(5)
                            tabs:[FakeDistantTab
                                     createFakeTabsForServerURL:self.testServer
                                                                    ->base_url()
                                                   numberOfTabs:4]];
  [ChromeEarlGrey triggerSyncCycleForType:syncer::SESSIONS];

  // Check that the tile is displayed when there is a distant tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(true);
  [[EarlGrey
      selectElementWithMatcher:TabResumptionLabelMatcher(@"FROM \"DESKTOP\"")]
      assertWithMatcher:grey_sufficientlyVisible()];
  // Tab resumption 2 displays Tab0 in that case.
  NSString* displayedTab = [self isUsingTabResumption2] ? @"Tab 0" : @"Tab 3";
  [[EarlGrey selectElementWithMatcher:TabResumptionLabelMatcher(displayedTab)]
      assertWithMatcher:grey_sufficientlyVisible()];
  NSString* footerLabel =
      [NSString stringWithFormat:@"%@ • %@",
                                 HostnameFromGURL(self.testServer->base_url()),
                                 @"5 mins ago"];
  [[EarlGrey selectElementWithMatcher:TabResumptionLabelMatcher(footerLabel)]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Tap on the tab resumption item.
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kTabResumptionViewIdentifier)]
      performAction:grey_tap()];

  // Verify that the location bar shows the distant tab URL in a short form.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::DefocusedLocationView()]
      assertWithMatcher:chrome_test_util::LocationViewContainingText(
                            self.testServer->base_url().host())];
}

// Tests that the tab resumption 2 tile is correctly displayed for a distant
// tab.
// TODO(crbug.com/346713831): This test timed out on some configs.
- (void)DISABLED_testTabResumptionTileDisplayedForDistantTabTR2 {
  [self testTabResumptionTileDisplayedForDistantTab];
}

// Tests that the tab resumption tile is correctly displayed for a local tab.
- (void)testTabResumptionTileDisplayedForLocalTab {
  // TODO(crbug.com/333500324): Test failing on iPad device and simulator.
  if ([ChromeEarlGrey isIPadIdiom]) {
    EARL_GREY_TEST_DISABLED(@"Test is flaky on iPad.")
  }

  // Check that the tile is not displayed when there is no local tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(false);

  const GURL destinationUrl = self.testServer->GetURL("/pony.html");
  [ChromeEarlGrey loadURL:destinationUrl];

  // Relaunch the app.
  [self relaunchAppWithStartSurfaceEnabled];

  // Check that the tile is displayed when there is a local tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(true);
  [[EarlGrey selectElementWithMatcher:TabResumptionLabelMatcher(@"ponies")]
      assertWithMatcher:grey_sufficientlyVisible()];
  NSString* footerLabel =
      [NSString stringWithFormat:@"%@ • %@",
                                 HostnameFromGURL(self.testServer->base_url()),
                                 @"just now"];
  [[EarlGrey selectElementWithMatcher:TabResumptionLabelMatcher(footerLabel)]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Tap on the tab resumption item.
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kTabResumptionViewIdentifier)]
      performAction:grey_tap()];

  // Verify that the location bar shows the local tab URL in a short form.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::DefocusedLocationView()]
      assertWithMatcher:chrome_test_util::LocationViewContainingText(
                            destinationUrl.host())];
  [ChromeEarlGrey
      waitForWebStateContainingText:"Anyone know any good pony jokes?"];
}

// Tests that the tab resumption 2 tile is correctly displayed for a local tab.
- (void)testTabResumptionTileDisplayedForLocalTabTR2 {
  [self testTabResumptionTileDisplayedForLocalTab];
}

// Tests that interacting with the Magic Stack edit button works when the tab
// resumption tile is displayed.
- (void)testInteractWithAnotherTile {
  // Check that the tile is not displayed when there is no distant tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(false);

  // Create a distant session with 4 tabs.
  [DistantTabsAppInterface
      addSessionToFakeSyncServer:@"Desktop"
               modifiedTimeDelta:base::Minutes(5)
                            tabs:[FakeDistantTab
                                     createFakeTabsForServerURL:self.testServer
                                                                    ->base_url()
                                                   numberOfTabs:4]];
  [ChromeEarlGrey triggerSyncCycleForType:syncer::SESSIONS];

  // Check that the tile is displayed when there is a distant tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(true);

  [[[EarlGrey selectElementWithMatcher:
                  grey_allOf(grey_accessibilityID(
                                 kMagicStackEditButtonAccessibilityIdentifier),
                             grey_sufficientlyVisible(), nil)]
         usingSearchAction:grey_swipeFastInDirection(kGREYDirectionLeft)
      onElementWithMatcher:grey_accessibilityID(
                               kMagicStackScrollViewAccessibilityIdentifier)]
      assertWithMatcher:grey_sufficientlyVisible()];
}

// Tests that the tab resumption 2 tile is correctly displayed for a distant
// tab.
- (void)testInteractWithAnotherTileTR2 {
  [self testInteractWithAnotherTile];
}

// Tests that the context menu has the correct action and correctly hides the
// tile.
- (void)testTabResumptionTileLongPress {
  // Check that the tile is not displayed when there is no distant tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(false);

  // Create a distant session with 4 tabs.
  [DistantTabsAppInterface
      addSessionToFakeSyncServer:@"Desktop"
               modifiedTimeDelta:base::Minutes(5)
                            tabs:[FakeDistantTab
                                     createFakeTabsForServerURL:self.testServer
                                                                    ->base_url()
                                                   numberOfTabs:4]];
  [ChromeEarlGrey triggerSyncCycleForType:syncer::SESSIONS];

  // Check that the tile is displayed when there is a distant tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(true);

  // Long press the distant tab.
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kTabResumptionViewIdentifier)]
      performAction:grey_longPress()];
  [[EarlGrey selectElementWithMatcher:
                 grey_text(l10n_util::GetNSString(
                     IDS_IOS_TAB_RESUMPTION_CONTEXT_MENU_DESCRIPTION))]
      assertWithMatcher:grey_notNil()];
  [[EarlGrey selectElementWithMatcher:
                 grey_text(l10n_util::GetNSString(
                     IDS_IOS_MAGIC_STACK_CONTEXT_MENU_CUSTOMIZE_CARDS_TITLE))]
      assertWithMatcher:grey_notNil()];
  [[EarlGrey selectElementWithMatcher:
                 grey_text(l10n_util::GetNSString(
                     IDS_IOS_TAB_RESUMPTION_CONTEXT_MENU_DESCRIPTION))]
      performAction:grey_tap()];

  // Check that the tile is hidden.
  WaitUntilTabResumptionTileVisibleOrTimeout(false);
}

// Tests that the context menu has the correct action and correctly hides the
// tile.
// TODO(crbug.com/333500324): Test is flaky.
- (void)FLAKY_testTabResumptionTileLongPressTR2 {
  [self testTabResumptionTileLongPress];
}

- (void)testShowMoreVisibleTR15 {
  // Check that the tile is not displayed when there is no local tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(false);

  const GURL destinationUrl = self.testServer->GetURL("/pony.html");
  [ChromeEarlGrey loadURL:destinationUrl];

  // Relaunch the app.
  [self relaunchAppWithStartSurfaceEnabled];

  // Check that the tile is displayed when there is a local tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(true);
  [[EarlGrey
      selectElementWithMatcher:grey_allOf(grey_accessibilityID(@"See More"),
                                          grey_sufficientlyVisible(), nil)]
      performAction:grey_tap()];
  [[EarlGrey
      selectElementWithMatcher:
          grey_allOf(grey_accessibilityID(
                         kRecentTabsTableViewControllerAccessibilityIdentifier),
                     grey_sufficientlyVisible(), nil)]
      assertWithMatcher:grey_sufficientlyVisible()];
}

- (void)testShowMoreNotVisible {
  // Check that the tile is not displayed when there is no local tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(false);

  const GURL destinationUrl = self.testServer->GetURL("/pony.html");
  [ChromeEarlGrey loadURL:destinationUrl];

  // Relaunch the app.
  [self relaunchAppWithStartSurfaceEnabled];

  // Check that the tile is displayed when there is a local tab.
  WaitUntilTabResumptionTileVisibleOrTimeout(true);
  NSError* error = nil;
  [[EarlGrey
      selectElementWithMatcher:grey_allOf(grey_accessibilityID(@"See More"),
                                          grey_sufficientlyVisible(), nil)]
      assertWithMatcher:grey_sufficientlyVisible()
                  error:&error];
  GREYAssertTrue(error, @"See more button is visible.");
}

@end