chromium/ios/chrome/browser/ui/authentication/account_menu/account_menu_egtest.mm

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

#import <UIKit/UIKit.h>

#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/bookmark_earl_grey.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/account_menu/account_menu_constants.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/settings/google_services/google_services_settings_constants.h"
#import "ios/chrome/browser/ui/settings/google_services/manage_accounts/accounts_table_view_controller_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_earl_grey_ui.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_matchers_app_interface.h"
#import "ios/chrome/test/earl_grey/test_switches.h"
#import "ios/chrome/test/earl_grey/web_http_server_chrome_test_case.h"
#import "ios/testing/earl_grey/app_launch_manager.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

// The passphrase for the fake sync server.
NSString* const kPassphrase = @"hello";

// The primary identity.
FakeSystemIdentity* kPrimaryIdentity = [FakeSystemIdentity fakeIdentity1];

FakeSystemIdentity* kSecondaryIdentity = [FakeSystemIdentity fakeIdentity2];

// Matcher for the account menu.
id<GREYMatcher> accountMenuMatcher() {
  return grey_accessibilityID(kAccountMenuTableViewId);
}

// Matcher for the identity disc.
id<GREYMatcher> identityDiscMatcher() {
  return grey_accessibilityID(kNTPFeedHeaderIdentityDisc);
}

// A matcher for the snackbar message, when the user is signed in with primary
// identity `identity`.
id<GREYMatcher> snackbarMessageMatcher(FakeSystemIdentity* identity) {
  NSString* snackbarMessage =
      l10n_util::GetNSStringF(IDS_IOS_ACCOUNT_MENU_SWITCH_CONFIRMATION_TITLE,
                              base::SysNSStringToUTF16(identity.userGivenName));
  return grey_allOf(grey_text(snackbarMessage), grey_sufficientlyVisible(),
                    nil);
}
}  // namespace

// Integration tests using the Account Menu.
@interface AccountMenuTestCase : WebHttpServerChromeTestCase
@end

@implementation AccountMenuTestCase

- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config = [super appConfigurationForTestCase];

  config.features_enabled.push_back(kIdentityDiscAccountMenu);

  if ([self isRunningTest:@selector
            (testMultipleIdentities_IdentityConfirmationToast)] ||
      [self isRunningTest:@selector
            (testSingleIdentity_IdentityConfirmationToast)] ||
      [self isRunningTest:@selector
            (testFrequencyLimitation_IdentityConfirmationToast)] ||
      [self isRunningTest:@selector
            (testRecentSignin_IdentityConfirmationToast)]) {
    config.features_enabled.push_back(kIdentityConfirmationSnackbar);
  }

  return config;
}

- (void)setUp {
  [super setUp];
  // Adding the sync passphrase must be done before signin due to limitation of
  // the fakes.
  [ChromeEarlGrey addSyncPassphrase:kPassphrase];
  [SigninEarlGrey signinWithFakeIdentity:kPrimaryIdentity];
}

- (void)tearDown {
  base::TimeDelta marginToAllowIdentityConfirmationSnackbar = base::Days(20);
  [ChromeEarlGrey
      setTimeValue:base::Time::FromDeltaSinceWindowsEpoch(
                       marginToAllowIdentityConfirmationSnackbar)
       forUserPref:prefs::kIdentityConfirmationSnackbarLastPromptTime];
  [ChromeEarlGrey signOutAndClearIdentities];
  [super tearDown];
}

// Update the last sign-in to be long enough in the past that we should display
// the account snackbar.
- (void)updateLastSignInToPastDate {
  base::TimeDelta marginBetweenLastSigninAndIdentityConfirmationPrompt =
      base::Days(20);
  [ChromeEarlGrey
      setTimeValue:base::Time::FromDeltaSinceWindowsEpoch(
                       marginBetweenLastSigninAndIdentityConfirmationPrompt)
       forUserPref:prefs::kLastSigninTimestamp];
}

// Select the identity disc particle.
- (void)selectIdentityDisc {
  [[EarlGrey selectElementWithMatcher:identityDiscMatcher()]
      performAction:grey_tap()];
}

// Select the identity disc particle and verify the account menu is displayed.
- (void)selectIdentityDiscAndVerify {
  [[EarlGrey selectElementWithMatcher:identityDiscMatcher()]
      performAction:grey_tap()];
  // Ensure the Account Menu is displayed.
  [[EarlGrey selectElementWithMatcher:accountMenuMatcher()]
      assertWithMatcher:grey_sufficientlyVisible()];
}

