chromium/ios/chrome/browser/passwords/ui_bundled/password_suggestion_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 <XCTest/XCTest.h>

#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/time/time.h"
#import "components/password_manager/core/browser/features/password_features.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "ios/chrome/browser/passwords/model/password_manager_app_interface.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.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/password/password_manager_egtest_utils.h"
#import "ios/chrome/browser/ui/settings/password/password_manager_ui_features.h"
#import "ios/chrome/browser/ui/settings/password/password_settings_app_interface.h"
#import "ios/chrome/common/ui/confirmation_alert/constants.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/chrome/test/scoped_eg_traits_overrider.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/testing/earl_grey/matchers.h"
#import "net/base/apple/url_conversions.h"
#import "net/test/embedded_test_server/default_handlers.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

constexpr char kNewPasswordFieldID[] = "pw";
constexpr char kConfirmationPasswordFieldID[] = "cpw";

using password_manager_test_utils::DeleteCredential;

// Get the top presented view controller, in this case the bottom sheet view
// controller.
UIViewController* TopPresentedViewController() {
  UIViewController* topController =
      chrome_test_util::GetAnyKeyWindow().rootViewController;
  for (UIViewController* controller = [topController presentedViewController];
       controller && ![controller isBeingDismissed];
       controller = [controller presentedViewController]) {
    topController = controller;
  }
  return topController;
}

// Returns the matcher for the use password button.
id<GREYMatcher> UseSuggestedPasswordButton() {
  return chrome_test_util::StaticTextWithAccessibilityLabel(
      l10n_util::GetNSString(IDS_IOS_USE_SUGGESTED_STRONG_PASSWORD));
}

// Returns the matcher for the use keyboard button.
id<GREYMatcher> ProactivePasswordGenerationUseKeyboardButton() {
  return chrome_test_util::ButtonWithAccessibilityLabelId(
      IDS_IOS_PASSWORD_BOTTOM_SHEET_USE_KEYBOARD);
}

}  // namespace

@interface PasswordSuggestionEGTest : ChromeTestCase
@end

@implementation PasswordSuggestionEGTest

- (void)setUp {
  [super setUp];

  // Set up server.
  net::test_server::RegisterDefaultHandlers(self.testServer);
  GREYAssertTrue(self.testServer->Start(), @"Server did not start.");

  // Sign in to a chrome account.
  [SigninEarlGrey signinWithFakeIdentity:[FakeSystemIdentity fakeIdentity1]];
  [ChromeEarlGrey waitForSyncTransportStateActiveWithTimeout:base::Seconds(10)];

  // Also reset the dismiss count pref to 0 to make sure the bottom sheet is
  // enabled by default.
  [ChromeEarlGrey clearUserPrefWithName:
                      prefs::kIosPasswordGenerationBottomSheetDismissCount];
}

- (void)tearDown {
  [ChromeEarlGrey clearUserPrefWithName:
                      prefs::kIosPasswordGenerationBottomSheetDismissCount];
  [super tearDown];
}

- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config;
  config.features_enabled.push_back(
      password_manager::features::kIOSProactivePasswordGenerationBottomSheet);
  return config;
}

#pragma mark - Helper methods

// Loads simple page on localhost.
- (void)loadSignupPage {
  // Loads simple page with a signup form. It is on localhost so it is
  // considered a secure context.
  [ChromeEarlGrey loadURL:self.testServer->GetURL(
                              "/new_password_and_confirmation_form.html")];
  [ChromeEarlGrey waitForWebStateContainingText:"Signup form."];
}

- (void)loadSignupAutofocusPage {
  // Loads simple page with a signup form that is auto-focused. It is on
  // localhost so it is considered a secure context.
  [ChromeEarlGrey
      loadURL:self.testServer->GetURL(
                  "/new_password_and_confirmation_form_autofocus.html")];
  [ChromeEarlGrey waitForWebStateContainingText:"Signup form."];
}

- (void)verifyNewPasswordFieldsHaveBeenFilled {
  // Verify that the new password field is not empty.
  NSString* newPasswordfilledFieldCondition =
      [NSString stringWithFormat:@"document.getElementById('%s').value !== ''",
                                 kNewPasswordFieldID];

  // Verify that the new password field contains at least one non-space
  // character.
  NSString* newPasswordNonSpaceChar =
      [NSString stringWithFormat:@"document.getElementById('%s').value.replace("
                                 @"/\\s+/g, '').length === "
                                 @"document.getElementById('%s').value.length",
                                 kNewPasswordFieldID, kNewPasswordFieldID];

  // Verify that the new password field and confirmation password field have the
  // same value.
  NSString* passwordValuesMatch = [NSString
      stringWithFormat:@"document.getElementById('%s').value === "
                       @"document.getElementById('%s').value",
                       kNewPasswordFieldID, kConfirmationPasswordFieldID];

  NSString* condition = [NSString
      stringWithFormat:@"%@ && %@ && %@", newPasswordfilledFieldCondition,
                       newPasswordNonSpaceChar, passwordValuesMatch];
  [ChromeEarlGrey waitForJavaScriptCondition:condition];
}

