chromium/ios/chrome/browser/ui/content_suggestions/content_suggestions_egtest.mm

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

#import <memory>
#import <vector>

#import "base/apple/foundation_util.h"
#import "base/functional/bind.h"
#import "base/ios/ios_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "components/segmentation_platform/public/constants.h"
#import "components/segmentation_platform/public/features.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/first_run/ui_bundled/first_run_constants.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_constants.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_feature.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/browser/signin/model/test_constants.h"
#import "ios/chrome/browser/ui/authentication/signin/signin_constants.h"
#import "ios/chrome/browser/ui/authentication/signin_earl_grey.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/ntp_home_constant.h"
#import "ios/chrome/browser/ui/content_suggestions/set_up_list/constants.h"
#import "ios/chrome/common/ui/confirmation_alert/constants.h"
#import "ios/chrome/common/ui/promo_style/constants.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/earl_grey/chrome_actions.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey_ui.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/base/apple/url_conversions.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "net/test/embedded_test_server/http_request.h"
#import "net/test/embedded_test_server/http_response.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/strings/grit/ui_strings.h"

namespace {

const char kPageLoadedString[] = "Page loaded!";
const char kPageURL[] = "/test-page.html";
const char kPageTitle[] = "Page title!";

// Provides responses for redirect and changed window location URLs.
std::unique_ptr<net::test_server::HttpResponse> StandardResponse(
    const net::test_server::HttpRequest& request) {
  if (request.relative_url != kPageURL) {
    return nullptr;
  }
  std::unique_ptr<net::test_server::BasicHttpResponse> http_response =
      std::make_unique<net::test_server::BasicHttpResponse>();
  http_response->set_code(net::HTTP_OK);
  http_response->set_content("<html><head><title>" + std::string(kPageTitle) +
                             "</title></head><body>" +
                             std::string(kPageLoadedString) + "</body></html>");
  return std::move(http_response);
}

// Taps the view with the given `accessibility_id`.
void TapView(NSString* accessibility_id) {
  id<GREYMatcher> matcher = grey_accessibilityID(accessibility_id);
  [[EarlGrey selectElementWithMatcher:matcher] assertWithMatcher:grey_notNil()];
  [[EarlGrey selectElementWithMatcher:matcher] performAction:grey_tap()];
}

// Tap the PromoStyleSecondaryActionButton.
void TapPromoStyleSecondaryActionButton() {
  id<GREYMatcher> button =
      grey_accessibilityID(kPromoStyleSecondaryActionAccessibilityIdentifier);
  [[EarlGrey selectElementWithMatcher:button] assertWithMatcher:grey_notNil()];
  [[EarlGrey selectElementWithMatcher:button] performAction:grey_tap()];
}

// Tap the ConfirmationAlertSecondaryAction Button.
void TapSecondaryActionButton() {
  id<GREYMatcher> button = grey_accessibilityID(
      kConfirmationAlertSecondaryActionAccessibilityIdentifier);
  [[EarlGrey selectElementWithMatcher:button] assertWithMatcher:grey_notNil()];
  [[EarlGrey selectElementWithMatcher:button] performAction:grey_tap()];
}

}  // namespace

#pragma mark - TestCase

// Test case for the ContentSuggestion UI.
@interface ContentSuggestionsTestCase : ChromeTestCase

@end

@implementation ContentSuggestionsTestCase

#pragma mark - Setup/Teardown

- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config;
  config.features_enabled.push_back(kEnableFeedAblation);
  config.additional_args.push_back("--test-ios-module-ranker=mvt");
  if ([self isRunningTest:@selector
            (DISABLED_testMagicStackSetUpListCompleteAllItems)] ||
      [self isRunningTest:@selector(testMagicStackEditButton)] ||
      [self isRunningTest:@selector
            (testMagicStackCompactedSetUpListCompleteAllItems)]) {
    config.features_disabled.push_back(kContentPushNotifications);
    config.features_disabled.push_back(kIOSTipsNotifications);
  }
  if ([self isRunningTest:@selector(testMVTInMagicStack)]) {
    std::string enable_mvt_arg = std::string(kMagicStack.name) + ":" +
                                 kMagicStackMostVisitedModuleParam + "/true";
    config.additional_args.push_back("--enable-features=" + enable_mvt_arg);
  }
  return config;
}

+ (void)setUpForTestCase {
  [super setUpForTestCase];
  [self setUpHelper];
}

+ (void)setUpHelper {
  [self closeAllTabs];
}