// Asserts that there is no account menu.
- (void)assertAccountMenuIsNotShown {
  [[EarlGrey selectElementWithMatcher:accountMenuMatcher()]
      assertWithMatcher:grey_notVisible()];
}

// Assert the snackbar is not shown for kPrimaryIdentity.
- (void)assertSnackbarNotShown {
  [[EarlGrey selectElementWithMatcher:snackbarMessageMatcher(kPrimaryIdentity)]
      assertWithMatcher:grey_nil()];
}

// Assert the snackbar is shown for `identity`.
- (void)assertSnackbarShown:(FakeSystemIdentity*)identity {
  [[EarlGrey selectElementWithMatcher:snackbarMessageMatcher(identity)]
      assertWithMatcher:grey_sufficientlyVisible()];
}

// Close the account menu.
- (void)closeAccountMenu {
  if ([ChromeEarlGrey isIPadIdiom]) {
    // There is no stop button on ipad.
    [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                            kAccountMenuCloseButtonId)]
        assertWithMatcher:grey_nil()];
    // Dismiss the menu by tapping on the identity disc particle.
    [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                            kNTPFeedHeaderIdentityDisc)]
        performAction:grey_tap()];
  } else {
    // Tap on the Close button.
    [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                            kAccountMenuCloseButtonId)]
        performAction:grey_tap()];
  }
}

#pragma mark - Test open and close

// Tests that the identity disc particle can be selected, and lead to opening
// the account menu.
- (void)testViewAccountMenu {
  // Select the identity disc particle.
  [self selectIdentityDiscAndVerify];
}

// Tests that the close button appears if and only if it’s not an ipad and that
// if it’s present it close the account menu.
- (void)testCloseButtonAccountMenu {
  [self selectIdentityDiscAndVerify];

  [self closeAccountMenu];

  // Verify the Account Menu is dismissed.
  [self assertAccountMenuIsNotShown];
}

// Test that the account menu can’t be opened when the user is signed out.
- (void)testNoAccountMenuWhenSignedOut {
  // Keep the identity but sign-out.
  [SigninEarlGrey signOut];
  [self selectIdentityDisc];
  [self assertAccountMenuIsNotShown];
}

#pragma mark - Test tapping on views

// Test the manage account menu entry opens the manage account view.
- (void)testManageAccount {
  [self selectIdentityDisc];
  // Tap on the Ellipsis button.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(
                                   kAccountMenuSecondaryActionMenuButtonId)]
      performAction:grey_tap()];
  // Tap on Manage your account.
  [[EarlGrey
      selectElementWithMatcher:
          grey_allOf(
              grey_text(l10n_util::GetNSString(
                  IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_MANAGE_GOOGLE_ACCOUNT_ITEM)),
              grey_interactable(), nil)] performAction:grey_tap()];
  // Checks the Fake Account Detail View Controller is shown
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kFakeAccountDetailsViewIdentifier)]
      assertWithMatcher:grey_sufficientlyVisible()];
}

// Tests the edit accounts menu entry opens the edit account list view.
- (void)testEditAccountsList {
  [self selectIdentityDisc];
  // Tap on the Ellipsis button.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(
                                   kAccountMenuSecondaryActionMenuButtonId)]
      performAction:grey_tap()];
  // Tap on Manage your account.
  [[EarlGrey
      selectElementWithMatcher:grey_allOf(
                                   grey_text(l10n_util::GetNSString(
                                       IDS_IOS_ACCOUNT_MENU_EDIT_ACCOUNT_LIST)),
                                   grey_interactable(), nil)]
      performAction:grey_tap()];
  // Checks the account settings is shown
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kSettingsEditAccountListTableViewId)]
      assertWithMatcher:grey_sufficientlyVisible()];
}

// Tests that the sign out button actually signs out and the account menu view
// is closed.
- (void)testSignOut {
  [self selectIdentityDisc];
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kAccountMenuSignoutButtonId)]
      performAction:grey_tap()];
  [SigninEarlGrey verifySignedOut];
  [self assertAccountMenuIsNotShown];
}