- (void)openAndDismissBottomSheet {
  [self loadSignupPage];

  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
      performAction:chrome_test_util::TapWebElementWithId(kNewPasswordFieldID)];

  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:UseSuggestedPasswordButton()];

  [[EarlGrey
      selectElementWithMatcher:ProactivePasswordGenerationUseKeyboardButton()]
      performAction:grey_tap()];

  [ChromeEarlGrey waitForKeyboardToAppear];
}

#pragma mark - Tests

// Tests that the bottom sheet populates the new password and confirm password
// fields.
- (void)testFillNewPasswordWithProactiveBottomSheet {
  [self loadSignupPage];

  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
      performAction:chrome_test_util::TapWebElementWithId(kNewPasswordFieldID)];

  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:UseSuggestedPasswordButton()];

  [[EarlGrey selectElementWithMatcher:UseSuggestedPasswordButton()]
      performAction:grey_tap()];

  [self verifyNewPasswordFieldsHaveBeenFilled];
}

// Tests that the bottom sheet opens on autofocus events.
- (void)testAutofocusOnProactiveBottomSheet {
  [self loadSignupAutofocusPage];

  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:UseSuggestedPasswordButton()];
}

// Tests that the keyboard appears if the "Use Keyboard" button is
// tapped.
- (void)testShowKeyboardFromButtonOnProactiveBottomSheet {
  [self loadSignupPage];

  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
      performAction:chrome_test_util::TapWebElementWithId(kNewPasswordFieldID)];

  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:UseSuggestedPasswordButton()];

  [[EarlGrey
      selectElementWithMatcher:ProactivePasswordGenerationUseKeyboardButton()]
      performAction:grey_tap()];

  [ChromeEarlGrey waitForKeyboardToAppear];
}

// Tests that the bottom sheet does not show after it has been
// dismissed three consecutive times.
- (void)testSilenceProactiveBottomSheet {
  // Dismiss #1
  [self loadSignupPage];
  [self openAndDismissBottomSheet];

  // Dismiss #2.
  [ChromeEarlGrey reload];
  [self openAndDismissBottomSheet];

  // Dismiss #3.
  [ChromeEarlGrey reload];
  [self openAndDismissBottomSheet];

  // Verify that the keyboard is shown instead of the bottom sheet when
  // silenced.
  [ChromeEarlGrey reload];

  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
      performAction:chrome_test_util::TapWebElementWithId(kNewPasswordFieldID)];
  [ChromeEarlGrey waitForKeyboardToAppear];

  // Re-enable proactive password generation bottom sheet by using the
  // suggested password from the keyboard accessory.
  id<GREYMatcher> suggest_password_chip =
      grey_accessibilityLabel(@"Suggest Strong Password");

  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:suggest_password_chip];

  [[EarlGrey selectElementWithMatcher:suggest_password_chip]
      performAction:grey_tap()];

  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:UseSuggestedPasswordButton()];

  [[EarlGrey selectElementWithMatcher:UseSuggestedPasswordButton()]
      performAction:grey_tap()];

  [self verifyNewPasswordFieldsHaveBeenFilled];

  // Verify that the bottom sheet is unsilenced when triggered again.
  [ChromeEarlGrey reload];

  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
      performAction:chrome_test_util::TapWebElementWithId(kNewPasswordFieldID)];

  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:UseSuggestedPasswordButton()];

  [[EarlGrey selectElementWithMatcher:UseSuggestedPasswordButton()]
      performAction:grey_tap()];

  [self verifyNewPasswordFieldsHaveBeenFilled];
}

// Tests dynamic sizing.
- (void)testProactiveBottomSheetWithDynamicTypeSizing {
  if (@available(iOS 17.0, *)) {
    [self loadSignupPage];

    [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
        performAction:chrome_test_util::TapWebElementWithId(
                          kNewPasswordFieldID)];

    [ChromeEarlGrey
        waitForUIElementToAppearWithMatcher:UseSuggestedPasswordButton()];

    // Change trait collection to use accessibility large content size.
    ScopedTraitOverrider overrider(TopPresentedViewController());
    overrider.SetContentSizeCategory(UIContentSizeCategoryAccessibilityLarge);

    [ChromeEarlGreyUI waitForAppToIdle];

    // Verify that the "Use Suggested Password" and "Use Keyboard" buttons are
    // still visible.
    [[EarlGrey selectElementWithMatcher:UseSuggestedPasswordButton()]
        assertWithMatcher:grey_sufficientlyVisible()];

    [[EarlGrey
        selectElementWithMatcher:ProactivePasswordGenerationUseKeyboardButton()]
        assertWithMatcher:grey_sufficientlyVisible()];

    [[EarlGrey selectElementWithMatcher:UseSuggestedPasswordButton()]
        performAction:grey_tap()];

    [self verifyNewPasswordFieldsHaveBeenFilled];
  } else {
    EARL_GREY_TEST_SKIPPED(@"Not available for under iOS 17.");
  }
}

// Tests that the bottom sheet does not show if the user isn't signed in.
- (void)testUserSignedOut {
  [ChromeEarlGrey signOutAndClearIdentities];

  [self loadSignupPage];

  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
      performAction:chrome_test_util::TapWebElementWithId(kNewPasswordFieldID)];

  [ChromeEarlGrey waitForKeyboardToAppear];
}

@end