// 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 <TargetConditionals.h>
#import <utility>
#import "base/functional/callback.h"
#import "base/ios/ios_util.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/scoped_feature_list.h"
#import "base/time/time.h"
#import "components/feature_engagement/public/feature_constants.h"
#import "components/password_manager/core/common/password_manager_constants.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "components/policy/policy_constants.h"
#import "components/strings/grit/components_strings.h"
#import "components/sync/base/features.h"
#import "components/sync/base/user_selectable_type.h"
#import "components/sync/service/sync_prefs.h"
#import "ios/chrome/browser/metrics/model/metrics_app_interface.h"
#import "ios/chrome/browser/passwords/model/metrics/ios_password_manager_metrics.h"
#import "ios/chrome/browser/policy/model/policy_earl_grey_utils.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_app_interface.h"
#import "ios/chrome/browser/ui/authentication/signin_earl_grey_ui_test_util.h"
#import "ios/chrome/browser/ui/settings/google_services/manage_sync_settings_constants.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_table_view_constants.h"
#import "ios/chrome/browser/ui/settings/password/password_manager_egtest_utils.h"
#import "ios/chrome/browser/ui/settings/password/password_settings/password_settings_constants.h"
#import "ios/chrome/browser/ui/settings/password/password_settings_app_interface.h"
#import "ios/chrome/browser/ui/settings/password/passwords_in_other_apps/passwords_in_other_apps_app_interface.h"
#import "ios/chrome/browser/ui/settings/password/passwords_table_view_constants.h"
#import "ios/chrome/browser/ui/settings/password/reauthentication/reauthentication_constants.h"
#import "ios/chrome/browser/ui/settings/password/widget_promo_instructions/widget_promo_instructions_constants.h"
#import "ios/chrome/browser/ui/settings/settings_root_table_constants.h"
#import "ios/chrome/common/ui/confirmation_alert/constants.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_event.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_protocol.h"
#import "ios/chrome/common/ui/table_view/table_view_cells_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/chrome/test/earl_grey/earl_grey_scoped_block_swizzler.h"
#import "ios/chrome/test/earl_grey/test_switches.h"
#import "ios/testing/earl_grey/app_launch_manager.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/testing/earl_grey/matchers.h"
#import "ios/third_party/earl_grey2/src/CommonLib/Matcher/GREYLayoutConstraint.h" // nogncheck
#import "ios/web/public/test/element_selector.h"
#import "ui/base/l10n/l10n_util.h"
using chrome_test_util::ButtonWithAccessibilityLabel;
using chrome_test_util::ButtonWithAccessibilityLabelId;
using chrome_test_util::NavigationBarCancelButton;
using chrome_test_util::NavigationBarDoneButton;
using chrome_test_util::PasswordsTableViewMatcher;
using chrome_test_util::SettingsCollectionView;
using chrome_test_util::SettingsDoneButton;
using chrome_test_util::SettingsNavigationBar;
using chrome_test_util::TabGridEditButton;
using chrome_test_util::TextFieldForCellWithLabelId;
using chrome_test_util::TurnTableViewSwitchOn;
using password_manager::kPasswordManagerSurfaceVisitHistogramName;
using password_manager_test_utils::DeleteButtonForUsernameAndSites;
using password_manager_test_utils::DeleteCredential;
using password_manager_test_utils::EditDoneButton;
using password_manager_test_utils::EditPasswordConfirmationButton;
using password_manager_test_utils::GetInteractionForPasswordIssueEntry;
using password_manager_test_utils::kDefaultPassword;
using password_manager_test_utils::kDefaultSite;
using password_manager_test_utils::kDefaultUserDisplayName;
using password_manager_test_utils::kDefaultUsername;
using password_manager_test_utils::kPasswordStoreErrorMessage;
using password_manager_test_utils::kScrollAmount;
using password_manager_test_utils::NavigationBarEditButton;
using password_manager_test_utils::OpenPasswordManager;
using password_manager_test_utils::PasswordDetailPassword;
using password_manager_test_utils::PasswordDetailsTableViewMatcher;
using password_manager_test_utils::PasswordSettingsTableView;
using password_manager_test_utils::PasswordTextfieldForUsernameAndSites;
using password_manager_test_utils::ReauthenticationController;
using password_manager_test_utils::SaveExamplePasskeyToStore;
using password_manager_test_utils::SavePasswordFormToProfileStore;
using password_manager_test_utils::TapNavigationBarEditButton;
using password_manager_test_utils::UsernameTextfieldForUsernameAndSites;
using testing::ElementWithAccessibilityLabelSubstring;
using testing::NavigationBarBackButton;
namespace {
constexpr base::TimeDelta kSyncInitializedTimeout = base::Seconds(5);
id<GREYMatcher> ButtonWithAccessibilityID(NSString* id) {
return grey_allOf(grey_accessibilityID(id),
grey_accessibilityTrait(UIAccessibilityTraitButton), nil);
}
NSString* GetTextFieldForID(int category_id) {
return [NSString
stringWithFormat:@"%@_textField", l10n_util::GetNSString(category_id)];
}
// Returns the GREYElementInteraction* for the item on the password list with
// the given `matcher`. It scrolls in `direction` if necessary to ensure that
// the matched item is sufficiently visible, thus interactable.
GREYElementInteraction* GetInteractionForListItem(id<GREYMatcher> matcher,
GREYDirection direction) {
return [[EarlGrey
selectElementWithMatcher:grey_allOf(matcher, grey_sufficientlyVisible(),
nil)]
usingSearchAction:grey_scrollInDirection(direction, kScrollAmount)
onElementWithMatcher:grey_accessibilityID(kPasswordsTableViewID)];
}
// Returns the GREYElementInteraction* for the cell on the password list with
// the given `username`. It scrolls down if necessary to ensure that the matched
// cell is interactable.
GREYElementInteraction* GetInteractionForPasswordEntry(NSString* username) {
// ID, not label because the latter might contain an extra label for the
// "local password icon" and most tests don't care about it.
return GetInteractionForListItem(ButtonWithAccessibilityID(username),
kGREYDirectionDown);
}
// Returns the GREYElementInteraction* for the item on the detail view
// identified with the given `matcher`. It scrolls down if necessary to ensure
// that the matched cell is interactable.
GREYElementInteraction* GetInteractionForPasswordDetailItem(
id<GREYMatcher> matcher) {
return [[EarlGrey
selectElementWithMatcher:grey_allOf(matcher, grey_interactable(), nil)]
usingSearchAction:grey_scrollToContentEdge(kGREYContentEdgeTop)
onElementWithMatcher:PasswordDetailsTableViewMatcher()];
}
// Returns the GREYElementInteraction* for the item on the deletion alert
// identified with the given `matcher`.
GREYElementInteraction* GetInteractionForPasswordsExportConfirmAlert(
id<GREYMatcher> matcher) {
return [[EarlGrey
selectElementWithMatcher:grey_allOf(matcher, grey_interactable(), nil)]
inRoot:grey_accessibilityID(kPasswordSettingsExportConfirmViewId)];
}
GREYElementInteraction* GetPasswordDetailTextFieldWithID(int detail_id) {
return GetInteractionForPasswordDetailItem(
grey_allOf(grey_accessibilityID(GetTextFieldForID(detail_id)),
grey_kindOfClassName(@"UITextField"), nil));
}
// Matcher for "Save in Account" confirmation dialog button for bulk save
// passwords in account flow.
GREYElementInteraction* SaveInAccountConfirmationDialogButton() {
return [[EarlGrey
selectElementWithMatcher:
grey_allOf(chrome_test_util::ButtonWithAccessibilityLabelId(
IDS_IOS_BULK_UPLOAD_BUTTON_TITLE),
grey_interactable(), nil)]
inRoot:grey_accessibilityID(
kPasswordSettingsBulkMovePasswordsToAccountAlertViewId)];
}
// Matcher for "Saved Passwords" header in the password list.
id<GREYMatcher> SavedPasswordsHeaderMatcher() {
return grey_allOf(
grey_accessibilityLabel(
l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORDS_SAVED_HEADING)),
grey_accessibilityTrait(UIAccessibilityTraitHeader), nullptr);
}
// Matcher for a UITextField inside a SettingsSearchCell.
id<GREYMatcher> SearchTextField() {
return grey_accessibilityID(kPasswordsSearchBarID);
}
GREYLayoutConstraint* Below() {
return [GREYLayoutConstraint
layoutConstraintWithAttribute:kGREYLayoutAttributeTop
relatedBy:kGREYLayoutRelationGreaterThanOrEqual
toReferenceAttribute:kGREYLayoutAttributeBottom
multiplier:1.0
constant:0.0];
}
// Matcher for the website in the Add Password view.
id<GREYMatcher> AddPasswordWebsite() {
return TextFieldForCellWithLabelId(IDS_IOS_SHOW_PASSWORD_VIEW_SITE);
}
// Matcher for the username in Password Details view.
id<GREYMatcher> CredentialDetailUsername() {
return TextFieldForCellWithLabelId(IDS_IOS_SHOW_PASSWORD_VIEW_USERNAME);
}
id<GREYMatcher> PasskeyDetailUserDisplayName() {
return TextFieldForCellWithLabelId(IDS_IOS_SHOW_PASSKEY_DISPLAY_NAME);
}
// Matcher for the text view of the note in Password Details view.
id<GREYMatcher> PasswordDetailNote() {
return grey_allOf(
grey_accessibilityID(GetTextFieldForID(IDS_IOS_SHOW_PASSWORD_VIEW_NOTE)),
grey_kindOfClassName(@"UITextView"), nil);
}
// Returns matcher for the label of the note in Password Details view.
id<GREYMatcher> PasswordDetailNoteLabel() {
return grey_allOf(grey_kindOfClass([UILabel class]), grey_text(@"Note"), nil);
}
// Matcher for the federation details in Password Details view.
id<GREYMatcher> PasswordDetailFederation() {
return grey_allOf(grey_accessibilityID(GetTextFieldForID(
IDS_IOS_SHOW_PASSWORD_VIEW_FEDERATION)),
grey_kindOfClassName(@"UITextField"), nil);
}
// Matcher for the Show password button in Password Details view.
id<GREYMatcher> ShowPasswordButton() {
return grey_allOf(ButtonWithAccessibilityLabel(l10n_util::GetNSString(
IDS_IOS_SETTINGS_PASSWORD_SHOW_BUTTON)),
grey_interactable(), nullptr);
}
// Matcher for the Hide password button in Password Details view.
id<GREYMatcher> HidePasswordButton() {
return grey_allOf(ButtonWithAccessibilityLabel(l10n_util::GetNSString(
IDS_IOS_SETTINGS_PASSWORD_HIDE_BUTTON)),
grey_interactable(), nullptr);
}
// Matcher for the Delete button in Password Details view.
id<GREYMatcher> DeleteButton() {
return grey_allOf(
ButtonWithAccessibilityLabelId(IDS_IOS_SETTINGS_TOOLBAR_DELETE),
grey_not(grey_accessibilityTrait(UIAccessibilityTraitNotEnabled)),
nullptr);
}
// Matcher for the Delete button in Confirmation Alert for password
// deletion.
id<GREYMatcher> BatchDeleteConfirmationButton() {
return chrome_test_util::AlertAction(
l10n_util::GetNSString(IDS_IOS_DELETE_ACTION_TITLE));
}
// Matcher for the Delete button in the list view, located at the bottom of the
// screen.
id<GREYMatcher> DeleteButtonAtBottom() {
return grey_accessibilityID(kSettingsToolbarDeleteButtonId);
}
// Matcher for the "Delete" associated with the blocked site.
id<GREYMatcher> DeleteBlockedSiteButton() {
return grey_allOf(ButtonWithAccessibilityLabel(
l10n_util::GetNSString(IDS_IOS_DELETE_ACTION_TITLE)),
grey_interactable(), nullptr);
}
// Matcher for the "Delete" associated with the blocked site.
id<GREYMatcher> DeletePasskeyButton() {
return grey_allOf(ButtonWithAccessibilityLabel(l10n_util::GetNSString(
IDS_IOS_CONFIRM_PASSKEY_DELETION)),
grey_interactable(), nullptr);
}
// Matcher for the "View Password" Button presented when a duplicated credential
// is found in the add credential flow.
id<GREYMatcher> DuplicateCredentialViewPasswordButton() {
return grey_allOf(grey_accessibilityLabel(l10n_util::GetNSString(
IDS_IOS_PASSWORD_SETTINGS_VIEW_PASSWORD_BUTTON)),
grey_accessibilityTrait(UIAccessibilityTraitButton),
nullptr);
}
// Matches the pop-up (call-out) menu item with accessibility label equal to the
// translated string identified by `label`.
id<GREYMatcher> PopUpMenuItemWithLabel(int label) {
// iOS13 reworked menu button subviews to no longer be accessibility
// elements. Multiple menu button subviews no longer show up as potential
// matches, which means the matcher logic does not need to be as complex as
// the iOS 11/12 logic. Various table view cells may share the same
// accesibility label, but those can be filtered out by ignoring
// UIAccessibilityTraitButton.
return grey_allOf(
grey_accessibilityLabel(l10n_util::GetNSString(label)),
grey_not(grey_accessibilityTrait(UIAccessibilityTraitButton)),
grey_userInteractionEnabled(), nil);
}
// Returns matcher for the "Add Password" button.
id<GREYMatcher> AddPasswordButton() {
return grey_accessibilityID(kAddPasswordButtonID);
}
// Returns matcher for the "Add Password" toolbar button located at the bottom
// of the screen.
id<GREYMatcher> AddPasswordToolbarButton() {
return grey_accessibilityID(kSettingsToolbarAddButtonId);
}
// Returns matcher for the "Save" button in the "Add Password" view.
id<GREYMatcher> AddPasswordSaveButton() {
return grey_accessibilityID(kPasswordsAddPasswordSaveButtonID);
}
id<GREYMatcher> ToolbarSettingsSubmenuButton() {
return grey_accessibilityID(kSettingsToolbarSettingsButtonId);
}
// Returns matcher for the password details / add password view footer displayed
// when the note length is approaching max limit.
id<GREYMatcher> TooLongNoteFooter() {
return grey_text(l10n_util::GetNSStringF(
IDS_IOS_SETTINGS_PASSWORDS_TOO_LONG_NOTE_DESCRIPTION,
base::NumberToString16(
password_manager::constants::kMaxPasswordNoteLength)));
}
// Returns matcher for the Password Manager widget promo.
id<GREYMatcher> PasswordManagerWidgetPromo() {
return grey_accessibilityID(kWidgetPromoID);
}
// Returns matcher for the Password Manager widget promo's close button.
id<GREYMatcher> PasswordManagerWidgetPromoCloseButton() {
return grey_accessibilityID(kWidgetPromoCloseButtonID);
}
// Returns matcher for the Password Manager widget promo's more info button.
id<GREYMatcher> PasswordManagerWidgetPromoMoreInfoButton(bool enabled = YES) {
if (enabled) {
return grey_allOf(
ButtonWithAccessibilityLabelId(
IDS_IOS_PASSWORD_MANAGER_WIDGET_PROMO_BUTTON_TITLE),
grey_not(grey_accessibilityTrait(UIAccessibilityTraitNotEnabled)),
nullptr);
}
return grey_allOf(ButtonWithAccessibilityLabelId(
IDS_IOS_PASSWORD_MANAGER_WIDGET_PROMO_BUTTON_TITLE),
grey_accessibilityTrait(UIAccessibilityTraitNotEnabled),
nullptr);
}
// Returns matcher for the Password Manager widget promo instructions screen.
id<GREYMatcher> PasswordManagerWidgetPromoInstructions() {
return grey_accessibilityID(password_manager::kWidgetPromoInstructionsViewID);
}
// Returns matcher for the close button of the Password Manager widget promo
// instruction screen.
id<GREYMatcher> PasswordManagerWidgetPromoInstructionsCloseButton() {
return grey_allOf(
grey_accessibilityID(
kConfirmationAlertSecondaryActionAccessibilityIdentifier),
grey_interactable(), nullptr);
}
// Returns matcher for the Password Details move to account button.
id<GREYMatcher> PasswordDetailsMoveToAccountButton() {
return grey_accessibilityID(kMovePasswordToAccountButtonID);
}
// Saves two example forms in the store.
void SaveExamplePasswordForms() {
SavePasswordFormToProfileStore(/*password=*/@"password1",
/*username=*/@"user1",
/*origin=*/@"https://example11.com");
SavePasswordFormToProfileStore(/*password=*/@"password2",
/*username=*/@"user2",
/*origin=*/@"https://example12.com");
}
// Saves an example form with note in the store.
void SaveExamplePasswordFormToProfileStoreWithNote() {
GREYAssert([PasswordSettingsAppInterface
saveExampleNoteToProfileStore:@"concrete note"
password:kDefaultPassword
username:kDefaultUsername
origin:@"https://example.com"],
kPasswordStoreErrorMessage);
}
// Saves two example blocked forms in the store.
void SaveExampleBlockedFormsToProfileStore() {
GREYAssert(
[PasswordSettingsAppInterface
saveExampleBlockedOriginToProfileStore:@"https://exclude1.com"],
kPasswordStoreErrorMessage);
GREYAssert(
[PasswordSettingsAppInterface
saveExampleBlockedOriginToProfileStore:@"https://exclude2.com"],
kPasswordStoreErrorMessage);
}
// Taps on the "Settings" option to show the submenu.
void OpenSettingsSubmenu() {
[[EarlGrey selectElementWithMatcher:ToolbarSettingsSubmenuButton()]
performAction:grey_tap()];
}
void CopyPasswordDetailWithInteraction(GREYElementInteraction* element) {
[element performAction:grey_tap()];
// Tap the context menu item for copying.
[[EarlGrey selectElementWithMatcher:PopUpMenuItemWithLabel(
IDS_IOS_SETTINGS_SITE_COPY_MENU_ITEM)]
performAction:grey_tap()];
}
void CopyPasswordDetailWithID(int detail_id) {
CopyPasswordDetailWithInteraction(
GetPasswordDetailTextFieldWithID(detail_id));
}
// Ensure the save passwords in account section is visible.
void CheckSavePasswordsInAccountSectionVisible() {
[ChromeEarlGrey
waitForSufficientlyVisibleElementWithMatcher:
grey_accessibilityID(
kPasswordSettingsBulkMovePasswordsToAccountDescriptionTableViewId)];
[ChromeEarlGrey
waitForSufficientlyVisibleElementWithMatcher:
grey_accessibilityID(
kPasswordSettingsBulkMovePasswordsToAccountButtonTableViewId)];
}
// Ensure the save passwords in account section is no longer visible.
void CheckSavePasswordsInAccountSectionHidden() {
[ChromeEarlGrey
waitForNotSufficientlyVisibleElementWithMatcher:
grey_accessibilityID(
kPasswordSettingsBulkMovePasswordsToAccountDescriptionTableViewId)];
[ChromeEarlGrey
waitForNotSufficientlyVisibleElementWithMatcher:
grey_accessibilityID(
kPasswordSettingsBulkMovePasswordsToAccountButtonTableViewId)];
}
// Verifies reauthentication UI histogram was recorded.
void CheckReauthenticationUIEventMetric(ReauthenticationEvent event) {
NSString* histogram = base::SysUTF8ToNSString(
password_manager::kReauthenticationUIEventHistogram);
NSError* error = [MetricsAppInterface expectCount:1
forBucket:static_cast<int>(event)
forHistogram:histogram];
GREYAssertNil(error,
@"Failed to record reauthentication ui event histogram.");
}
// Verifies the total count of reauthentication UI histogram recorded.
void CheckReauthenticationUIEventMetricTotalCount(int count) {
NSString* histogram = base::SysUTF8ToNSString(
password_manager::kReauthenticationUIEventHistogram);
NSError* error = [MetricsAppInterface expectTotalCount:count
forHistogram:histogram];
GREYAssertNil(error,
@"Unexpected reauthentication ui event histogram count.");
}
// Verifies the total count of password manager visit histogram recorded.
void CheckPasswordManagerVisitMetricCount(int count) {
// Check password manager visit metric.
NSError* error = [MetricsAppInterface
expectTotalCount:count
forHistogram:@(kPasswordManagerSurfaceVisitHistogramName)];
GREYAssertNil(error, @"Unexpected Password Manager Visit histogram count");
error = [MetricsAppInterface
expectCount:count
forBucket:static_cast<int>(
password_manager::PasswordManagerSurface::kPasswordList)
forHistogram:@(kPasswordManagerSurfaceVisitHistogramName)];
GREYAssertNil(error, @"Unexpected Password Manager Visit histogram count");
}
// Verifies that the elements of the Password Manager widget promo are as
// expected.
void CheckPasswordManagerWidgetPromoVisible() {
[[EarlGrey selectElementWithMatcher:PasswordManagerWidgetPromo()]
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey selectElementWithMatcher:PasswordManagerWidgetPromoCloseButton()]
assertWithMatcher:grey_sufficientlyVisible()];
id<GREYMatcher> image_matcher = grey_accessibilityID(kWidgetPromoImageID);
[[EarlGrey selectElementWithMatcher:image_matcher]
assertWithMatcher:grey_sufficientlyVisible()];
id<GREYMatcher> text_matcher = grey_accessibilityLabel(
l10n_util::GetNSString(IDS_IOS_PASSWORD_MANAGER_WIDGET_PROMO_TEXT));
[[EarlGrey selectElementWithMatcher:text_matcher]
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey
selectElementWithMatcher:PasswordManagerWidgetPromoMoreInfoButton()]
assertWithMatcher:grey_sufficientlyVisible()];
}
// Verifies that the elements of the Password Manager widget promo instruction
// screen are as expected.
void CheckPasswordManagerWidgetPromoInstructionScreenVisible(
bool image_hidden = false) {
[[EarlGrey selectElementWithMatcher:PasswordManagerWidgetPromoInstructions()]
assertWithMatcher:grey_sufficientlyVisible()];
id<GREYMatcher> image_matcher =
grey_accessibilityID(password_manager::kWidgetPromoInstructionsImageID);
[[EarlGrey selectElementWithMatcher:image_matcher]
assertWithMatcher:image_hidden ? grey_notVisible()
: grey_minimumVisiblePercent(0.4)];
id<GREYMatcher> title_matcher = grey_accessibilityLabel(
l10n_util::GetNSString(IDS_IOS_WIDGET_PROMO_INSTRUCTIONS_TITLE));
[[EarlGrey selectElementWithMatcher:title_matcher]
assertWithMatcher:grey_sufficientlyVisible()];
id<GREYMatcher> subtitle_matcher = grey_text(
l10n_util::GetNSString(IDS_IOS_WIDGET_PROMO_INSTRUCTIONS_SUBTITLE));
[[EarlGrey selectElementWithMatcher:subtitle_matcher]
assertWithMatcher:grey_sufficientlyVisible()];
id<GREYMatcher> step1_matcher = grey_accessibilityLabel(
l10n_util::GetNSString(IDS_IOS_WIDGET_PROMO_INSTRUCTIONS_STEP_1));
[[EarlGrey
selectElementWithMatcher:grey_allOf(step1_matcher,
grey_sufficientlyVisible(), nil)]
usingSearchAction:grey_scrollInDirection(kGREYDirectionDown,
kScrollAmount)
onElementWithMatcher:
grey_accessibilityID(
password_manager::kWidgetPromoInstructionsScrollableViewID)];
id<GREYMatcher> step2_matcher = grey_accessibilityLabel(
l10n_util::GetNSString(IDS_IOS_WIDGET_PROMO_INSTRUCTIONS_STEP_2));
[[EarlGrey
selectElementWithMatcher:grey_allOf(step2_matcher,
grey_sufficientlyVisible(), nil)]
usingSearchAction:grey_scrollInDirection(kGREYDirectionDown,
kScrollAmount)
onElementWithMatcher:
grey_accessibilityID(
password_manager::kWidgetPromoInstructionsScrollableViewID)];
id<GREYMatcher> step3_matcher = grey_accessibilityLabel(
l10n_util::GetNSString(IDS_IOS_WIDGET_PROMO_INSTRUCTIONS_STEP_3));
[[EarlGrey
selectElementWithMatcher:grey_allOf(step3_matcher,
grey_sufficientlyVisible(), nil)]
usingSearchAction:grey_scrollInDirection(kGREYDirectionDown,
kScrollAmount)
onElementWithMatcher:
grey_accessibilityID(
password_manager::kWidgetPromoInstructionsScrollableViewID)];
id<GREYMatcher> step4_matcher = grey_accessibilityLabel(
l10n_util::GetNSString(IDS_IOS_WIDGET_PROMO_INSTRUCTIONS_STEP_4));
[[EarlGrey
selectElementWithMatcher:grey_allOf(step4_matcher,
grey_sufficientlyVisible(), nil)]
usingSearchAction:grey_scrollInDirection(kGREYDirectionDown,
kScrollAmount)
onElementWithMatcher:
grey_accessibilityID(
password_manager::kWidgetPromoInstructionsScrollableViewID)];
[[EarlGrey selectElementWithMatcher:
PasswordManagerWidgetPromoInstructionsCloseButton()]
assertWithMatcher:grey_sufficientlyVisible()];
}
void CheckVisibilityOfElement(id<GREYMatcher> matcher, bool is_visible) {
[[EarlGrey selectElementWithMatcher:matcher]
assertWithMatcher:is_visible ? grey_sufficientlyVisible()
: grey_notVisible()];
}
// Opens the instructions for enabling the Password Manager Widget.
void OpenPasswordManagerWidgetPromoInstructions() {
OpenPasswordManager();
// The Password Manager widget promo should be visible.
CheckPasswordManagerWidgetPromoVisible();
// Tap the promo's more info button.
[[EarlGrey
selectElementWithMatcher:PasswordManagerWidgetPromoMoreInfoButton()]
performAction:grey_tap()];
// The Password Manager widget promo's instructions should be visible.
CheckPasswordManagerWidgetPromoInstructionScreenVisible();
}
} // namespace
// Various tests for the main Password Manager UI.
@interface PasswordManagerTestCase : ChromeTestCase
- (GREYElementInteraction*)interactionForSinglePasswordEntryWithDomain:
(NSString*)domain;
// Matcher for the websites in Password Details view.
// `websites` should be in the format "website1, website2,..." with `websiteN`
// being the website displayed in the nth detail row of the website cell.
- (id<GREYMatcher>)matcherForPasswordDetailCellWithWebsites:(NSString*)websites;
// Matcher for the delete button for a given username/password in the details
// screen.
- (id<GREYMatcher>)
matcherForDeleteButtonInDetailsWithUsername:(NSString*)username
sites:(NSString*)password;
@end
@implementation PasswordManagerTestCase {
// A swizzler to observe fake auto-fill status instead of real one.
std::unique_ptr<EarlGreyScopedBlockSwizzler> _passwordAutoFillStatusSwizzler;
}
- (GREYElementInteraction*)interactionForSinglePasswordEntryWithDomain:
(NSString*)domain {
// Since passwords notes launch authentication is required before interacting
// with password details.
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
// ID, not label because the latter might contain an extra label for the
// "local password icon" and most tests don't care about it.
return GetInteractionForListItem(ButtonWithAccessibilityID(domain),
kGREYDirectionDown);
}
- (id<GREYMatcher>)matcherForPasswordDetailCellWithWebsites:
(NSString*)websites {
return grey_accessibilityLabel(
[NSString stringWithFormat:@"Sites, %@", websites]);
}
- (id<GREYMatcher>)
matcherForDeleteButtonInDetailsWithUsername:(NSString*)username
sites:(NSString*)sites {
return DeleteButtonForUsernameAndSites(username, sites);
}
- (void)setUp {
[super setUp];
GREYAssertNil([MetricsAppInterface setupHistogramTester],
@"Cannot setup histogram tester.");
_passwordAutoFillStatusSwizzler =
std::make_unique<EarlGreyScopedBlockSwizzler>(
@"PasswordAutoFillStatusManager", @"sharedManager",
[PasswordsInOtherAppsAppInterface
swizzlePasswordAutoFillStatusManagerWithFake]);
[PasswordSettingsAppInterface setUpMockReauthenticationModule];
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
}
- (void)tearDown {
// Snackbars triggered by tests stay up for a limited time even if the
// settings get closed. Ensure that they are closed to avoid interference with
// other tests.
[PasswordSettingsAppInterface dismissSnackBar];
GREYAssert([PasswordSettingsAppInterface clearProfilePasswordStore],
@"PasswordStore was not cleared.");
GREYAssertNil([MetricsAppInterface releaseHistogramTester],
@"Cannot reset histogram tester.");
[PasswordsInOtherAppsAppInterface resetManager];
_passwordAutoFillStatusSwizzler.reset();
[PasswordSettingsAppInterface removeMockReauthenticationModule];
[super tearDown];
}
- (AppLaunchConfiguration)appConfigurationForTestCase {
AppLaunchConfiguration config;
// See (crbug.com/1479593). Enforcing relaunch between tests is necessary to
// prevent flakiness, due to a spinner that appears in some tests and blocks
// later ones from interacting with the UI.
config.relaunch_policy = ForceRelaunchByCleanShutdown;
if ([self isRunningTest:@selector(testClosingPasswordManagerWidgetPromo)] ||
[self isRunningTest:@selector
(testOpeningPasswordManagerWidgetPromoInstructions)] ||
[self
isRunningTest:@selector(testPasswordManagerWidgetPromoInEditMode)] ||
[self isRunningTest:@selector
(testPasswordManagerWidgetPromoDeviceOrientation)] ||
[self isRunningTest:@selector
(testDismissPasswordManagerWidgetPromoInstructionsScreen)] ||
[self isRunningTest:@selector
(testPasswordManagerWidgetPromoInstructionsDeviceOrientation)] ||
[self
isRunningTest:@selector
(testOpeningPasswordManagerWidgetPromoInstructionsWithFailedAuth)] ||
[self isRunningTest:@selector(testDeletingLastAffiliatedGroup)]) {
config.iph_feature_enabled = "IPH_iOSPromoPasswordManagerWidget";
}
if ([self isRunningTest:@selector(testEditPasskeyUsername)] ||
[self isRunningTest:@selector(testEditPasskeyUserDisplayName)] ||
[self isRunningTest:@selector(testDeletePasskey)]) {
config.features_enabled.push_back(syncer::kSyncWebauthnCredentials);
}
return config;
}
// Verifies the UI elements are accessible on the Passwords page.
- (void)testAccessibilityOnPasswords {
// Saving a form is needed for using the "password details" view.
SavePasswordFormToProfileStore();
OpenPasswordManager();
[ChromeEarlGrey verifyAccessibilityForCurrentScreen];
TapNavigationBarEditButton();
[ChromeEarlGrey verifyAccessibilityForCurrentScreen];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
performAction:grey_tap()];
// Inspect "password details" view.
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
[ChromeEarlGrey verifyAccessibilityForCurrentScreen];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that attempt to copy a password provides appropriate feedback.
- (void)testCopyPasswordToast {
// Saving a form is needed for using the "password details" view.
SavePasswordFormToProfileStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
CopyPasswordDetailWithID(IDS_IOS_SHOW_PASSWORD_VIEW_PASSWORD);
NSString* snackbarLabel =
l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORD_WAS_COPIED_MESSAGE);
// The tap checks the existence of the snackbar and also closes it.
[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(snackbarLabel)]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that an attempt to show a password provides an appropriate feedback.
- (void)testShowPasswordSucceeded {
// Saving a form is needed for using the "password details" view.
SavePasswordFormToProfileStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
[GetInteractionForPasswordDetailItem(ShowPasswordButton())
performAction:grey_tap()];
// Ensure that password is shown.
[GetInteractionForPasswordDetailItem(grey_textFieldValue(kDefaultPassword))
assertWithMatcher:grey_notNil()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that attempts to copy a username provide appropriate feedback.
- (void)testCopyUsernameToast {
// Saving a form is needed for using the "password details" view.
SavePasswordFormToProfileStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
CopyPasswordDetailWithID(IDS_IOS_SHOW_PASSWORD_VIEW_USERNAME);
NSString* snackbarLabel =
l10n_util::GetNSString(IDS_IOS_SETTINGS_USERNAME_WAS_COPIED_MESSAGE);
// The tap checks the existence of the snackbar and also closes it.
[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(snackbarLabel)]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that attempts to copy a site URL provide appropriate feedback.
- (void)testCopySiteToast {
// Saving a form is needed for using the "password details" view.
SavePasswordFormToProfileStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
CopyPasswordDetailWithInteraction(GetInteractionForPasswordDetailItem(
[self matcherForPasswordDetailCellWithWebsites:kDefaultSite]));
NSString* snackbarLabel =
l10n_util::GetNSString(IDS_IOS_SETTINGS_SITES_WERE_COPIED_MESSAGE);
// The tap checks the existence of the snackbar and also closes it.
[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(snackbarLabel)]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that deleting a saved password from password details view goes back
// to the list-of-passwords view which doesn't display that form anymore.
- (void)testSavedFormDeletionInDetailView {
// Save form to be deleted later.
SavePasswordFormToProfileStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarEditButton()]
performAction:grey_tap()];
DeleteCredential(kDefaultUsername, kDefaultSite);
// Wait until the alert and the detail view are dismissed.
[ChromeEarlGreyUI waitForAppToIdle];
// Check that the current view is now the list view, by locating
// PasswordTableViewController.
[[EarlGrey
selectElementWithMatcher:grey_accessibilityID(kPasswordsTableViewID)]
assertWithMatcher:grey_notNil()];
// Verify that the deletion was propagated to the ProfilePasswordStore.
GREYAssertEqual(
0, [PasswordSettingsAppInterface passwordProfileStoreResultsCount],
@"Stored password was not removed from ProfilePasswordStore.");
// Also verify that the removed password is no longer in the list.
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
assertWithMatcher:grey_not(grey_sufficientlyVisible())];
// Finally, verify that the Add button is visible and enabled, because there
// are no other password entries left for deletion via the "Edit" mode.
[[EarlGrey selectElementWithMatcher:AddPasswordToolbarButton()]
assertWithMatcher:grey_allOf(grey_enabled(), grey_sufficientlyVisible(),
nil)];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Tests that the detail view is dismissed when the last password is deleted
// after the user had edited the password.
- (void)testSavedFormDeletionInDetailViewAfterEditingFields {
// Save form to be deleted later.
SavePasswordFormToProfileStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
TapNavigationBarEditButton();
// Edit password field.
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
assertWithMatcher:grey_textFieldValue(kDefaultPassword)];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"")];
// Delete password.
DeleteCredential(kDefaultUsername, kDefaultSite);
// Wait until the alert and the detail view are dismissed.
[ChromeEarlGreyUI waitForAppToIdle];
// Check that the current view is now the list view, by locating
// PasswordTableViewController.
[[EarlGrey
selectElementWithMatcher:grey_accessibilityID(kPasswordsTableViewID)]
assertWithMatcher:grey_notNil()];
// Verify that the deletion was propagated to the ProfilePasswordStore.
GREYAssertEqual(
0, [PasswordSettingsAppInterface passwordProfileStoreResultsCount],
@"Stored password was not removed from ProfilePasswordStore.");
// Also verify that the removed password is no longer in the list.
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
assertWithMatcher:grey_not(grey_sufficientlyVisible())];
// Finally, verify that the Add button is visible and enabled, because there
// are no other password entries left for deletion via the "Edit" mode.
[[EarlGrey selectElementWithMatcher:AddPasswordToolbarButton()]
assertWithMatcher:grey_allOf(grey_enabled(), grey_sufficientlyVisible(),
nil)];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that deleting a saved password from password details view goes back
// to the list-of-passwords showing only previously saved blocked sites.
- (void)testSavedFormDeletionInDetailViewWithBlockedSites {
// Save form to be deleted later.
SavePasswordFormToProfileStore();
// Saved blocked sites that should not be affected.
SaveExampleBlockedFormsToProfileStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarEditButton()]
performAction:grey_tap()];
DeleteCredential(kDefaultUsername, kDefaultSite);
// Wait until the alert and the detail view are dismissed.
[ChromeEarlGreyUI waitForAppToIdle];
// Check that the current view is now the list view, by locating
// PasswordTableViewController.
[[EarlGrey
selectElementWithMatcher:grey_accessibilityID(kPasswordsTableViewID)]
assertWithMatcher:grey_notNil()];
// Verify that the deletion was propagated to the ProfilePasswordStore.
GREYAssertEqual(
2, [PasswordSettingsAppInterface passwordProfileStoreResultsCount],
@"Stored password was not removed from ProfilePasswordStore.");
// Also verify that the removed password is no longer in the list.
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
assertWithMatcher:grey_not(grey_sufficientlyVisible())];
// Verify blocked sites are still there.
[GetInteractionForPasswordEntry(@"exclude1.com")
assertWithMatcher:grey_sufficientlyVisible()];
[GetInteractionForPasswordEntry(@"exclude2.com")
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that deleting a duplicated saved password from password details view
// goes back to the list-of-passwords view which doesn't display that form
// anymore.
- (void)testDuplicatedSavedFormDeletionInDetailView {
// Save form to be deleted later.
SavePasswordFormToProfileStore();
// Save duplicate of the previously saved form to be deleted at the same time.
// This entry is considered duplicated because it maps to the same sort key
// as the previous one.
SavePasswordFormToProfileStore(/*password=*/kDefaultPassword,
/*username=*/kDefaultUsername,
/*origin=*/@"https://example.com/example");
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarEditButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:DeleteButtonForUsernameAndSites(
kDefaultUsername, kDefaultSite)]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:BatchDeleteConfirmationButton()]
performAction:grey_tap()];
// Wait until the alert and the detail view are dismissed.
[ChromeEarlGreyUI waitForAppToIdle];
// Check that the current view is now the list view, by locating
// PasswordTableViewController.
[[EarlGrey
selectElementWithMatcher:grey_accessibilityID(kPasswordsTableViewID)]
assertWithMatcher:grey_notNil()];
// Verify that the deletion was propagated to the ProfilePasswordStore.
GREYAssertEqual(
0, [PasswordSettingsAppInterface passwordProfileStoreResultsCount],
@"Stored password was not removed from ProfilePasswordStore.");
// Also verify that the removed password is no longer in the list.
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
assertWithMatcher:grey_not(grey_sufficientlyVisible())];
// Finally, verify that the Add button is visible and enabled, because there
// are no other password entries left for deletion via the "Edit" mode.
[[EarlGrey selectElementWithMatcher:AddPasswordToolbarButton()]
assertWithMatcher:grey_allOf(grey_enabled(), grey_sufficientlyVisible(),
nil)];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that deleting a blocked form from password details view goes
// back to the list-of-passwords view which doesn't display that form anymore.
- (void)testBlockedFormDeletionInDetailView {
// Save blocked form to be deleted later.
GREYAssert([PasswordSettingsAppInterface
saveExampleBlockedOriginToProfileStore:@"https://blocked.com"],
kPasswordStoreErrorMessage);
OpenPasswordManager();
[GetInteractionForPasswordEntry(@"blocked.com") performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarEditButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:DeleteBlockedSiteButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:BatchDeleteConfirmationButton()]
performAction:grey_tap()];
// Wait until the alert and the detail view are dismissed.
[ChromeEarlGreyUI waitForAppToIdle];
// Check that the current view is now the list view, by locating
// PasswordTableViewController.
[[EarlGrey
selectElementWithMatcher:grey_accessibilityID(kPasswordsTableViewID)]
assertWithMatcher:grey_notNil()];
// Verify that the deletion was propagated to the ProfilePasswordStore.
GREYAssertEqual(
0, [PasswordSettingsAppInterface passwordProfileStoreResultsCount],
@"Stored password was not removed from ProfilePasswordStore.");
// Also verify that the removed password is no longer in the list.
[GetInteractionForPasswordEntry(@"secret.com")
assertWithMatcher:grey_not(grey_sufficientlyVisible())];
// Finally, verify that the Add button is visible and enabled, because there
// are no other password entries left for deletion via the "Edit" mode.
[[EarlGrey selectElementWithMatcher:AddPasswordToolbarButton()]
assertWithMatcher:grey_allOf(grey_enabled(), grey_sufficientlyVisible(),
nil)];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that deleting a blocked form from password details view goes
// back to the list-of-passwords view which only displays a previously saved
// password.
- (void)testBlockedFormDeletionInDetailViewWithSavedForm {
// Save blocked form to be deleted later.
GREYAssert([PasswordSettingsAppInterface
saveExampleBlockedOriginToProfileStore:@"https://blocked.com"],
kPasswordStoreErrorMessage);
SavePasswordFormToProfileStore();
OpenPasswordManager();
[GetInteractionForPasswordEntry(@"blocked.com") performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarEditButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:DeleteBlockedSiteButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:BatchDeleteConfirmationButton()]
performAction:grey_tap()];
// Wait until the alert and the detail view are dismissed.
[ChromeEarlGreyUI waitForAppToIdle];
// Check that the current view is now the list view, by locating
// PasswordTableViewController.
[[EarlGrey
selectElementWithMatcher:grey_accessibilityID(kPasswordsTableViewID)]
assertWithMatcher:grey_notNil()];
// Verify that the deletion was propagated to the ProfilePasswordStore.
GREYAssertEqual(
1, [PasswordSettingsAppInterface passwordProfileStoreResultsCount],
@"Stored password was not removed from ProfilePasswordStore.");
// Also verify that the removed blocked site is no longer in the list.
[GetInteractionForPasswordEntry(@"secret.com")
assertWithMatcher:grey_not(grey_sufficientlyVisible())];
// Verify existing saved password is still in the list.
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that deleting a password from password details can be cancelled.
- (void)testCancelDeletionInDetailView {
// Save form to be deleted later.
SavePasswordFormToProfileStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarEditButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:DeleteButtonForUsernameAndSites(
kDefaultUsername, kDefaultSite)]
performAction:grey_tap()];
// Close the dialog by tapping on Password Details screen cancel button.
[[EarlGrey selectElementWithMatcher:NavigationBarCancelButton()]
performAction:grey_tap()];
// Check that the current view is still the detail view.
[[EarlGrey selectElementWithMatcher:PasswordDetailsTableViewMatcher()]
assertWithMatcher:grey_notNil()];
// Verify that the deletion did not happen.
GREYAssertEqual(
1u, [PasswordSettingsAppInterface passwordProfileStoreResultsCount],
@"Stored password was removed from ProfilePasswordStore.");
// Go back to the list view and verify that the password is still in the
// list.
[[EarlGrey selectElementWithMatcher:NavigationBarCancelButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that if the list view is in edit mode, the details password view is
// not accessible on tapping the entries.
- (void)testEditMode {
// Save a form to have something to tap on.
SavePasswordFormToProfileStore();
OpenPasswordManager();
TapNavigationBarEditButton();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
// Check that the current view is not the detail view, by failing to locate
// the Copy button.
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
assertWithMatcher:grey_nil()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that attempts to copy the password via the context menu item provide
// an appropriate feedback.
- (void)testCopyPasswordMenuItem {
// Saving a form is needed for using the "password details" view.
SavePasswordFormToProfileStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
// Tap the password cell to display the context menu.
[GetInteractionForPasswordDetailItem(grey_text(kMaskedPassword))
performAction:grey_tap()];
// Tap the context menu item for copying.
[[EarlGrey
selectElementWithMatcher:PopUpMenuItemWithLabel(
IDS_IOS_SETTINGS_PASSWORD_COPY_MENU_ITEM)]
performAction:grey_tap()];
// Check the snackbar.
NSString* snackbarLabel =
l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORD_WAS_COPIED_MESSAGE);
// The tap checks the existence of the snackbar and also closes it.
[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(snackbarLabel)]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that federated credentials have no password but show the federation.
- (void)testFederated {
GREYAssert(
[PasswordSettingsAppInterface
saveExampleFederatedOriginToProfileStore:
@"https://famous.provider.net"
username:@"federated username"
origin:@"https://example.com"],
kPasswordStoreErrorMessage);
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
// Check that the Site and Username are present and correct.
[[EarlGrey selectElementWithMatcher:
[self matcherForPasswordDetailCellWithWebsites:kDefaultSite]]
assertWithMatcher:grey_notNil()];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
assertWithMatcher:grey_textFieldValue(@"federated username")];
[[EarlGrey selectElementWithMatcher:PasswordDetailFederation()]
assertWithMatcher:grey_textFieldValue(@"famous.provider.net")];
// Check that the password is not present.
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
assertWithMatcher:grey_nil()];
// Check that editing doesn't require reauth.
[[EarlGrey selectElementWithMatcher:NavigationBarEditButton()]
performAction:grey_tap()];
// Ensure delete button is present after entering editing mode.
[[EarlGrey selectElementWithMatcher:DeleteButtonForUsernameAndSites(
@"federated username", kDefaultSite)]
assertWithMatcher:grey_notNil()];
[[EarlGrey selectElementWithMatcher:NavigationBarCancelButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks the order of the elements in the detail view layout for a
// non-federated, non-blocked credential.
- (void)testLayoutNormal {
SaveExamplePasswordFormToProfileStoreWithNote();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:
[self matcherForPasswordDetailCellWithWebsites:kDefaultSite]]
assertWithMatcher:grey_notNil()];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
assertWithMatcher:grey_textFieldValue(kDefaultUsername)];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
assertWithMatcher:grey_textFieldValue(kMaskedPassword)];
[[EarlGrey selectElementWithMatcher:PasswordDetailNote()]
assertWithMatcher:grey_text(@"concrete note")];
[[EarlGrey selectElementWithMatcher:PasswordDetailFederation()]
assertWithMatcher:grey_nil()];
[GetInteractionForPasswordDetailItem(CredentialDetailUsername())
assertWithMatcher:
grey_layout(
@[ Below() ],
[self matcherForPasswordDetailCellWithWebsites:kDefaultSite])];
[GetInteractionForPasswordDetailItem(PasswordDetailPassword())
assertWithMatcher:grey_layout(@[ Below() ], CredentialDetailUsername())];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that entering too long note while editing a password blocks the save
// button and displays a footer explanation.
- (void)testLayoutWithLongNotes {
SaveExamplePasswordFormToProfileStoreWithNote();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:TooLongNoteFooter()]
assertWithMatcher:grey_nil()];
// Entering too long note results in "Done" button being disabled and footer
// displayed.
NSString* note = [@"" stringByPaddingToLength:1001
withString:@"a"
startingAtIndex:0];
[[EarlGrey selectElementWithMatcher:PasswordDetailNote()]
performAction:grey_replaceText(note)];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
assertWithMatcher:grey_not(grey_enabled())];
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(
kPasswordDetailsViewControllerID)]
performAction:grey_scrollToContentEdge(kGREYContentEdgeBottom)];
[[EarlGrey selectElementWithMatcher:TooLongNoteFooter()]
assertWithMatcher:grey_sufficientlyVisible()];
// Entering note with length close to the limit should result in displaying
// footer only ("Done" button should be enabled).
note = [@"" stringByPaddingToLength:1000 withString:@"a" startingAtIndex:0];
[[EarlGrey selectElementWithMatcher:PasswordDetailNote()]
performAction:grey_replaceText(note)];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
assertWithMatcher:grey_enabled()];
[[EarlGrey selectElementWithMatcher:TooLongNoteFooter()]
assertWithMatcher:grey_sufficientlyVisible()];
// For shorter notes there should be no footer and "Done" button enabled.
note = [@"" stringByPaddingToLength:100 withString:@"a" startingAtIndex:0];
[[EarlGrey selectElementWithMatcher:PasswordDetailNote()]
performAction:grey_replaceText(note)];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
assertWithMatcher:grey_enabled()];
[[EarlGrey selectElementWithMatcher:TooLongNoteFooter()]
assertWithMatcher:grey_nil()];
}
// Checks the order of the elements in the detail view layout for a blocked
// credential.
- (void)testLayoutForBlockedCredential {
GREYAssert([PasswordSettingsAppInterface
saveExampleBlockedOriginToProfileStore:@"https://example.com"],
kPasswordStoreErrorMessage);
OpenPasswordManager();
[GetInteractionForPasswordEntry(@"example.com") performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:
[self matcherForPasswordDetailCellWithWebsites:kDefaultSite]]
assertWithMatcher:grey_notNil()];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
assertWithMatcher:grey_nil()];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
assertWithMatcher:grey_nil()];
[[EarlGrey selectElementWithMatcher:PasswordDetailFederation()]
assertWithMatcher:grey_nil()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks the order of the elements in the detail view layout for a federated
// credential.
- (void)testLayoutFederated {
GREYAssert(
[PasswordSettingsAppInterface
saveExampleFederatedOriginToProfileStore:
@"https://famous.provider.net"
username:@"federated username"
origin:@"https://example.com"],
kPasswordStoreErrorMessage);
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:
[self matcherForPasswordDetailCellWithWebsites:kDefaultSite]]
assertWithMatcher:grey_notNil()];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
assertWithMatcher:grey_textFieldValue(@"federated username")];
[[EarlGrey selectElementWithMatcher:PasswordDetailFederation()]
assertWithMatcher:grey_textFieldValue(@"famous.provider.net")];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
assertWithMatcher:grey_nil()];
[GetInteractionForPasswordDetailItem(CredentialDetailUsername())
assertWithMatcher:
grey_layout(
@[ Below() ],
[self matcherForPasswordDetailCellWithWebsites:kDefaultSite])];
[[EarlGrey selectElementWithMatcher:PasswordDetailFederation()]
assertWithMatcher:grey_layout(@[ Below() ], CredentialDetailUsername())];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Check that stored entries are shown no matter what the preference for saving
// passwords is.
- (void)testStoredEntriesAlwaysShown {
SavePasswordFormToProfileStore();
OpenPasswordManager();
// Toggle the "Save Passwords" control off and back on and check that stored
// items are still present.
BOOL isSwitchEnabled =
[PasswordSettingsAppInterface isCredentialsServiceEnabled];
BOOL kExpectedState[] = {isSwitchEnabled, !isSwitchEnabled};
for (BOOL expected_state : kExpectedState) {
OpenSettingsSubmenu();
// Toggle the switch. It is located near the top, so if not interactable,
// try scrolling up.
[[EarlGrey
selectElementWithMatcher:
grey_allOf(chrome_test_util::TableViewSwitchCell(
kPasswordSettingsSavePasswordSwitchTableViewId,
expected_state),
grey_sufficientlyVisible(), nil)]
performAction:TurnTableViewSwitchOn(!expected_state)];
// Check that the switch has been modified.
[[EarlGrey selectElementWithMatcher:
chrome_test_util::TableViewSwitchCell(
kPasswordSettingsSavePasswordSwitchTableViewId,
!expected_state)]
assertWithMatcher:grey_sufficientlyVisible()];
// Close settings submenu.
[[EarlGrey
selectElementWithMatcher:grey_allOf(SettingsDoneButton(),
grey_sufficientlyVisible(), nil)]
performAction:grey_tap()];
// Check the stored items. Scroll down if needed.
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
assertWithMatcher:grey_notNil()];
}
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Check that toggling the switch for the "save passwords" preference changes
// the settings.
- (void)testPrefToggle {
OpenPasswordManager();
OpenSettingsSubmenu();
// Toggle the "Save Passwords" control off and back on and check the
// preferences.
constexpr BOOL kExpectedState[] = {YES, NO};
int count = 0;
for (BOOL expected_initial_state : kExpectedState) {
[[EarlGrey selectElementWithMatcher:
chrome_test_util::TableViewSwitchCell(
kPasswordSettingsSavePasswordSwitchTableViewId,
expected_initial_state)]
performAction:TurnTableViewSwitchOn(!expected_initial_state)];
const bool expected_final_state = !expected_initial_state;
GREYAssertEqual(expected_final_state,
[PasswordSettingsAppInterface isCredentialsServiceEnabled],
@"State of the UI toggle differs from real preferences.");
count++;
// Verify histogram total count.
GREYAssertNil(
[MetricsAppInterface
expectTotalCount:count
forHistogram:
@"PasswordManager.Settings.ToggleOfferToSavePasswords"],
@"Unexpected password settings toggle offer to save passwords switch "
@"histogram count");
// Verify histogram value and specific bucket count.
GREYAssertNil(
[MetricsAppInterface
expectCount:1
forBucket:expected_final_state
forHistogram:
@"PasswordManager.Settings.ToggleOfferToSavePasswords"],
@"Unexpected histogram error for password settings toggle offer to "
@"save passwords switch");
}
// "Done" to close settings submenu.
[[EarlGrey
selectElementWithMatcher:grey_allOf(SettingsDoneButton(),
grey_sufficientlyVisible(), nil)]
performAction:grey_tap()];
// "Back" to go to root settings menu.
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
// "Done" to close out.
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that deleting a password from the list view works.
- (void)testDeletionInListView {
// Save a password to be deleted later.
SavePasswordFormToProfileStore();
OpenPasswordManager();
TapNavigationBarEditButton();
// Select password entry to be removed.
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:DeleteButtonAtBottom()]
performAction:grey_tap()];
// Tap on the Delete button of the alert dialog.
[[EarlGrey selectElementWithMatcher:BatchDeleteConfirmationButton()]
performAction:grey_tap()];
// Verify that the deletion was propagated to the ProfilePasswordStore.
GREYAssertEqual(
0, [PasswordSettingsAppInterface passwordProfileStoreResultsCount],
@"Stored password was not removed from ProfilePasswordStore.");
// Verify that the removed password is no longer in the list.
[GetInteractionForPasswordEntry(@"example.com, concrete username")
assertWithMatcher:grey_not(grey_sufficientlyVisible())];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Test that even with many passwords the settings are still usable. In
// particular, ensure that password entries "below the fold" are reachable and
// their detail view is shown on tapping.
// There are two bottlenecks potentially affecting the runtime of the test:
// (1) Storing passwords on initialisation.
// (2) Speed of EarlGrey UI operations such as scrolling.
// To keep the test duration reasonable, the delay from (1) is eliminated in
// storing just about enough passwords to ensure filling more than one page on
// any device. To limit the effect of (2), custom large scrolling steps are
// added to the usual scrolling actions.
// TODO(crbug.com/40910877): This test is flaky.
- (void)FLAKY_testManyPasswords {
if ([ChromeEarlGrey isIPadIdiom]) {
// TODO(crbug.com/40602996): Enable the test on iPad once the bug is fixed.
EARL_GREY_TEST_DISABLED(@"Disabled for iPad.");
}
// Enough just to ensure filling more than one page on all devices.
constexpr int kPasswordsCount = 15;
// Send the passwords to the queue to be added to the ProfilePasswordStore.
[PasswordSettingsAppInterface
saveExamplePasswordToProfileWithCount:kPasswordsCount];
// Use TestStoreConsumer::GetStoreResults to wait for the background storing
// task to complete and to verify that the passwords have been stored.
GREYAssertEqual(
kPasswordsCount,
[PasswordSettingsAppInterface passwordProfileStoreResultsCount],
@"Unexpected ProfilePasswordStore results.");
OpenPasswordManager();
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
// Wait for the loading indicator to disappear, and the sections to be on
// screen, before scrolling.
[[EarlGrey selectElementWithMatcher:SavedPasswordsHeaderMatcher()]
assertWithMatcher:grey_notNil()];
// Aim at an entry almost at the end of the list.
constexpr int kRemoteIndex = kPasswordsCount - 4;
[GetInteractionForPasswordEntry(
[NSString stringWithFormat:@"example.com, %d accounts", kPasswordsCount])
performAction:grey_tap()];
// Check that the detail view loaded correctly by verifying the site content.
[[[EarlGrey
selectElementWithMatcher:
[self matcherForPasswordDetailCellWithWebsites:
[NSString stringWithFormat:@"https://www%02d.example.com/",
kRemoteIndex]]]
usingSearchAction:grey_scrollInDirection(kGREYDirectionDown,
kScrollAmount)
onElementWithMatcher:PasswordDetailsTableViewMatcher()]
assertWithMatcher:grey_notNil()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that if all passwords are deleted in the list view, the enabled Add
// button replaces the Done button.
- (void)testEditButtonUpdateOnDeletion {
// Save a password to be deleted later.
SavePasswordFormToProfileStore();
OpenPasswordManager();
TapNavigationBarEditButton();
// Select password entry to be removed.
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:DeleteButtonAtBottom()]
performAction:grey_tap()];
// Tap on the Delete button of the alert dialog.
[[EarlGrey selectElementWithMatcher:BatchDeleteConfirmationButton()]
performAction:grey_tap()];
// Verify that the Add button is visible and enabled.
[[EarlGrey selectElementWithMatcher:AddPasswordToolbarButton()]
assertWithMatcher:grey_allOf(grey_enabled(), grey_sufficientlyVisible(),
nil)];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Test export flow
- (void)testExportFlow {
// Saving a form is needed for exporting passwords.
SavePasswordFormToProfileStore();
OpenPasswordManager();
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
[[EarlGrey selectElementWithMatcher:ToolbarSettingsSubmenuButton()]
performAction:grey_tap()];
[[[EarlGrey selectElementWithMatcher:
grey_allOf(chrome_test_util::ButtonWithAccessibilityLabelId(
IDS_IOS_EXPORT_PASSWORDS),
grey_sufficientlyVisible(), nil)]
usingSearchAction:grey_scrollInDirection(kGREYDirectionDown,
kScrollAmount)
onElementWithMatcher:grey_accessibilityID(kPasswordsTableViewID)]
performAction:grey_tap()];
[GetInteractionForPasswordsExportConfirmAlert(
chrome_test_util::ButtonWithAccessibilityLabelId(
IDS_IOS_EXPORT_PASSWORDS)) performAction:grey_tap()];
// Wait until the alerts are dismissed.
[ChromeEarlGreyUI waitForAppToIdle];
id<GREYMatcher> exportButtonStatusMatcher =
grey_accessibilityTrait(UIAccessibilityTraitNotEnabled);
[[EarlGrey
selectElementWithMatcher:chrome_test_util::ButtonWithAccessibilityLabelId(
IDS_IOS_EXPORT_PASSWORDS)]
assertWithMatcher:exportButtonStatusMatcher];
[ChromeEarlGrey verifyActivitySheetVisible];
[ChromeEarlGrey closeActivitySheet];
// Wait until the activity view is dismissed.
[ChromeEarlGreyUI waitForAppToIdle];
// Check that export button is re-enabled.
[[EarlGrey
selectElementWithMatcher:chrome_test_util::ButtonWithAccessibilityLabelId(
IDS_IOS_EXPORT_PASSWORDS)]
assertWithMatcher:grey_not(grey_accessibilityTrait(
UIAccessibilityTraitNotEnabled))];
[[EarlGrey
selectElementWithMatcher:grey_allOf(SettingsDoneButton(),
grey_sufficientlyVisible(), nil)]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Test that when user types text in search field, passwords and blocked
// items are filtered out and "save passwords" switch is removed.
- (void)testSearchPasswords {
// TODO(crbug.com/40683159): Test doesn't pass on iPad device or simulator.
if ([ChromeEarlGrey isIPadIdiom]) {
EARL_GREY_TEST_SKIPPED(
@"This test doesn't pass on iPad device or simulator.");
}
SaveExamplePasswordForms();
SaveExampleBlockedFormsToProfileStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example11.com"]
assertWithMatcher:grey_notNil()];
[[self interactionForSinglePasswordEntryWithDomain:@"example12.com"]
assertWithMatcher:grey_notNil()];
[GetInteractionForPasswordEntry(@"exclude1.com")
assertWithMatcher:grey_notNil()];
[GetInteractionForPasswordEntry(@"exclude2.com")
assertWithMatcher:grey_notNil()];
[[EarlGrey selectElementWithMatcher:SearchTextField()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SearchTextField()]
performAction:grey_replaceText(@"2")];
[[self interactionForSinglePasswordEntryWithDomain:@"example11.com"]
assertWithMatcher:grey_nil()];
[[self interactionForSinglePasswordEntryWithDomain:@"example12.com"]
assertWithMatcher:grey_notNil()];
[GetInteractionForPasswordEntry(@"exclude1.com")
assertWithMatcher:grey_nil()];
[GetInteractionForPasswordEntry(@"exclude2.com")
assertWithMatcher:grey_notNil()];
[[EarlGrey
selectElementWithMatcher:ButtonWithAccessibilityLabelId(IDS_CANCEL)]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Test search and delete all passwords and blocked items.
// TODO(crbug.com/40910165): Flaky.
- (void)DISABLED_testSearchAndDeleteAllPasswords {
SaveExamplePasswordForms();
SaveExampleBlockedFormsToProfileStore();
OpenPasswordManager();
// TODO(crbug.com/40609735): Comment out because currently activating the
// search bar will hide the "Edit" button in the top toolbar. Recover this
// when the "Edit" button is moved to the bottom toolbar in the new Settings
// UI.
// [[EarlGrey selectElementWithMatcher:SearchTextField()]
// performAction:grey_replaceText(@"u")];
// [ChromeEarlGrey simulatePhysicalKeyboardEvent:@"\n" flags:0];
[[EarlGrey
selectElementWithMatcher:grey_accessibilityID(kPasswordsTableViewID)]
performAction:grey_scrollToContentEdge(kGREYContentEdgeBottom)];
TapNavigationBarEditButton();
// Select all.
[[self interactionForSinglePasswordEntryWithDomain:@"example11.com"]
performAction:grey_tap()];
[[self interactionForSinglePasswordEntryWithDomain:@"example12.com"]
performAction:grey_tap()];
[GetInteractionForPasswordEntry(@"exclude1.com") performAction:grey_tap()];
[GetInteractionForPasswordEntry(@"exclude2.com") performAction:grey_tap()];
// Delete them.
[[EarlGrey selectElementWithMatcher:DeleteButtonAtBottom()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:BatchDeleteConfirmationButton()]
performAction:grey_tap()];
[ChromeEarlGreyUI waitForAppToIdle];
// All should be gone.
[[self interactionForSinglePasswordEntryWithDomain:@"example11.com"]
assertWithMatcher:grey_nil()];
[[self interactionForSinglePasswordEntryWithDomain:@"example12.com"]
assertWithMatcher:grey_nil()];
[GetInteractionForPasswordEntry(@"exclude1.com")
assertWithMatcher:grey_nil()];
[GetInteractionForPasswordEntry(@"exclude2.com")
assertWithMatcher:grey_nil()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Test that user can't search passwords while in edit mode.
- (void)testCantSearchPasswordsWhileInEditMode {
SaveExamplePasswordForms();
OpenPasswordManager();
TapNavigationBarEditButton();
// Verify search bar is disabled.
[[EarlGrey selectElementWithMatcher:SearchTextField()]
assertWithMatcher:grey_not(grey_userInteractionEnabled())];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Test that the user can edit a password that is part of search results.
- (void)testCanEditPasswordsFromASearch {
SaveExamplePasswordForms();
OpenPasswordManager();
// TODO(crbug.com/40609735): Comment out because currently activating the
// search bar will hide the "Edit" button in the top toolbar. Recover this
// when the "Edit" button is moved to the bottom toolbar in the new Settings
// UI.
// [[EarlGrey selectElementWithMatcher:SearchTextField()]
// performAction:grey_replaceText(@"2")];
TapNavigationBarEditButton();
// Select password entry to be edited.
[GetInteractionForPasswordEntry(@"example12.com") performAction:grey_tap()];
// Delete it
[[EarlGrey selectElementWithMatcher:DeleteButtonAtBottom()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:BatchDeleteConfirmationButton()]
performAction:grey_tap()];
// Filter results in nothing.
// TODO(crbug.com/40609735): Comment out because currently activating the
// search bar will hide the "Edit" button in the top toolbar. Recover this
// when the "Edit" button is moved to the bottom toolbar in the new Settings
// UI.
// [GetInteractionForPasswordEntry(@"example11.com, user1")
// assertWithMatcher:grey_nil()];
// [GetInteractionForPasswordEntry(@"example12.com, user2")
// assertWithMatcher:grey_nil()];
// Get out of edit mode.
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
performAction:grey_tap()];
// Remove filter search term.
// TODO(crbug.com/40916973): Revert to grey_clearText when fixed in EG.
[[EarlGrey selectElementWithMatcher:SearchTextField()]
performAction:grey_replaceText(@"")];
// Only password 1 should show.
[GetInteractionForPasswordEntry(@"example11.com")
assertWithMatcher:grey_notNil()];
[GetInteractionForPasswordEntry(@"example12.com")
assertWithMatcher:grey_nil()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Edit a password with only incognito tab opened should work.
- (void)testEditPasswordWithOnlyIncognitoTabOpen {
SavePasswordFormToProfileStore();
[ChromeEarlGrey openNewIncognitoTab];
[ChromeEarlGrey closeAllNormalTabs];
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
assertWithMatcher:grey_textFieldValue(kDefaultPassword)];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"new password")];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:EditPasswordConfirmationButton()]
performAction:grey_tap()];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
assertWithMatcher:grey_textFieldValue(@"new password")];
[[EarlGrey selectElementWithMatcher:NavigationBarCancelButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that attempts to edit a password provide appropriate feedback.
- (void)testEditPassword {
SavePasswordFormToProfileStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
assertWithMatcher:grey_textFieldValue(kDefaultPassword)];
// Check that empty password is not allowed, and done button is disabled.
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"")];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
assertWithMatcher:grey_allOf(grey_not(grey_enabled()),
grey_sufficientlyVisible(), nil)];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"new password")];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:EditPasswordConfirmationButton()]
performAction:grey_tap()];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
assertWithMatcher:grey_textFieldValue(@"new password")];
[[EarlGrey selectElementWithMatcher:NavigationBarCancelButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that attempts to edit a username provide appropriate feedback.
- (void)testEditUsername {
SavePasswordFormToProfileStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
assertWithMatcher:grey_textFieldValue(kDefaultUsername)];
// Empty username should work as well.
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
performAction:grey_replaceText(@"")];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
assertWithMatcher:grey_textFieldValue(@"")];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
performAction:grey_replaceText(@"new username")];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
assertWithMatcher:grey_textFieldValue(@"new username")];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
assertWithMatcher:grey_notNil()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that attempts to edit a username to a value which is already used for
// the same domain fails.
- (void)testEditUsernameFails {
SavePasswordFormToProfileStore(/*password=*/kDefaultPassword,
/*username=*/@"concrete username1");
SavePasswordFormToProfileStore(/*password=*/kDefaultPassword,
/*username=*/@"concrete username2");
OpenPasswordManager();
[GetInteractionForPasswordEntry(@"example.com, 2 accounts")
performAction:grey_tap()];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:UsernameTextfieldForUsernameAndSites(
@"concrete username1", kDefaultSite)]
assertWithMatcher:grey_textFieldValue(@"concrete username1")];
// TODO(crbug.com/40916973): Revert to grey_clearText when fixed in EG.
[[EarlGrey selectElementWithMatcher:UsernameTextfieldForUsernameAndSites(
@"concrete username1", kDefaultSite)]
performAction:grey_replaceText(@"")];
[[EarlGrey selectElementWithMatcher:UsernameTextfieldForUsernameAndSites(
@"concrete username1", kDefaultSite)]
performAction:grey_replaceText(@"concrete username2")];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
assertWithMatcher:grey_allOf(grey_not(grey_enabled()),
grey_sufficientlyVisible(), nil)];
[[EarlGrey selectElementWithMatcher:NavigationBarCancelButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
- (void)testEditPasskeyUsername {
SaveExamplePasskeyToStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
assertWithMatcher:grey_textFieldValue(kDefaultUsername)];
// Empty username should work as well.
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
performAction:grey_replaceText(@"")];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
assertWithMatcher:grey_textFieldValue(@"")];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
performAction:grey_replaceText(@"new username")];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
assertWithMatcher:grey_textFieldValue(@"new username")];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
assertWithMatcher:grey_notNil()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
- (void)testEditPasskeyUserDisplayName {
SaveExamplePasskeyToStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:PasskeyDetailUserDisplayName()]
assertWithMatcher:grey_textFieldValue(kDefaultUserDisplayName)];
// Empty username should work as well.
[[EarlGrey selectElementWithMatcher:PasskeyDetailUserDisplayName()]
performAction:grey_replaceText(@"")];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:PasskeyDetailUserDisplayName()]
assertWithMatcher:grey_textFieldValue(@"")];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:PasskeyDetailUserDisplayName()]
performAction:grey_replaceText(@"new user display name")];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:PasskeyDetailUserDisplayName()]
assertWithMatcher:grey_textFieldValue(@"new user display name")];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
assertWithMatcher:grey_notNil()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
- (void)testDeletePasskey {
SaveExamplePasskeyToStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
TapNavigationBarEditButton();
id<GREYMatcher> userDisplayName = PasskeyDetailUserDisplayName();
// Verify that the passkey exists.
[[EarlGrey selectElementWithMatcher:userDisplayName]
assertWithMatcher:grey_sufficientlyVisible()];
// Tap the delete button of the credential.
[[EarlGrey selectElementWithMatcher:DeletePasskeyButton()]
performAction:grey_tap()];
// Tap on the Delete confirmation button of the alert dialog.
[[EarlGrey selectElementWithMatcher:BatchDeleteConfirmationButton()]
performAction:grey_tap()];
// Verify that the passkey no longer exists.
[[EarlGrey selectElementWithMatcher:userDisplayName]
assertWithMatcher:grey_not(grey_sufficientlyVisible())];
}
// Checks that attempts to edit a username provide appropriate feedback.
- (void)testCancelDuringEditing {
SavePasswordFormToProfileStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"new password")];
[[EarlGrey selectElementWithMatcher:NavigationBarCancelButton()]
performAction:grey_tap()];
// Test that password value is unchanged.
[GetInteractionForPasswordDetailItem(ShowPasswordButton())
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
assertWithMatcher:grey_textFieldValue(kDefaultPassword)];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Tests that removing multiple passwords works fine.
- (void)testRemovingMultiplePasswords {
constexpr int kPasswordsCount = 4;
// Send the passwords to the queue to be added to the ProfilePasswordStore.
[PasswordSettingsAppInterface
saveExamplePasswordToProfileWithCount:kPasswordsCount];
// Also save passwords for example11.com and example12.com, since the rest
// will be grouped together.
SaveExamplePasswordForms();
OpenPasswordManager();
[ChromeEarlGrey verifyAccessibilityForCurrentScreen];
TapNavigationBarEditButton();
[[GetInteractionForPasswordEntry(@"example.com, 4 accounts")
assertWithMatcher:grey_notNil()] performAction:grey_tap()];
[[GetInteractionForPasswordEntry(@"example11.com")
assertWithMatcher:grey_notNil()] performAction:grey_tap()];
[[GetInteractionForPasswordEntry(@"example12.com")
assertWithMatcher:grey_notNil()] performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:DeleteButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:BatchDeleteConfirmationButton()]
performAction:grey_tap()];
// Wait until animation is over.
[ChromeEarlGreyUI waitForAppToIdle];
// Check that saved forms header is removed.
[[EarlGrey selectElementWithMatcher:SavedPasswordsHeaderMatcher()]
assertWithMatcher:grey_nil()];
// Verify that the deletion was propagated to the ProfilePasswordStore.
GREYAssertEqual(
0, [PasswordSettingsAppInterface passwordProfileStoreResultsCount],
@"Stored password was not removed from ProfilePasswordStore.");
// Finally, verify that the Add button is visible and enabled, because there
// are no other password entries left for deletion via the "Edit" mode.
[[EarlGrey selectElementWithMatcher:AddPasswordToolbarButton()]
assertWithMatcher:grey_allOf(grey_enabled(), grey_sufficientlyVisible(),
nil)];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Checks that the "Add" button is not shown on Edit.
- (void)testAddButtonDisabledInEditMode {
SavePasswordFormToProfileStore();
OpenPasswordManager();
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:AddPasswordButton()]
performAction:grey_tap()];
// Verify that the dialog didn't show up after tapping the Add button.
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
assertWithMatcher:grey_nil()];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Tests the add password flow from the toolbar button.
// TODO(crbug.com/40255054): Flaky, please re-enable once fixed.
- (void)DISABLED_testAddNewPasswordCredential {
OpenPasswordManager();
// Press "Add".
[[EarlGrey selectElementWithMatcher:AddPasswordToolbarButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
assertWithMatcher:grey_not(grey_enabled())];
// Fill form.
[[EarlGrey selectElementWithMatcher:AddPasswordWebsite()]
performAction:grey_replaceText(kDefaultSite)];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
performAction:grey_replaceText(@"new username")];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"new password")];
// The "Add" button is enabled after site and password have been entered.
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
assertWithMatcher:grey_enabled()];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
performAction:grey_tap()];
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
assertWithMatcher:grey_textFieldValue(@"new password")];
}
// Validates that the Password Manager UI is dismissed if local authentication
// fails while within the Add Password UI.
- (void)testAddNewPasswordWithFailedAuth {
OpenPasswordManager();
// Press "Add".
[[EarlGrey selectElementWithMatcher:AddPasswordToolbarButton()]
performAction:grey_tap()];
CheckVisibilityOfElement(/*matcher=*/AddPasswordSaveButton(),
/*is_visible=*/true);
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kFailure];
[PasswordSettingsAppInterface
mockReauthenticationModuleShouldReturnSynchronously:NO];
[[AppLaunchManager sharedManager] backgroundAndForegroundApp];
// Verify that Add Password UI is covered by Reauthentication UI until local
// authentication is passed.
CheckVisibilityOfElement(/*matcher=*/AddPasswordSaveButton(),
/*is_visible=*/false);
CheckVisibilityOfElement(/*matcher=*/ReauthenticationController(),
/*is_visible=*/true);
[PasswordSettingsAppInterface mockReauthenticationModuleReturnMockedResult];
// Password Manager should be dismissed leaving the Settings UI visible.
CheckVisibilityOfElement(/*matcher=*/AddPasswordSaveButton(),
/*is_visible=*/false);
CheckVisibilityOfElement(/*matcher=*/ReauthenticationController(),
/*is_visible=*/false);
CheckVisibilityOfElement(/*matcher=*/SettingsCollectionView(),
/*is_visible=*/true);
}
// Checks that entering too long note while adding passwords blocks the save
// button and displays a footer explanation.
- (void)testAddPasswordLayoutWithLongNotes {
OpenPasswordManager();
[[EarlGrey selectElementWithMatcher:AddPasswordToolbarButton()]
performAction:grey_tap()];
// Fill form.
[[EarlGrey selectElementWithMatcher:AddPasswordWebsite()]
performAction:grey_replaceText(kDefaultSite)];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
performAction:grey_replaceText(@"new username")];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"new password")];
// Entering too long note results in "Add" password being disabled and footer
// displayed.
NSString* note = [@"" stringByPaddingToLength:1001
withString:@"a"
startingAtIndex:0];
[[EarlGrey selectElementWithMatcher:PasswordDetailNote()]
performAction:grey_replaceText(note)];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
assertWithMatcher:grey_not(grey_enabled())];
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(
kPasswordDetailsViewControllerID)]
performAction:grey_scrollToContentEdge(kGREYContentEdgeBottom)];
[[EarlGrey selectElementWithMatcher:TooLongNoteFooter()]
assertWithMatcher:grey_sufficientlyVisible()];
// Entering note with length close to the limit should result in displaying
// footer only ("add" button should be enabled).
note = [@"" stringByPaddingToLength:1000 withString:@"a" startingAtIndex:0];
[[EarlGrey selectElementWithMatcher:PasswordDetailNote()]
performAction:grey_replaceText(note)];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
assertWithMatcher:grey_enabled()];
[[EarlGrey selectElementWithMatcher:TooLongNoteFooter()]
assertWithMatcher:grey_sufficientlyVisible()];
// For shorter notes there should be no footer and "add" button enabled.
note = [@"" stringByPaddingToLength:100 withString:@"a" startingAtIndex:0];
[[EarlGrey selectElementWithMatcher:PasswordDetailNote()]
performAction:grey_replaceText(note)];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
assertWithMatcher:grey_enabled()];
[[EarlGrey selectElementWithMatcher:TooLongNoteFooter()]
assertWithMatcher:grey_nil()];
}
// Tests that when a new credential is saved or an existing one is updated via
// the add credential flow, the VC auto scrolls to the newly created or the
// updated entry.
// TODO(crbug.com/40874087): Flaky, please re-enable once fixed.
- (void)testAutoScroll {
for (int i = 0; i < 20; i++) {
NSString* username = [NSString stringWithFormat:@"username %d", i];
NSString* password = [NSString stringWithFormat:@"password %d", i];
NSString* site = [NSString stringWithFormat:@"https://example%d.com", i];
SavePasswordFormToProfileStore(password, username, site);
}
OpenPasswordManager();
// Press "Add".
[[EarlGrey selectElementWithMatcher:AddPasswordButton()]
performAction:grey_tap()];
NSString* const kAddedDomain = @"zexample.com";
// Fill form.
[[EarlGrey selectElementWithMatcher:AddPasswordWebsite()]
performAction:grey_replaceText([NSString
stringWithFormat:@"https://%@", kAddedDomain])];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
performAction:grey_replaceText(@"zconcrete username")];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"new password")];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
performAction:grey_tap()];
// Verify that the added credential was automatically scrolled at and visible.
ConditionBlock condition = ^{
NSError* error = nil;
[[EarlGrey selectElementWithMatcher:ButtonWithAccessibilityID(kAddedDomain)]
assertWithMatcher:grey_sufficientlyVisible()
error:&error];
return error == nil;
};
GREYAssert(
base::test::ios::WaitUntilConditionOrTimeout(base::Seconds(2), condition),
@"Didn't scroll to the added credential item");
}
// Tests that adding new password credential where the username and website
// matches with an existing credential results in showing a section alert for
// the existing credential.
- (void)testAddNewDuplicatedPasswordCredential {
SavePasswordFormToProfileStore();
OpenPasswordManager();
// Press "Add".
[[EarlGrey selectElementWithMatcher:AddPasswordButton()]
performAction:grey_tap()];
// Fill form.
[[EarlGrey selectElementWithMatcher:AddPasswordWebsite()]
performAction:grey_replaceText(@"https://example.com")];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"password")];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
performAction:grey_replaceText(kDefaultUsername)];
// Verify Save Button is not enabled.
// The enabled state is set async after checking for credential duplication.
// Waiting until the button is not enabled.
ConditionBlock condition = ^{
NSError* error = nil;
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
assertWithMatcher:grey_not(grey_enabled())
error:&error];
return error == nil;
};
GREYAssert(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForUIElementTimeout, condition),
@"Waiting Save Button to be disabled.");
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
[[EarlGrey selectElementWithMatcher:DuplicateCredentialViewPasswordButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
performAction:grey_replaceText(@"new username")];
[[EarlGrey selectElementWithMatcher:EditDoneButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
assertWithMatcher:grey_textFieldValue(@"new username")];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
assertWithMatcher:grey_notNil()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Tests that save button in add password view remains disabled when we switch
// from invalid to valid input in any of the fields (website, password, note),
// when there are still other fields with invalid input.
- (void)testAddPasswordSaveButtonStateOnFieldChanges {
OpenPasswordManager();
[[EarlGrey selectElementWithMatcher:AddPasswordToolbarButton()]
performAction:grey_tap()];
NSString* long_note = [@"" stringByPaddingToLength:1001
withString:@"a"
startingAtIndex:0];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
performAction:grey_replaceText(@"username")];
// Make sure that switching from invalid to valid note doesn't enable the save
// button when website is invalid.
[[EarlGrey selectElementWithMatcher:AddPasswordWebsite()]
performAction:grey_replaceText(@"example")];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"password")];
[[EarlGrey selectElementWithMatcher:PasswordDetailNote()]
performAction:grey_replaceText(long_note)];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
assertWithMatcher:grey_not(grey_enabled())];
[[EarlGrey selectElementWithMatcher:PasswordDetailNote()]
performAction:grey_replaceText(@"note")];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
assertWithMatcher:grey_not(grey_enabled())];
// Make sure that switching from invalid to valid note doesn't enable the save
// button when password is invalid.
[[EarlGrey selectElementWithMatcher:AddPasswordWebsite()]
performAction:grey_replaceText(kDefaultSite)];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"")];
[[EarlGrey selectElementWithMatcher:PasswordDetailNote()]
performAction:grey_replaceText(long_note)];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
assertWithMatcher:grey_not(grey_enabled())];
[[EarlGrey selectElementWithMatcher:PasswordDetailNote()]
performAction:grey_replaceText(@"note")];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
assertWithMatcher:grey_not(grey_enabled())];
// Make sure that from invalid to valid website and password doesn't enable
// the save button when note is too long.
[[EarlGrey selectElementWithMatcher:AddPasswordWebsite()]
performAction:grey_replaceText(@"example")];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"")];
[[EarlGrey selectElementWithMatcher:PasswordDetailNote()]
performAction:grey_replaceText(long_note)];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
assertWithMatcher:grey_not(grey_enabled())];
[[EarlGrey selectElementWithMatcher:AddPasswordWebsite()]
performAction:grey_replaceText(kDefaultSite)];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
assertWithMatcher:grey_not(grey_enabled())];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"password")];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
assertWithMatcher:grey_not(grey_enabled())];
}
// Tests that the duplicate credential section alert is shown when the user adds
// a credential that has the same website as that of an existing credential
// (does not contain username).
// TODO(crbug.com/40279461): Fix flaky test & re-enable.
#if TARGET_OS_SIMULATOR
#define MAYBE_testDuplicatedCredentialWithNoUsername \
DISABLED_testDuplicatedCredentialWithNoUsername
#else
#define MAYBE_testDuplicatedCredentialWithNoUsername \
testDuplicatedCredentialWithNoUsername
#endif
- (void)MAYBE_testDuplicatedCredentialWithNoUsername {
OpenPasswordManager();
[[EarlGrey selectElementWithMatcher:AddPasswordToolbarButton()]
performAction:grey_tap()];
// Fill form.
[[EarlGrey selectElementWithMatcher:AddPasswordWebsite()]
performAction:grey_replaceText(@"https://www.example.com")];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
performAction:grey_replaceText(@"")];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"new password")];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
performAction:grey_tap()];
// Add another credential.
[[EarlGrey selectElementWithMatcher:AddPasswordButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:AddPasswordWebsite()]
performAction:grey_replaceText(@"https://www.example.com")];
// Test that the section alert for duplicated credential is shown.
[[EarlGrey selectElementWithMatcher:DuplicateCredentialViewPasswordButton()]
assertWithMatcher:grey_enabled()];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
performAction:grey_replaceText(@"new username")];
// Wait until duplicated message disappearing animation is done.
[ChromeEarlGreyUI waitForAppToIdle];
// Test that the section alert for duplicated credential is removed.
[[EarlGrey selectElementWithMatcher:DuplicateCredentialViewPasswordButton()]
assertWithMatcher:grey_not(grey_sufficientlyVisible())];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"znew password")];
[[EarlGrey selectElementWithMatcher:AddPasswordSaveButton()]
performAction:grey_tap()];
[GetInteractionForPasswordEntry(@"example.com, 2 accounts")
assertWithMatcher:grey_notNil()];
[GetInteractionForPasswordEntry(@"example.com, 2 accounts")
performAction:grey_tap()];
TapNavigationBarEditButton();
[[EarlGrey selectElementWithMatcher:PasswordTextfieldForUsernameAndSites(
@"new username",
@"https://www.example.com/")]
assertWithMatcher:grey_textFieldValue(@"znew password")];
}
// Tests that the error message is shown when the top-level domain is missing
// when adding a new credential.
- (void)testTLDMissingMessage {
OpenPasswordManager();
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
// Press "Add".
[[EarlGrey selectElementWithMatcher:AddPasswordToolbarButton()]
performAction:grey_tap()];
// Fill form.
[[EarlGrey selectElementWithMatcher:AddPasswordWebsite()]
performAction:grey_replaceText(@"example")];
[[EarlGrey selectElementWithMatcher:PasswordDetailPassword()]
performAction:grey_replaceText(@"password")];
[[EarlGrey selectElementWithMatcher:CredentialDetailUsername()]
performAction:grey_replaceText(kDefaultUsername)];
[[EarlGrey selectElementWithMatcher:
grey_text(l10n_util::GetNSStringF(
IDS_IOS_SETTINGS_PASSWORDS_MISSING_TLD_DESCRIPTION,
u"example.com"))]
assertWithMatcher:grey_sufficientlyVisible()];
}
// Tests that reauthentication is not required to show password when notes are
// enabled since the reauthentication happens before navigating to the details
// view in this scenario.
- (void)testShowHidePassword {
SavePasswordFormToProfileStore();
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
[GetInteractionForPasswordDetailItem(ShowPasswordButton())
assertWithMatcher:grey_sufficientlyVisible()];
[GetInteractionForPasswordDetailItem(ShowPasswordButton())
performAction:grey_tap()];
[GetInteractionForPasswordDetailItem(HidePasswordButton())
assertWithMatcher:grey_sufficientlyVisible()];
[GetInteractionForPasswordDetailItem(HidePasswordButton())
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Tests that the percentage of favicons for the password manager metric is
// logged properly when there are passwords with a favicon.
- (void)testLogFaviconsForPasswordsPercentageMetricWithPassword {
// Sign-in and synced user.
FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
[SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
[ChromeEarlGrey waitForSyncEngineInitialized:YES
syncTimeout:kSyncInitializedTimeout];
// Add passwords for the user.
SaveExamplePasswordForms();
OpenPasswordManager();
// Make sure the cell is loaded properly before tapping on it.
ConditionBlock condition = ^{
NSError* error = nil;
[[self interactionForSinglePasswordEntryWithDomain:@"example12.com"]
assertWithMatcher:grey_sufficientlyVisible()
error:&error];
return error == nil;
};
GREYAssert(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForUIElementTimeout, condition),
@"Waiting for the cell to load");
[[self interactionForSinglePasswordEntryWithDomain:@"example12.com"]
performAction:grey_tap()];
// Metric: Percentage of favicons with image.
// Verify that histogram is called.
NSError* error = [MetricsAppInterface
expectTotalCount:1
forHistogram:@"IOS.PasswordManager.Favicons.Percentage"];
if (error) {
GREYFail([error description]);
}
// Verify the logged value of the histogram.
error = [MetricsAppInterface
expectSum:0
forHistogram:@"IOS.PasswordManager.Favicons.Percentage"];
if (error) {
GREYFail([error description]);
}
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Tests that the percentage of favicons for the password manager metric is
// logged properly when there are no password.
- (void)testLogFaviconsForPasswordsPercentageMetricNoPassword {
OpenPasswordManager();
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
// Metric: Percentage of favicons with image.
// This histogram is not logged.
NSError* error = [MetricsAppInterface
expectTotalCount:0
forHistogram:@"IOS.PasswordManager.Favicons.Percentage"];
if (error) {
GREYFail([error description]);
}
}
- (void)testOpenPasswordSettingsSubmenu {
OpenPasswordManager();
OpenSettingsSubmenu();
CheckVisibilityOfElement(/*matcher=*/PasswordSettingsTableView(),
/*is_visible=*/true);
}
// Tests that the Password Manager UI is dismissed when local authentication
// fails while within the Password Settings UI.
- (void)testOpenPasswordSettingsSubmenuWithFailedAuth {
OpenPasswordManager();
OpenSettingsSubmenu();
CheckVisibilityOfElement(/*matcher=*/PasswordSettingsTableView(),
/*is_visible=*/true);
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kFailure];
[PasswordSettingsAppInterface
mockReauthenticationModuleShouldReturnSynchronously:NO];
[[AppLaunchManager sharedManager] backgroundAndForegroundApp];
// Password Settings shouldn't be visible until successful authentication.
CheckVisibilityOfElement(/*matcher=*/PasswordSettingsTableView(),
/*is_visible=*/false);
CheckVisibilityOfElement(/*matcher=*/ReauthenticationController(),
/*is_visible=*/true);
[PasswordSettingsAppInterface mockReauthenticationModuleReturnMockedResult];
// The Password Manager UI should be gone leaving the Settings UI visible.
CheckVisibilityOfElement(/*matcher=*/PasswordSettingsTableView(),
/*is_visible=*/false);
CheckVisibilityOfElement(/*matcher=*/PasswordsTableViewMatcher(),
/*is_visible=*/false);
CheckVisibilityOfElement(/*matcher=*/ReauthenticationController(),
/*is_visible=*/false);
CheckVisibilityOfElement(/*matcher=*/SettingsCollectionView(),
/*is_visible=*/true);
}
// Tests that the detail text in this row reflects the status of the system
// setting.
- (void)testPasswordsInOtherAppsItem {
OpenPasswordManager();
OpenSettingsSubmenu();
id<GREYMatcher> onMatcher = grey_allOf(
grey_accessibilityLabel(l10n_util::GetNSString(IDS_IOS_SETTING_ON)),
grey_sufficientlyVisible(), nil);
id<GREYMatcher> offMatcher = grey_allOf(
grey_accessibilityLabel(l10n_util::GetNSString(IDS_IOS_SETTING_OFF)),
grey_sufficientlyVisible(), nil);
// No detail text should appear until the AutoFill status has been populated.
[[EarlGrey selectElementWithMatcher:onMatcher] assertWithMatcher:grey_nil()];
[[EarlGrey selectElementWithMatcher:offMatcher] assertWithMatcher:grey_nil()];
[PasswordsInOtherAppsAppInterface startFakeManagerWithAutoFillStatus:NO];
[ChromeEarlGrey waitForUIElementToAppearWithMatcher:offMatcher];
[PasswordsInOtherAppsAppInterface setAutoFillStatus:YES];
[ChromeEarlGrey waitForUIElementToAppearWithMatcher:onMatcher];
}
// Tests that the detail view is dismissed when the last password is deleted,
// but stays if there are still passwords on the page.
- (void)testPasswordsDeletionNavigation {
// Save forms with the same origin to be deleted later.
SavePasswordFormToProfileStore(/*password=*/@"password1",
/*username=*/@"user1",
/*origin=*/@"https://example1.com");
SavePasswordFormToProfileStore(/*password=*/@"password2",
/*username=*/@"user2",
/*origin=*/@"https://example1.com");
SavePasswordFormToProfileStore(/*password=*/@"password3",
/*username=*/@"user3",
/*origin=*/@"https://example3.com");
OpenPasswordManager();
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
[GetInteractionForPasswordEntry(@"example1.com, 2 accounts")
assertWithMatcher:grey_notNil()];
[GetInteractionForPasswordEntry(@"example1.com, 2 accounts")
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:NavigationBarEditButton()]
performAction:grey_tap()];
// Delete first password.
DeleteCredential(@"user1", @"https://example1.com/");
// Check that the current view is still the password details since there is
// still one more password left on the view.
ConditionBlock condition = ^{
NSError* error = nil;
[[EarlGrey selectElementWithMatcher:PasswordDetailsTableViewMatcher()]
assertWithMatcher:grey_notNil()
error:&error];
return error == nil;
};
GREYAssert(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForUIElementTimeout, condition),
@"Waiting for the view to load");
// Delete last password.
DeleteCredential(@"user2", @"https://example1.com/");
// Check that the current view is now the password manager since we deleted
// the last password.
condition = ^{
NSError* error = nil;
[[EarlGrey
selectElementWithMatcher:grey_accessibilityID(kPasswordsTableViewID)]
assertWithMatcher:grey_notNil()
error:&error];
return error == nil;
};
GREYAssert(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForUIElementTimeout, condition),
@"Waiting for the view to load");
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
- (void)testMovePasswordToAccount {
SavePasswordFormToProfileStore(/*password=*/@"localPassword",
/*username=*/@"username",
/*origin=*/@"https://local.com");
[SigninEarlGrey signinWithFakeIdentity:[FakeSystemIdentity fakeIdentity1]];
OpenPasswordManager();
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
// `passwordMatcher` includes grey_sufficientlyVisible() because there are
// other invisible cells when password details is closed later.
id<GREYMatcher> passwordMatcher = grey_allOf(
ButtonWithAccessibilityID(@"local.com"), grey_sufficientlyVisible(), nil);
id<GREYMatcher> localIconMatcher =
grey_allOf(grey_accessibilityID(kLocalOnlyPasswordIconID),
grey_ancestor(passwordMatcher), nil);
[GetInteractionForListItem(localIconMatcher, kGREYDirectionDown)
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey selectElementWithMatcher:passwordMatcher]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(
kMovePasswordToAccountButtonID)]
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(
kMovePasswordToAccountButtonID)]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(
kMovePasswordToAccountButtonID)]
assertWithMatcher:grey_notVisible()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[GetInteractionForListItem(localIconMatcher, kGREYDirectionDown)
assertWithMatcher:grey_notVisible()];
}
// Regression test for crbug.com/1431975. Similar to testMovePasswordToAccount
// above but the only open tab is an incognito one.
- (void)testMovePasswordToAccountWithOnlyIncognitoTabOpen {
SavePasswordFormToProfileStore(/*password=*/@"localPassword",
/*username=*/@"username",
/*origin=*/@"https://local.com");
[SigninEarlGrey signinWithFakeIdentity:[FakeSystemIdentity fakeIdentity1]];
[ChromeEarlGrey openNewIncognitoTab];
[ChromeEarlGrey closeAllNormalTabs];
OpenPasswordManager();
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
// `passwordMatcher` includes grey_sufficientlyVisible() because there are
// other invisible cells when password details is closed later.
id<GREYMatcher> passwordMatcher = grey_allOf(
ButtonWithAccessibilityID(@"local.com"), grey_sufficientlyVisible(), nil);
id<GREYMatcher> localIconMatcher =
grey_allOf(grey_accessibilityID(kLocalOnlyPasswordIconID),
grey_ancestor(passwordMatcher), nil);
[GetInteractionForListItem(localIconMatcher, kGREYDirectionDown)
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey selectElementWithMatcher:passwordMatcher]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(
kMovePasswordToAccountButtonID)]
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(
kMovePasswordToAccountButtonID)]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(
kMovePasswordToAccountButtonID)]
assertWithMatcher:grey_notVisible()];
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[GetInteractionForListItem(localIconMatcher, kGREYDirectionDown)
assertWithMatcher:grey_notVisible()];
}
// Tests that the save passwords in account section is hidden when syncing.
- (void)testSavePasswordsInAccountHiddenWhenSyncing {
SavePasswordFormToProfileStore();
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
[SigninEarlGrey signinAndEnableLegacySyncFeature:fakeIdentity];
OpenPasswordManager();
OpenSettingsSubmenu();
// Ensure module is hidden.
CheckSavePasswordsInAccountSectionHidden();
// Close password manager settings.
[[EarlGrey
selectElementWithMatcher:grey_allOf(SettingsDoneButton(),
grey_sufficientlyVisible(), nil)]
performAction:grey_tap()];
}
// Tests that the save passwords in account section is hidden when not
// signed-in.
- (void)testSavePasswordsInAccountHiddenWhenNotSignedIn {
SavePasswordFormToProfileStore();
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
OpenPasswordManager();
OpenSettingsSubmenu();
// Ensure module is hidden.
CheckSavePasswordsInAccountSectionHidden();
// Close password manager settings.
[[EarlGrey
selectElementWithMatcher:grey_allOf(SettingsDoneButton(),
grey_sufficientlyVisible(), nil)]
performAction:grey_tap()];
}
// Tests that the save passwords in account section is hidden when not opted-in
// for account storage.
- (void)testSavePasswordsInAccountHiddenWhenNotOptedInToAccountStorage {
SavePasswordFormToProfileStore();
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
[SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
OpenPasswordManager();
OpenSettingsSubmenu();
[ChromeEarlGreyUI waitForAppToIdle];
// Opt out of account storage.
[SigninEarlGreyAppInterface
setSelectedType:(syncer::UserSelectableType::kPasswords)
enabled:NO];
[ChromeEarlGreyUI waitForAppToIdle];
// Ensure module is now hidden.
CheckSavePasswordsInAccountSectionHidden();
}
// Tests that the save passwords in account section is shown when the user is
// eligible.
- (void)testSavePasswordsInAccountShownWhenEligible {
SavePasswordFormToProfileStore(@"passwordtest1", @"user1",
@"https://test1.com");
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
[SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
OpenPasswordManager();
OpenSettingsSubmenu();
// Ensure the move passwords to account section is shown.
CheckSavePasswordsInAccountSectionVisible();
// Close password manager settings menu.
[[EarlGrey
selectElementWithMatcher:grey_allOf(SettingsDoneButton(),
grey_sufficientlyVisible(), nil)]
performAction:grey_tap()];
}
// Tests that the confirmation dialog contains the correct string for saving one
// distinct domain to the account.
- (void)testSavePasswordsInAccountOneDistinctDomain {
SavePasswordFormToProfileStore(@"password1", @"user1",
@"https://example1.com");
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
[SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
OpenPasswordManager();
OpenSettingsSubmenu();
[ChromeEarlGreyUI waitForAppToIdle];
// Tap on save passwords to account button.
[[EarlGrey
selectElementWithMatcher:
grey_accessibilityID(
kPasswordSettingsBulkMovePasswordsToAccountButtonTableViewId)]
performAction:grey_tap()];
[ChromeEarlGreyUI waitForAppToIdle];
// Ensure the confirmation dialog appears with the correct patterned string.
NSString* result = @"You can save your password for example1.com in your "
@"Google Account, [email protected].";
[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(result)]
assertWithMatcher:grey_sufficientlyVisible()];
}
// Tests that the confirmation dialog contains the correct string for saving two
// distinct domains to the account.
- (void)testSavePasswordsInAccountTwoDistinctDomains {
SavePasswordFormToProfileStore(@"password1", @"user1",
@"https://example1.com");
SavePasswordFormToProfileStore(@"password1", @"user1",
@"https://example2.com");
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
[SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
OpenPasswordManager();
OpenSettingsSubmenu();
[ChromeEarlGreyUI waitForAppToIdle];
// Tap on save passwords to account button.
[[EarlGrey
selectElementWithMatcher:
grey_accessibilityID(
kPasswordSettingsBulkMovePasswordsToAccountButtonTableViewId)]
performAction:grey_tap()];
[ChromeEarlGreyUI waitForAppToIdle];
// Ensure the confirmation dialog appears with the correct patterned string.
NSString* result = @"You can save your passwords for example1.com and "
@"example2.com in your Google Account, [email protected].";
[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(result)]
assertWithMatcher:grey_sufficientlyVisible()];
}
// Tests that the confirmation dialog contains the correct string for saving
// three distinct domains to the account.
- (void)testSavePasswordsInAccountThreeDistinctDomains {
SavePasswordFormToProfileStore(@"password1", @"user1",
@"https://example1.com");
SavePasswordFormToProfileStore(@"password1", @"user1",
@"https://example2.com");
SavePasswordFormToProfileStore(@"password1", @"user1",
@"https://example3.com");
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
[SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
OpenPasswordManager();
OpenSettingsSubmenu();
[ChromeEarlGreyUI waitForAppToIdle];
// Tap on save passwords to account button.
[[EarlGrey
selectElementWithMatcher:
grey_accessibilityID(
kPasswordSettingsBulkMovePasswordsToAccountButtonTableViewId)]
performAction:grey_tap()];
[ChromeEarlGreyUI waitForAppToIdle];
// Ensure the confirmation dialog appears with the correct patterned string.
NSString* result = @"You can save your passwords for example1.com, "
@"example2.com, and 1 other "
@"in your Google Account, [email protected].";
[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(result)]
assertWithMatcher:grey_sufficientlyVisible()];
}
// Tests that the confirmation dialog contains the correct string for saving
// four distinct domains to the account.
- (void)testSavePasswordsInAccountFourDistinctDomains {
SavePasswordFormToProfileStore(@"password1", @"user1",
@"https://example1.com");
SavePasswordFormToProfileStore(@"password1", @"user1",
@"https://example2.com");
SavePasswordFormToProfileStore(@"password1", @"user1",
@"https://example3.com");
SavePasswordFormToProfileStore(@"password1", @"user1",
@"https://example4.com");
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
[SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
OpenPasswordManager();
OpenSettingsSubmenu();
[ChromeEarlGreyUI waitForAppToIdle];
// Tap on save passwords to account button.
[[EarlGrey
selectElementWithMatcher:
grey_accessibilityID(
kPasswordSettingsBulkMovePasswordsToAccountButtonTableViewId)]
performAction:grey_tap()];
[ChromeEarlGreyUI waitForAppToIdle];
// Ensure the confirmation dialog appears with the correct patterned string.
NSString* result =
@"You can save your passwords for example1.com, example2.com, and 2 "
@"others in your Google Account, [email protected].";
[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(result)]
assertWithMatcher:grey_sufficientlyVisible()];
}
// Tests that the local password is moved when accepting the confirmation
// dialog, and that the corresponding snackbar appears.
- (void)testSavePasswordsInAccountFlowCompletes {
SavePasswordFormToProfileStore(@"password1", @"user1",
@"https://example1.com");
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
[SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
OpenPasswordManager();
OpenSettingsSubmenu();
[ChromeEarlGreyUI waitForAppToIdle];
// Tap on save passwords to account button.
[[EarlGrey
selectElementWithMatcher:
grey_accessibilityID(
kPasswordSettingsBulkMovePasswordsToAccountButtonTableViewId)]
performAction:grey_tap()];
[ChromeEarlGreyUI waitForAppToIdle];
// Tap on "Save in Account" (accept) button.
[SaveInAccountConfirmationDialogButton() performAction:grey_tap()];
[ChromeEarlGreyUI waitForAppToIdle];
// Ensure that the save passwords to account module has disappeared.
CheckSavePasswordsInAccountSectionHidden();
// Ensure the correct snackbar appears.
[ChromeEarlGrey
waitForSufficientlyVisibleElementWithMatcher:
grey_accessibilityLabel(
@"Password saved in your Google Account, [email protected]")];
}
// Tests that the local passwords are correctly handled in the save
// passwords to account flow, and the correct snackbar appears.
- (void)testSavePasswordsInAccountFlowCompletesMovingPasswords {
SavePasswordFormToProfileStore(@"password1", @"user1",
@"https://example1.com");
SavePasswordFormToProfileStore(@"password2", @"user1",
@"https://example1.com");
SavePasswordFormToProfileStore(@"password1", @"user1",
@"https://example2.com");
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
[SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
SavePasswordFormToProfileStore(@"password1", @"user1",
@"https://example1.com");
OpenPasswordManager();
OpenSettingsSubmenu();
[ChromeEarlGreyUI waitForAppToIdle];
// Tap on save passwords to account button.
[[EarlGrey
selectElementWithMatcher:
grey_accessibilityID(
kPasswordSettingsBulkMovePasswordsToAccountButtonTableViewId)]
performAction:grey_tap()];
[ChromeEarlGreyUI waitForAppToIdle];
// Tap on "Save in Account" (accept) button.
[SaveInAccountConfirmationDialogButton() performAction:grey_tap()];
[ChromeEarlGreyUI waitForAppToIdle];
// Ensure that the save passwords to account module has disappeared.
CheckSavePasswordsInAccountSectionHidden();
// Ensure the correct snackbar appears.
[ChromeEarlGrey
waitForSufficientlyVisibleElementWithMatcher:
grey_accessibilityLabel(
@"Passwords saved in your Google Account, [email protected]")];
}
// Checks opening the password manager with a successful reauthentication shows
// the Password Manager.
- (void)testOpenPasswordManagerWithSuccessfulAuth {
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
// Delay the auth result to be able to validate that the passwords are not
// visible until the result is emitted.
[PasswordSettingsAppInterface
mockReauthenticationModuleShouldReturnSynchronously:NO];
OpenPasswordManager();
// Password Manager should be blocked until successful auth.
[[EarlGrey selectElementWithMatcher:PasswordsTableViewMatcher()]
assertWithMatcher:grey_notVisible()];
// Successful auth should remove blocking view and Password Manager should be
// visible visible.
[PasswordSettingsAppInterface mockReauthenticationModuleReturnMockedResult];
[[EarlGrey selectElementWithMatcher:PasswordsTableViewMatcher()]
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
// Check password manager visit metric.
CheckPasswordManagerVisitMetricCount(1);
// Check Reauthentication UI metrics.
CheckReauthenticationUIEventMetricTotalCount(2);
CheckReauthenticationUIEventMetric(ReauthenticationEvent::kAttempt);
CheckReauthenticationUIEventMetric(ReauthenticationEvent::kSuccess);
}
// Checks opening the password manager with a failed reauthentication does not
// show passwords and closes the Password Manager.
- (void)testOpenPasswordManagerWithFailedAuth {
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kFailure];
// Delay the auth result to be able to validate that the passwords are not
// visible until the result is emitted.
[PasswordSettingsAppInterface
mockReauthenticationModuleShouldReturnSynchronously:NO];
OpenPasswordManager();
// Password Manager should be blocked until successful auth.
[[EarlGrey selectElementWithMatcher:PasswordsTableViewMatcher()]
assertWithMatcher:grey_notVisible()];
// Failed auth should dismiss the Password Manager, the Settings menu is
// displayed.
[PasswordSettingsAppInterface mockReauthenticationModuleReturnMockedResult];
CheckVisibilityOfElement(/*matcher=*/SettingsCollectionView(),
/*is_visible=*/true);
// Check password manager visit metric.
CheckPasswordManagerVisitMetricCount(0);
// Check Reauthentication UI metrics.
CheckReauthenticationUIEventMetricTotalCount(2);
CheckReauthenticationUIEventMetric(ReauthenticationEvent::kAttempt);
CheckReauthenticationUIEventMetric(ReauthenticationEvent::kFailure);
}
// Checks opening the password manager with no passcode does not show passwords
// and displays an alert prompting the user to set a passcode.
- (void)testOpenPasswordManagerWithWithoutPasscodeSet {
[PasswordSettingsAppInterface mockReauthenticationModuleCanAttempt:NO];
OpenPasswordManager();
// Password Manager should be blocked.
[[EarlGrey selectElementWithMatcher:PasswordsTableViewMatcher()]
assertWithMatcher:grey_notVisible()];
if ([PasswordSettingsAppInterface isPasscodeSettingsAvailable]) {
// Go to Settings should be present.
[[EarlGrey selectElementWithMatcher:chrome_test_util::AlertAction(
@"Go to Settings")]
assertWithMatcher:grey_notNil()];
} else {
// Dismiss the passcode alert, this should dismiss the Password Manager.
[[EarlGrey
selectElementWithMatcher:grey_accessibilityLabel(l10n_util::GetNSString(
IDS_IOS_SETTINGS_SET_UP_SCREENLOCK_TITLE))]
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey selectElementWithMatcher:chrome_test_util::OKButton()]
performAction:grey_tap()];
// Check for the Settings page after Password Manager is gone.
[[EarlGrey
selectElementWithMatcher:chrome_test_util::SettingsCollectionView()]
assertWithMatcher:grey_sufficientlyVisible()];
}
// Check Reauthentication UI metrics.
CheckReauthenticationUIEventMetricTotalCount(2);
CheckReauthenticationUIEventMetric(ReauthenticationEvent::kAttempt);
CheckReauthenticationUIEventMetric(ReauthenticationEvent::kMissingPasscode);
// Check password manager visit metric.
CheckPasswordManagerVisitMetricCount(0);
}
// Checks that the user is prompt to set a passcode after backgrounding and
// foregrounding the app and no passcode is set, while in a surface that
// requires Local Authentication.
- (void)testSetPasscodeAlertPresentedAfterBackgroundingApp {
OpenPasswordManager();
// Password Manager should be visible.
CheckVisibilityOfElement(/*matcher=*/PasswordsTableViewMatcher(),
/*is_visible=*/true);
// Simulate passcode not set.
[PasswordSettingsAppInterface mockReauthenticationModuleCanAttempt:NO];
// Trigger local authentication by backgrounding the app.
[[AppLaunchManager sharedManager] backgroundAndForegroundApp];
if ([PasswordSettingsAppInterface isPasscodeSettingsAvailable]) {
// Go to Settings should be present.
[[EarlGrey selectElementWithMatcher:chrome_test_util::AlertAction(
@"Go to Settings")]
assertWithMatcher:grey_notNil()];
} else {
// Dismiss the passcode alert, this should dismiss the Password Manager.
[[EarlGrey
selectElementWithMatcher:grey_accessibilityLabel(l10n_util::GetNSString(
IDS_IOS_SETTINGS_SET_UP_SCREENLOCK_TITLE))]
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey selectElementWithMatcher:chrome_test_util::OKButton()]
performAction:grey_tap()];
// Check for the Settings page after Password Manager is gone.
[[EarlGrey
selectElementWithMatcher:chrome_test_util::SettingsCollectionView()]
assertWithMatcher:grey_sufficientlyVisible()];
}
}
// Tests that the Password Manager is opened is search mode when opened from the
// Search Passwords widget.
- (void)testOpenSearchPasswordsWidget {
// Add a saved password to not get the Password Manager's empty state.
SavePasswordFormToProfileStore();
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
[ChromeEarlGrey
sceneOpenURL:
GURL("chromewidgetkit://search-passwords-widget/search-passwords")];
// The Password Manager should be visible behind the keyboard.
[ChromeEarlGrey waitForKeyboardToAppear];
[[EarlGrey selectElementWithMatcher:PasswordsTableViewMatcher()]
assertWithMatcher:grey_minimumVisiblePercent(0.5)];
// The search bar should be enabled.
[[EarlGrey selectElementWithMatcher:SearchTextField()]
assertWithMatcher:grey_userInteractionEnabled()];
// Dismiss the search controller and the Password Manager.
[[EarlGrey
selectElementWithMatcher:ButtonWithAccessibilityLabelId(IDS_CANCEL)]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
performAction:grey_tap()];
}
// Tests that the indication to open the Password Manager in search mode does
// not persist after it was first opened. For example, the search bar shouldn't
// get automatically enabled when going back to the Password Manager when the
// Password Manager was initially opened with the Search Passwords widget.
- (void)testGoingBackAfterOpeningInSearchMode {
// Add a saved password to not get the Password Manager's empty state.
SavePasswordFormToProfileStore();
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kSuccess];
// Open the Password Manager in search mode with the Search Passwords widget.
[ChromeEarlGrey
sceneOpenURL:
GURL("chromewidgetkit://search-passwords-widget/search-passwords")];
// The search bar should be enabled.
[[EarlGrey selectElementWithMatcher:SearchTextField()]
assertWithMatcher:grey_userInteractionEnabled()];
// Dismiss the search controller.
[[EarlGrey
selectElementWithMatcher:ButtonWithAccessibilityLabelId(IDS_CANCEL)]
performAction:grey_tap()];
// Open password details.
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
// Navigate back to the Password Manager. The search bar should not be
// enabled.
[[EarlGrey selectElementWithMatcher:NavigationBarBackButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SearchTextField()]
assertWithMatcher:grey_userInteractionEnabled()];
}
// Tests that tapping the close button of the Password Manager widget promo
// removes the promo from the table view.
- (void)testClosingPasswordManagerWidgetPromo {
// Add a saved password to not get the Password Manager's empty state.
SavePasswordFormToProfileStore();
OpenPasswordManager();
// The Password Manager widget promo should be visible.
CheckPasswordManagerWidgetPromoVisible();
// Tap the promo's close button.
[[EarlGrey selectElementWithMatcher:PasswordManagerWidgetPromoCloseButton()]
performAction:grey_tap()];
// The Password Manager widget promo should now be gone.
[[EarlGrey selectElementWithMatcher:PasswordManagerWidgetPromo()]
assertWithMatcher:grey_notVisible()];
}
// Tests that tapping the more info button of the Password Manager widget
// promo displays the instructions on how to install the widget.
- (void)testOpeningPasswordManagerWidgetPromoInstructions {
// Add a saved password to not get the Password Manager's empty state.
SavePasswordFormToProfileStore();
OpenPasswordManagerWidgetPromoInstructions();
}
// Tests the Password Manager UI is dismissed after a failed local
// authentication while in the Widget Promo Instructions page.
- (void)testOpeningPasswordManagerWidgetPromoInstructionsWithFailedAuth {
// Add a saved password to not get the Password Manager's empty state.
SavePasswordFormToProfileStore();
OpenPasswordManagerWidgetPromoInstructions();
[PasswordSettingsAppInterface mockReauthenticationModuleExpectedResult:
ReauthenticationResult::kFailure];
// Settings UI should be covered by Password Manager UI.
[[EarlGrey
selectElementWithMatcher:chrome_test_util::SettingsCollectionView()]
assertWithMatcher:grey_notVisible()];
// Trigger local authentication by backgrounding the app.
[[AppLaunchManager sharedManager] backgroundAndForegroundApp];
// Failed auth should dismiss the whole Password Manager leaving the Settings
// UI visible.
[[EarlGrey
selectElementWithMatcher:chrome_test_util::SettingsCollectionView()]
assertWithMatcher:grey_sufficientlyVisible()];
}
// Tests that the more info and close buttons of the Password Manager widget
// promo are disabled when the Password Manager is in edit mode.
- (void)testPasswordManagerWidgetPromoInEditMode {
// Add a saved password to not get the Password Manager's empty state.
SavePasswordFormToProfileStore();
OpenPasswordManager();
TapNavigationBarEditButton();
// The Password Manager widget promo should be visible.
[[EarlGrey selectElementWithMatcher:PasswordManagerWidgetPromo()]
assertWithMatcher:grey_sufficientlyVisible()];
// The close button should be disabled.
[[EarlGrey selectElementWithMatcher:PasswordManagerWidgetPromoCloseButton()]
assertWithMatcher:grey_not(grey_enabled())];
// The more info button should be disabled.
[[EarlGrey selectElementWithMatcher:PasswordManagerWidgetPromoMoreInfoButton(
/*enabled=*/false)]
assertWithMatcher:grey_not(grey_enabled())];
}
// Tests that the Password Manager widget promo is as expected when
// transitioning between portrait and landscape modes. Also tests that the close
// button still works after the layout change.
- (void)testPasswordManagerWidgetPromoDeviceOrientation {
if ([ChromeEarlGrey isIPadIdiom]) {
EARL_GREY_TEST_SKIPPED(@"Landscape orientation doesn't change the look of "
@"the instruction view on iPads.");
}
// Add a saved password to not get the Password Manager's empty state.
SavePasswordFormToProfileStore();
OpenPasswordManager();
// The Password Manager widget promo should be visible.
CheckPasswordManagerWidgetPromoVisible();
// The Password Manager widget promo's elements should be visible in landscape
// mode.
[EarlGrey rotateDeviceToOrientation:UIDeviceOrientationLandscapeRight
error:nil];
CheckPasswordManagerWidgetPromoVisible();
// The promo's close button should still be tappable in landscape mode.
[[EarlGrey selectElementWithMatcher:PasswordManagerWidgetPromoCloseButton()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:PasswordManagerWidgetPromo()]
assertWithMatcher:grey_notVisible()];
}
// Tests that the Password Manager widget promo's instruction screen can be
// dismissed by swipping it down and by tapping its close button.
- (void)testDismissPasswordManagerWidgetPromoInstructionsScreen {
// Add a saved password to not get the Password Manager's empty state.
SavePasswordFormToProfileStore();
OpenPasswordManager();
// Tap the promo's more info button.
[[EarlGrey
selectElementWithMatcher:PasswordManagerWidgetPromoMoreInfoButton()]
performAction:grey_tap()];
// The Password Manager widget promo's instructions should be visible.
CheckPasswordManagerWidgetPromoInstructionScreenVisible();
// Swipe down to dismiss the instructions screen.
[[EarlGrey selectElementWithMatcher:PasswordManagerWidgetPromoInstructions()]
performAction:grey_swipeFastInDirection(kGREYDirectionDown)];
// The Password Manager widget promo should be visible.
[[EarlGrey selectElementWithMatcher:PasswordManagerWidgetPromo()]
assertWithMatcher:grey_sufficientlyVisible()];
// Tap the promo's more info button.
[[EarlGrey
selectElementWithMatcher:PasswordManagerWidgetPromoMoreInfoButton()]
performAction:grey_tap()];
// The Password Manager widget promo's instructions should be visible.
CheckPasswordManagerWidgetPromoInstructionScreenVisible();
// Tap the instruction screen's close button.
[[EarlGrey selectElementWithMatcher:
PasswordManagerWidgetPromoInstructionsCloseButton()]
performAction:grey_tap()];
// The Password Manager widget promo should be visible.
[[EarlGrey selectElementWithMatcher:PasswordManagerWidgetPromo()]
assertWithMatcher:grey_sufficientlyVisible()];
}
// Tests that the Password Manager widget promo's instruction screen is as
// expected when transitioning between portrait and landscape modes.
- (void)testPasswordManagerWidgetPromoInstructionsDeviceOrientation {
if ([ChromeEarlGrey isIPadIdiom]) {
EARL_GREY_TEST_SKIPPED(@"Landscape orientation doesn't change the look of "
@"the instruction view on iPads.");
}
// Add a saved password to not get the Password Manager's empty state.
SavePasswordFormToProfileStore();
OpenPasswordManager();
// The Password Manager widget promo should be visible.
[[EarlGrey selectElementWithMatcher:PasswordManagerWidgetPromo()]
assertWithMatcher:grey_sufficientlyVisible()];
// Tap the promo's more info button.
[[EarlGrey
selectElementWithMatcher:PasswordManagerWidgetPromoMoreInfoButton()]
performAction:grey_tap()];
// The Password Manager widget promo's instructions should be visible with its
// image.
CheckPasswordManagerWidgetPromoInstructionScreenVisible();
// The Password Manager widget promo's instructions should be visible with no
// image in landscape mode.
[EarlGrey rotateDeviceToOrientation:UIDeviceOrientationLandscapeRight
error:nil];
CheckPasswordManagerWidgetPromoInstructionScreenVisible(
/*image_hidden=*/true);
// When going back to portrait mode, the Password Manager widget promo's
// instructions should be visible with its image.
[EarlGrey rotateDeviceToOrientation:UIDeviceOrientationPortrait error:nil];
CheckPasswordManagerWidgetPromoInstructionScreenVisible();
}
// Checks password details page offers move to account option if the password is
// saved in the local store.
- (void)testMovePasswordToAccountStoreIfSignedIn {
// Save form to be moved to account later.
SavePasswordFormToProfileStore();
// Sign in.
FakeSystemIdentity* fakeIdentity = [FakeSystemIdentity fakeIdentity1];
[SigninEarlGrey signinWithFakeIdentity:fakeIdentity];
// Open password details view for the saved password.
OpenPasswordManager();
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
// Verify the locally-saved password details page has a move to account
// option.
[[EarlGrey selectElementWithMatcher:PasswordDetailsMoveToAccountButton()]
assertWithMatcher:grey_sufficientlyVisible()];
// Tap on the move to account button to move the local password to account
// store.
[[EarlGrey selectElementWithMatcher:PasswordDetailsMoveToAccountButton()]
performAction:grey_tap()];
[ChromeEarlGreyUI waitForAppToIdle];
// Verify the password details page does not show the move to account
// option anymore.
[[EarlGrey selectElementWithMatcher:PasswordDetailsMoveToAccountButton()]
assertWithMatcher:grey_notVisible()];
}
- (void)testAddPasswordTappingAnywhereInNoteFieldFocusesTextView {
SavePasswordFormToProfileStore();
OpenPasswordManager();
// Tap "Add Password"
[[EarlGrey selectElementWithMatcher:AddPasswordButton()]
performAction:grey_tap()];
// Tap the "Note" label of the note cell.
[[EarlGrey selectElementWithMatcher:PasswordDetailNoteLabel()]
performAction:grey_tap()];
// Check that the text view is the first responder.
[[EarlGrey selectElementWithMatcher:PasswordDetailNote()]
assertWithMatcher:grey_firstResponder()];
}
- (void)testEditPasswordTappingAnywhereInNoteFieldFocusesTextView {
SaveExamplePasswordFormToProfileStoreWithNote();
OpenPasswordManager();
// Open password details and tap "Edit".
[[self interactionForSinglePasswordEntryWithDomain:@"example.com"]
performAction:grey_tap()];
TapNavigationBarEditButton();
// Tap the "Note" label of the note cell.
[[EarlGrey selectElementWithMatcher:PasswordDetailNoteLabel()]
performAction:grey_tap()];
// Check that the text view is the first responder.
[[EarlGrey selectElementWithMatcher:PasswordDetailNote()]
assertWithMatcher:grey_firstResponder()];
}
// Tests that deleting the last affiliated group (composed of at least two
// passwords) while the Password Manager view is scrolled down results in
// showing the empty state view. See crbug.com/41492279.
- (void)testDeletingLastAffiliatedGroup {
// Form an affiliated group with two passwords.
SavePasswordFormToProfileStore(/*password=*/@"password1",
/*username=*/@"user1",
/*origin=*/@"https://example11.com");
SavePasswordFormToProfileStore(/*password=*/@"password2",
/*username=*/@"user2",
/*origin=*/@"https://example11.com");
OpenPasswordManager();
// Scroll down to the bottom of the Password Manager table view.
[[EarlGrey
selectElementWithMatcher:grey_accessibilityID(kPasswordsTableViewID)]
performAction:grey_scrollToContentEdge(kGREYContentEdgeBottom)];
// Delete the affiliated group with a left swipe.
[[EarlGrey selectElementWithMatcher:grey_text(@"example11.com")]
performAction:grey_swipeSlowInDirectionWithStartPoint(kGREYDirectionLeft,
0.9, 0.5)];
[ChromeEarlGreyUI waitForAppToIdle];
// Verify that the empty view is now displayed.
[[EarlGrey selectElementWithMatcher:password_manager_test_utils::
PasswordManagerEmptyView()]
assertWithMatcher:grey_sufficientlyVisible()];
}
- (void)testSwipingAnotherAffiliatedGroupWhenAnotherIsInEditMode {
// Form an affiliated group with two passwords.
SavePasswordFormToProfileStore(/*password=*/@"password1",
/*username=*/@"user1",
/*origin=*/@"https://example.com");
SavePasswordFormToProfileStore(/*password=*/@"password2",
/*username=*/@"user2",
/*origin=*/@"https://example1.com");
OpenPasswordManager();
// Swipe the affiliated group until the "Delete" button is revealed.
[[EarlGrey selectElementWithMatcher:grey_text(@"example.com")]
performAction:chrome_test_util::SwipeToShowDeleteButton()];
[ChromeEarlGreyUI waitForAppToIdle];
// Assert that "Delete" button is displayed.
[[EarlGrey selectElementWithMatcher:grey_kindOfClassName(
@"UISwipeActionStandardButton")]
assertWithMatcher:grey_sufficientlyVisible()];
// Swipe the next affiliated group until the "Delete" button is revealed.
[[EarlGrey selectElementWithMatcher:grey_text(@"example1.com")]
performAction:chrome_test_util::SwipeToShowDeleteButton()];
// Wait for the first affiliated group to reach its original state (i.e. no
// visible delete button).
[ChromeEarlGreyUI waitForAppToIdle];
// Assert that "Delete" button is displayed for the second affiliated group.
[[EarlGrey selectElementWithMatcher:grey_kindOfClassName(
@"UISwipeActionStandardButton")]
assertWithMatcher:grey_sufficientlyVisible()];
}
@end