+ (void)tearDown {
  [self closeAllTabs];

  [super tearDown];
}

- (void)setUp {
  [super setUp];
}

- (void)tearDown {
  [ChromeEarlGrey clearBrowsingHistory];
  [ChromeEarlGrey removeFirstRunSentinel];
  [super tearDown];
}

#pragma mark - Tests

// Tests the "Open in New Tab" action of the Most Visited context menu.
- (void)testMostVisitedNewTab {
  [self setupMostVisitedTileLongPress];
  const GURL pageURL = self.testServer->GetURL(kPageURL);

  // Open in new tab.
  [[EarlGrey selectElementWithMatcher:
                 chrome_test_util::ContextMenuItemWithAccessibilityLabelId(
                     IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWTAB)]
      performAction:grey_tap()];

  // Check a new page in normal model is opened.
  [ChromeEarlGrey waitForMainTabCount:2];
  [ChromeEarlGrey waitForIncognitoTabCount:0];

  // Check that the tab has been opened in background.
  ConditionBlock condition = ^{
    NSError* error = nil;
    [[EarlGrey selectElementWithMatcher:chrome_test_util::NTPCollectionView()]
        assertWithMatcher:grey_notNil()
                    error:&error];
    return error == nil;
  };
  GREYAssert(base::test::ios::WaitUntilConditionOrTimeout(
                 base::test::ios::kWaitForUIElementTimeout, condition),
             @"Collection view not visible");

  // Check the page has been correctly opened.
  [ChromeEarlGrey selectTabAtIndex:1];
  [ChromeEarlGrey waitForWebStateContainingText:kPageLoadedString];
  [[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(
                                          pageURL.GetContent())]
      assertWithMatcher:grey_notNil()];
}

// Tests the "Open in New Incognito Tab" action of the Most Visited context
// menu.
- (void)testMostVisitedNewIncognitoTab {
  [self setupMostVisitedTileLongPress];
  const GURL pageURL = self.testServer->GetURL(kPageURL);

  // Open in new incognito tab.
  [[EarlGrey
      selectElementWithMatcher:chrome_test_util::OpenLinkInIncognitoButton()]
      performAction:grey_tap()];

  [ChromeEarlGrey waitForMainTabCount:1];
  [ChromeEarlGrey waitForIncognitoTabCount:1];

  // Check that the tab has been opened in foreground.
  [ChromeEarlGrey waitForWebStateContainingText:kPageLoadedString];
  [[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(
                                          pageURL.GetContent())]
      assertWithMatcher:grey_notNil()];

  GREYAssertTrue([ChromeEarlGrey isIncognitoMode],
                 @"Test did not switch to incognito");
}