// Tests that the add account button opens the add account view.
- (void)testAddAccount {
  [self selectIdentityDisc];
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kAccountMenuAddAccountButtonId)]
      performAction:grey_tap()];
  // Checks the Fake authentication view is shown
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kFakeAuthActivityViewIdentifier)]
      assertWithMatcher:grey_sufficientlyVisible()];
  // Close the SSO view controller.
  id<GREYMatcher> matcher =
      grey_allOf(grey_accessibilityID(kFakeAuthCancelButtonIdentifier),
                 grey_sufficientlyVisible(), nil);
  [[EarlGrey selectElementWithMatcher:matcher] performAction:grey_tap()];
  // Make sure the SSO view controller is fully removed before ending the test.
  // The tear down needs to remove other view controllers, and it cannot be done
  // during the animation of the SSO view controler.
  [ChromeEarlGreyUI waitForAppToIdle];
}

// Tests the enter passphrase button.
- (void)testAddPassphrase {
  // Encrypt synced data with a passphrase to enable passphrase encryption for
  // the signed in account.
  [self selectIdentityDisc];
  // Check the error button is displayed.
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kAccountMenuErrorActionButtonId)]
      assertWithMatcher:grey_sufficientlyVisible()];
  // Tap on it
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kAccountMenuErrorActionButtonId)]
      performAction:grey_tap()];
  // Verify that the passphrase view was opened.
  [[EarlGrey selectElementWithMatcher:
                 grey_accessibilityID(
                     kSyncEncryptionPassphraseTableViewAccessibilityIdentifier)]
      assertWithMatcher:grey_sufficientlyVisible()];
  // Enter the passphrase.
  [SigninEarlGreyUI submitSyncPassphrase:kPassphrase];
  // Entering the passphrase closes the view.
  [[EarlGrey selectElementWithMatcher:
                 grey_accessibilityID(
                     kSyncEncryptionPassphraseTableViewAccessibilityIdentifier)]
      assertWithMatcher:grey_nil()];
  // Check the error button disappeared.
  [[EarlGrey
      selectElementWithMatcher:grey_allOf(grey_accessibilityID(
                                              kAccountMenuErrorActionButtonId),
                                          grey_sufficientlyVisible(), nil)]
      assertWithMatcher:grey_nil()];
}

// Tests that tapping on the secondary account button causes the primary account
// to be changed and the account menu view to be closed.
- (void)testSwitch {
  [SigninEarlGrey addFakeIdentity:kSecondaryIdentity];
  [self selectIdentityDisc];
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kAccountMenuSecondaryAccountButtonId)]
      performAction:grey_tap()];
  [SigninEarlGrey verifySignedInWithFakeIdentity:kSecondaryIdentity];
  [self assertAccountMenuIsNotShown];
  [self assertSnackbarShown:kSecondaryIdentity];
}

#pragma mark - Test snackbar

// Verifies identity confirmation snackbar shows on startup with multiple
// identities on device.
- (void)testMultipleIdentities_IdentityConfirmationToast {
  // Add multiple identities and sign in with one of them.
  [SigninEarlGrey addFakeIdentity:kSecondaryIdentity];
  [self updateLastSignInToPastDate];

  // Background then foreground the app.
  [[AppLaunchManager sharedManager] backgroundAndForegroundApp];

  // Confirm the snackbar shows.
  [self assertSnackbarShown:kPrimaryIdentity];
}

// Verifies no identity confirmation snackbar shows on startup with only one
// identity on device.
- (void)testSingleIdentity_IdentityConfirmationToast {
  // Add multiple identities and sign in with one of them.
  [self updateLastSignInToPastDate];

  // Background then foreground the app.
  [[AppLaunchManager sharedManager] backgroundAndForegroundApp];

  [self assertSnackbarNotShown];
}

// Verifies no identity confirmation snackbar shows on startup when there is an
// identity on the device but the user is signed-out.
- (void)testNoIdentity_IdentityConfirmationToast {
  // Keep the identity but sign-out.
  [SigninEarlGrey signOut];
  [self updateLastSignInToPastDate];

  // Background then foreground the app.
  [[AppLaunchManager sharedManager] backgroundAndForegroundApp];
  [self assertSnackbarNotShown];
}