// Tests the "Remove" action of the Most Visited context menu, and the "Undo"
// action.
// TODO(crbug.com/337064665): Test is flaky on simluator. Re-enable when fixed.
#if TARGET_IPHONE_SIMULATOR
#define MAYBE_testMostVisitedRemoveUndo FLAKY_testMostVisitedRemoveUndo
#else
#define MAYBE_testMostVisitedRemoveUndo testMostVisitedRemoveUndo
#endif
- (void)MAYBE_testMostVisitedRemoveUndo {
  [self setupMostVisitedTileLongPress];
  const GURL pageURL = self.testServer->GetURL(kPageURL);
  NSString* pageTitle = base::SysUTF8ToNSString(kPageTitle);

  // Tap on remove.
  [[EarlGrey selectElementWithMatcher:
                 chrome_test_util::ContextMenuItemWithAccessibilityLabelId(
                     IDS_IOS_CONTENT_SUGGESTIONS_REMOVE)]
      performAction:grey_tap()];

  // Check the tile is removed.
  [[EarlGrey
      selectElementWithMatcher:
          grey_allOf(
              chrome_test_util::StaticTextWithAccessibilityLabel(pageTitle),
              grey_sufficientlyVisible(), nil)] assertWithMatcher:grey_nil()];

  // Check the snack bar notifying the user that an element has been removed is
  // displayed.
  [[EarlGrey
      selectElementWithMatcher:
          grey_accessibilityID(@"MDCSnackbarMessageTitleAutomationIdentifier")]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Tap on undo.
  [[EarlGrey
      selectElementWithMatcher:chrome_test_util::ButtonWithAccessibilityLabelId(
                                   IDS_NEW_TAB_UNDO_THUMBNAIL_REMOVE)]
      performAction:grey_tap()];

  // Check the tile is back.
  ConditionBlock condition = ^{
    NSError* error = nil;
    [[EarlGrey
        selectElementWithMatcher:
            grey_allOf(
                chrome_test_util::StaticTextWithAccessibilityLabel(pageTitle),
                grey_sufficientlyVisible(), nil)]
        assertWithMatcher:grey_notNil()
                    error:&error];
    return error == nil;
  };
  NSString* errorMessage =
      @"The tile wasn't added back after hitting 'Undo' on the snackbar";
  GREYAssert(base::test::ios::WaitUntilConditionOrTimeout(
                 base::test::ios::kWaitForUIElementTimeout, condition),
             errorMessage);

  [[EarlGrey selectElementWithMatcher:
                 grey_allOf(chrome_test_util::StaticTextWithAccessibilityLabel(
                                pageTitle),
                            grey_sufficientlyVisible(), nil)]
      assertWithMatcher:grey_notNil()];
}

// Tests that the context menu has the correct actions.
- (void)testMostVisitedLongPress {
  [self setupMostVisitedTileLongPress];

  // No "Add to Reading List" item.
  [[EarlGrey
      selectElementWithMatcher:chrome_test_util::ButtonWithAccessibilityLabelId(
                                   IDS_IOS_CONTENT_CONTEXT_ADDTOREADINGLIST)]
      assertWithMatcher:grey_nil()];
}

// Tests that the "All Set" module is shown after completing all Set Up List
// Hero Cell modules in the Magic Stack.
// TODO(crbug.com/41493926): Test is flaky, re-enable when fixed.
- (void)DISABLED_testMagicStackSetUpListCompleteAllItems {
  [self prepareToTestSetUpListInMagicStack];

  // Tap the default browser item.
  TapView(set_up_list::kDefaultBrowserItemID);
  // Ensure the Default Browser Promo is displayed.
  id<GREYMatcher> defaultBrowserView = grey_accessibilityID(
      first_run::kFirstRunDefaultBrowserScreenAccessibilityIdentifier);
  [[EarlGrey selectElementWithMatcher:defaultBrowserView]
      assertWithMatcher:grey_notNil()];
  // Dismiss Default Browser Promo.
  TapPromoStyleSecondaryActionButton();

  ConditionBlock condition = ^{
    NSError* error = nil;
    [[EarlGrey
        selectElementWithMatcher:grey_allOf(grey_accessibilityID(
                                                set_up_list::kAutofillItemID),
                                            grey_sufficientlyVisible(), nil)]
        assertWithMatcher:grey_notNil()
                    error:&error];
    return error == nil;
  };
  GREYAssert(
      base::test::ios::WaitUntilConditionOrTimeout(base::Seconds(2), condition),
      @"Timeout waiting for Autofill Set Up List Item expired.");
  // Tap the autofill item.
  TapView(set_up_list::kAutofillItemID);
  // TODO - verify the CPE promo is displayed.
  id<GREYMatcher> CPEPromoView =
      grey_accessibilityID(@"kCredentialProviderPromoAccessibilityId");
  [[EarlGrey selectElementWithMatcher:CPEPromoView]
      assertWithMatcher:grey_notNil()];
  // Dismiss the CPE promo.
  TapSecondaryActionButton();

  condition = ^{
    NSError* error = nil;
    [[EarlGrey
        selectElementWithMatcher:grey_allOf(grey_accessibilityID(
                                                set_up_list::kSignInItemID),
                                            grey_sufficientlyVisible(), nil)]
        assertWithMatcher:grey_notNil()
                    error:&error];
    return error == nil;
  };
  GREYAssert(
      base::test::ios::WaitUntilConditionOrTimeout(base::Seconds(2), condition),
      @"Timeout waiting for Sign in Set Up List Item expired.");

  // Tap the signin item.
  TapView(set_up_list::kSignInItemID);
  [ChromeEarlGreyUI waitForAppToIdle];
  // The fake signin UI appears. Dismiss it.
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kFakeAuthCancelButtonIdentifier)]
      performAction:grey_tap()];

  // Verify the All Set item is shown.
  condition = ^{
    NSError* error = nil;
    [[EarlGrey
        selectElementWithMatcher:grey_allOf(grey_accessibilityID(
                                                set_up_list::kAllSetItemID),
                                            grey_sufficientlyVisible(), nil)]
        assertWithMatcher:grey_sufficientlyVisible()
                    error:&error];
    return error == nil;
  };
  GREYAssert(
      base::test::ios::WaitUntilConditionOrTimeout(base::Seconds(2), condition),
      @"Timeout waiting for the All Set Module to show expired.");
}

// Attempts to complete the Set Up List through the Compacted Magic Stack
// module.
- (void)testMagicStackCompactedSetUpListCompleteAllItems {
  [self prepareToTestSetUpListInMagicStack];

  // Tap the default browser item.
  TapView(set_up_list::kDefaultBrowserItemID);
  // Ensure the Default Browser Promo is displayed.
  id<GREYMatcher> defaultBrowserView = grey_accessibilityID(
      first_run::kFirstRunDefaultBrowserScreenAccessibilityIdentifier);
  [[EarlGrey selectElementWithMatcher:defaultBrowserView]
      assertWithMatcher:grey_notNil()];
  // Dismiss Default Browser Promo.
  TapPromoStyleSecondaryActionButton();

  ConditionBlock condition = ^{
    return [NewTabPageAppInterface
        setUpListItemDefaultBrowserInMagicStackIsComplete];
  };
  GREYAssert(
      base::test::ios::WaitUntilConditionOrTimeout(base::Seconds(2), condition),
      @"SetUpList item Default Browser not completed.");

  // Tap the autofill item.
  TapView(set_up_list::kAutofillItemID);
  id<GREYMatcher> CPEPromoView =
      grey_accessibilityID(@"kCredentialProviderPromoAccessibilityId");
  [[EarlGrey selectElementWithMatcher:CPEPromoView]
      assertWithMatcher:grey_notNil()];
  // Dismiss the CPE promo.
  TapSecondaryActionButton();

  condition = ^{
    return [NewTabPageAppInterface setUpListItemAutofillInMagicStackIsComplete];
  };
  GREYAssert(
      base::test::ios::WaitUntilConditionOrTimeout(base::Seconds(2), condition),
      @"SetUpList item Autofill not completed.");

  // Completed Set Up List items last one impression
  [ChromeEarlGrey closeAllTabs];
  [ChromeEarlGrey openNewTab];
  [ChromeEarlGrey closeAllTabs];
  [ChromeEarlGrey openNewTab];

  // Tap the signin item.
  TapView(set_up_list::kSignInItemID);
  [ChromeEarlGreyUI waitForAppToIdle];
  // The fake signin UI appears. Dismiss it.
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kFakeAuthCancelButtonIdentifier)]
      performAction:grey_tap()];

  // Verify the All Set item is shown.
  condition = ^{
    NSError* error = nil;
    [[EarlGrey
        selectElementWithMatcher:grey_allOf(grey_accessibilityID(
                                                set_up_list::kAllSetItemID),
                                            grey_sufficientlyVisible(), nil)]
        assertWithMatcher:grey_sufficientlyVisible()
                    error:&error];
    return error == nil;
  };
  GREYAssert(
      base::test::ios::WaitUntilConditionOrTimeout(base::Seconds(2), condition),
      @"Timeout waiting for the All Set Module to show expired.");
}

// Tests the edit button in the Magic Stack. Opens the edit half sheet, disables
// Set Up List, returns to the Magic Stack and ensures Set Up List is not in the
// Magic Stack anymore.
- (void)testMagicStackEditButton {
  [self prepareToTestSetUpListInMagicStack];

  // Swipe all the way over to the end of the Magic Stack.
  [[[EarlGrey selectElementWithMatcher:
                  grey_allOf(grey_accessibilityID(
                                 kMagicStackEditButtonAccessibilityIdentifier),
                             grey_sufficientlyVisible(), nil)]
         usingSearchAction:grey_swipeFastInDirection(kGREYDirectionLeft)
      onElementWithMatcher:grey_accessibilityID(
                               kMagicStackScrollViewAccessibilityIdentifier)]
      performAction:grey_tap()];

  // Verify edit half sheet is visible.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(l10n_util::GetNSString(
                                   IDS_IOS_MAGIC_STACK_EDIT_MODAL_TITLE))]
      assertWithMatcher:grey_sufficientlyVisible()];

  id<GREYMatcher> setUpToggle =
      grey_allOf(grey_accessibilityID([NewTabPageAppInterface setUpListTitle]),
                 grey_sufficientlyVisible(), nil);
  // Assert Set Up List toggle is on, and then turn if off.
  [[EarlGrey selectElementWithMatcher:setUpToggle]
      performAction:chrome_test_util::TurnTableViewSwitchOn(NO)];

  // Dismiss
  [[EarlGrey selectElementWithMatcher:
                 grey_accessibilityID(
                     kMagicStackEditHalfSheetDoneButtonAccessibilityIdentifier)]
      performAction:grey_tap()];

  // Swipe back to first module
  [[EarlGrey
      selectElementWithMatcher:
          grey_accessibilityID(kMagicStackScrollViewAccessibilityIdentifier)]
      performAction:grey_swipeFastInDirection(kGREYDirectionRight)];

  // Assert Set Up List is not there. If it is, it is always the first module.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(
                                   [NewTabPageAppInterface setUpListTitle])]
      assertWithMatcher:grey_notVisible()];
}