// Verifies identity confirmation snackbar shows on startup with multiple
// identities on device with frequency limitations.
- (void)testFrequencyLimitation_IdentityConfirmationToast {
  // Add multiple identities and sign in with one of them.
  [SigninEarlGrey addFakeIdentity:kSecondaryIdentity];
  [self updateLastSignInToPastDate];

  // Background then foreground the app.
  [[AppLaunchManager sharedManager] backgroundAndForegroundApp];

  // Confirm the snackbar shows.
  [self assertSnackbarShown:kPrimaryIdentity];

  // Dismiss the snackabr.
  [[EarlGrey selectElementWithMatcher:snackbarMessageMatcher(kPrimaryIdentity)]
      performAction:grey_tap()];

  // Background then foreground the app again.
  [[AppLaunchManager sharedManager] backgroundAndForegroundApp];

  [self assertSnackbarNotShown];
}

// Verifies identity confirmation snackbar on startup does not show after a
// recent sign-in.
- (void)testRecentSignin_IdentityConfirmationToast {
  // Add multiple identities and sign in with one of them.
  [SigninEarlGrey addFakeIdentity:kSecondaryIdentity];

  // Background then foreground the app.
  [[AppLaunchManager sharedManager] backgroundAndForegroundApp];
  [self assertSnackbarNotShown];
}

#pragma mark - Test Error Badge

- (void)testErrorBadge {
  [ChromeEarlGrey addBookmarkWithSyncPassphrase:kPassphrase];
  [SigninEarlGrey signinWithFakeIdentity:kPrimaryIdentity];
  [ChromeEarlGreyUI waitForAppToIdle];

  // Verify the error badge shows on the ADP.
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kNTPFeedHeaderIdentityDiscBadge)]
      assertWithMatcher:grey_sufficientlyVisible()];

  [self selectIdentityDiscAndVerify];

  // Check the error button is displayed.
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kAccountMenuErrorActionButtonId)]
      assertWithMatcher:grey_sufficientlyVisible()];
  // Tap on the error action button.
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kAccountMenuErrorActionButtonId)]
      performAction:grey_tap()];
  // Verify that the passphrase view was opened.
  [[EarlGrey selectElementWithMatcher:
                 grey_accessibilityID(
                     kSyncEncryptionPassphraseTableViewAccessibilityIdentifier)]
      assertWithMatcher:grey_sufficientlyVisible()];
  // Enter the passphrase.
  [SigninEarlGreyUI submitSyncPassphrase:kPassphrase];
  // Entering the passphrase closes the view.
  [[EarlGrey selectElementWithMatcher:
                 grey_accessibilityID(
                     kSyncEncryptionPassphraseTableViewAccessibilityIdentifier)]
      assertWithMatcher:grey_nil()];

  [self closeAccountMenu];

  [self assertAccountMenuIsNotShown];

  // Verify the error badge on the ADP disappears.
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kNTPFeedHeaderIdentityDiscBadge)]
      assertWithMatcher:grey_notVisible()];
}

// Tests remove account from the edit accounts menu.
- (void)testEditAccountsListRemoveAccount {
  [self selectIdentityDisc];
  // Tap on the Ellipsis button.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(
                                   kAccountMenuSecondaryActionMenuButtonId)]
      performAction:grey_tap()];
  // Tap on Manage your account.
  [[EarlGrey
      selectElementWithMatcher:grey_allOf(
                                   grey_text(l10n_util::GetNSString(
                                       IDS_IOS_ACCOUNT_MENU_EDIT_ACCOUNT_LIST)),
                                   grey_interactable(), nil)]
      performAction:grey_tap()];
  // Checks the account settings is shown
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kSettingsEditAccountListTableViewId)]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Tap on Remove kPrimaryIdentity button.
  [[EarlGrey
      selectElementWithMatcher:
          grey_accessibilityID(
              [kSettingsAccountsRemoveAccountButtonAccessibilityIdentifier
                  stringByAppendingString:kPrimaryIdentity.userEmail])]
      performAction:grey_tap()];

  // Tap on kPrimaryIdentity confirm remove button.
  [[EarlGrey
      selectElementWithMatcher:chrome_test_util::ButtonWithAccessibilityLabelId(
                                   IDS_IOS_REMOVE_ACCOUNT_LABEL)]
      performAction:grey_tap()];

  [SigninEarlGrey verifySignedOut];

  // Verify the Account Menu is dismissed.
  [self assertAccountMenuIsNotShown];
}

@end