// Test that MVT navigation and removal works when the module is put in the
// Magic Stack.
- (void)testMVTInMagicStack {
  self.testServer->RegisterRequestHandler(
      base::BindRepeating(&StandardResponse));
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  const GURL pageURL = self.testServer->GetURL(kPageURL);
  NSString* pageTitle = base::SysUTF8ToNSString(kPageTitle);

  // Clear history and verify that the tile does not exist.
  [ChromeEarlGrey clearBrowsingHistory];
  [ChromeEarlGrey loadURL:pageURL];
  [ChromeEarlGrey waitForWebStateContainingText:kPageLoadedString];

  // After loading URL, need to do another action before opening a new tab
  // with the icon present.
  [ChromeEarlGrey goBack];

  [[self class] closeAllTabs];
  [ChromeEarlGrey openNewTab];

  [[EarlGrey selectElementWithMatcher:
                 grey_accessibilityID(l10n_util::GetNSString(
                     IDS_IOS_CONTENT_SUGGESTIONS_MOST_VISITED_MODULE_TITLE))]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Navigate to MVT
  id<GREYMatcher> matcher =
      grey_allOf(chrome_test_util::StaticTextWithAccessibilityLabel(pageTitle),
                 grey_sufficientlyVisible(), nil);
  [[EarlGrey selectElementWithMatcher:matcher] performAction:grey_tap()];

  [ChromeEarlGrey waitForWebStateContainingText:kPageLoadedString];
  [[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(
                                          pageURL.GetContent())]
      assertWithMatcher:grey_notNil()];

  [ChromeEarlGrey goBack];
  [[EarlGrey selectElementWithMatcher:matcher] performAction:grey_longPress()];

  // Tap on remove.
  [[EarlGrey selectElementWithMatcher:
                 chrome_test_util::ContextMenuItemWithAccessibilityLabelId(
                     IDS_IOS_CONTENT_SUGGESTIONS_REMOVE)]
      performAction:grey_tap()];

  // Check the tile is removed.
  [[EarlGrey
      selectElementWithMatcher:
          grey_allOf(
              chrome_test_util::StaticTextWithAccessibilityLabel(pageTitle),
              grey_sufficientlyVisible(), nil)] assertWithMatcher:grey_nil()];
}

#pragma mark - Test utils

- (void)prepareToTestSetUpListInMagicStack {
  [ChromeEarlGrey writeFirstRunSentinel];
  [ChromeEarlGrey clearDefaultBrowserPromoData];
  [ChromeEarlGrey resetDataForLocalStatePref:
                      prefs::kIosCredentialProviderPromoLastActionTaken];
  [NewTabPageAppInterface resetSetUpListPrefs];
  [ChromeEarlGrey closeAllTabs];
  [ChromeEarlGrey openNewTab];
}

// Setup a most visited tile, and open the context menu by long pressing on it.
- (void)setupMostVisitedTileLongPress {
  self.testServer->RegisterRequestHandler(
      base::BindRepeating(&StandardResponse));
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
  const GURL pageURL = self.testServer->GetURL(kPageURL);
  NSString* pageTitle = base::SysUTF8ToNSString(kPageTitle);

  // Clear history and verify that the tile does not exist.
  [ChromeEarlGrey clearBrowsingHistory];
  [ChromeEarlGrey loadURL:pageURL];
  [ChromeEarlGrey waitForWebStateContainingText:kPageLoadedString];

  // After loading URL, need to do another action before opening a new tab
  // with the icon present.
  [ChromeEarlGrey goBack];

  [[self class] closeAllTabs];
  [ChromeEarlGrey openNewTab];

  id<GREYMatcher> matcher =
      grey_allOf(chrome_test_util::StaticTextWithAccessibilityLabel(pageTitle),
                 grey_sufficientlyVisible(), nil);
  [[EarlGrey selectElementWithMatcher:matcher] performAction:grey_longPress()];
}

@end