blob: 7498478c94d6ba79f08096039b2abd27a560b1ad [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <memory>
#import <string_view>
#import "base/ios/ios_util.h"
#import "base/strings/strcat.h"
#import "base/strings/string_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/time/time.h"
#import "components/autofill/core/browser/field_types.h"
#import "components/autofill/core/common/autofill_debug_features.h"
#import "components/autofill/core/common/autofill_features.h"
#import "components/autofill/ios/common/constants.h"
#import "components/autofill/ios/common/features.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/authentication/test/signin_earl_grey.h"
#import "ios/chrome/browser/authentication/test/signin_earl_grey_ui_test_util.h"
#import "ios/chrome/browser/autofill/ui_bundled/address_editor/autofill_constants.h"
#import "ios/chrome/browser/autofill/ui_bundled/autofill_app_interface.h"
#import "ios/chrome/browser/autofill/ui_bundled/bottom_sheet/bottom_sheet_constants.h"
#import "ios/chrome/browser/badges/model/features.h"
#import "ios/chrome/browser/badges/ui_bundled/badge_constants.h"
#import "ios/chrome/browser/infobars/ui_bundled/banners/infobar_banner_constants.h"
#import "ios/chrome/browser/infobars/ui_bundled/infobar_earl_grey_ui_test_util.h"
#import "ios/chrome/browser/infobars/ui_bundled/modals/infobar_address_profile_modal_constants.h"
#import "ios/chrome/browser/metrics/model/metrics_app_interface.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/earl_grey/chrome_actions.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey_ui.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/web/public/test/http_server/http_server.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/strings/grit/ui_strings.h"
namespace {
// URLs of the test pages.
constexpr char kProfileForm[] = "/autofill_smoke_test.html";
constexpr char kXframeFormPage[] = "/xhr_xframe_submit.html";
constexpr char kFullAddressFormPage[] = "/full_address_form.html";
// Ids of fields in the form.
constexpr char kFormElementName[] = "form_name";
constexpr char kFormElementEmail[] = "form_email";
constexpr char kFormElementSubmit[] = "submit_profile";
// Minimal cooldown period to wait for between typing characters. The bare
// minimum should be the frame rate, something aroung 17ms (1/60hz), but we
// prefer giving extra buffer as there are probably other latencies.
constexpr base::TimeDelta kTypingCoolDownPeriod = base::Milliseconds(50);
// Email value used by the tests.
constexpr std::string_view kEmail = "foo1@gmail.com";
// Histogram bucket representing renderer errors.
constexpr int kRendererErrorHistogramBucket = 8;
struct FullAddressFormPageParams {
// True if the submission should be default prevented.
bool default_prevented = false;
// True if there should be redirection done after submitting with
// a parameter that can stop the submit event from being handled by Autofill.
bool redirect = false;
// True if the submission should be prevented from propagating to any other
// listener regardless of their positioning.
bool stop_immediate_propagation = false;
// True if multiple submissions should be scheduled.
bool multiple_submissions = false;
// True if when using `multiple_submissions` the last programmatic submission
// should be skipped.
bool multiple_submissions_skip_programmatic = false;
};
// Matcher for the banner button.
id<GREYMatcher> BannerButtonMatcher() {
return chrome_test_util::ButtonWithAccessibilityLabel(l10n_util::GetNSString(
IDS_IOS_AUTOFILL_SAVE_ADDRESS_MESSAGE_PRIMARY_ACTION));
}
// Matcher for the update banner button.
id<GREYMatcher> UpdateBannerButtonMatcher() {
return chrome_test_util::ButtonWithAccessibilityLabel(l10n_util::GetNSString(
IDS_IOS_AUTOFILL_UPDATE_ADDRESS_MESSAGE_PRIMARY_ACTION));
}
// Matcher for the "Save Address" modal button.
id<GREYMatcher> ModalButtonMatcher() {
return chrome_test_util::ButtonWithAccessibilityLabel(
l10n_util::GetNSString(IDS_AUTOFILL_SAVE_ADDRESS_PROMPT_OK_BUTTON_LABEL));
}
// Matcher for the "Update Address" modal button.
id<GREYMatcher> UpdateModalButtonMatcher() {
return chrome_test_util::ButtonWithAccessibilityLabel(l10n_util::GetNSString(
IDS_AUTOFILL_UPDATE_ADDRESS_ADD_NEW_INFO_PROMPT_OK_BUTTON_LABEL));
}
// Matcher for the modal button.
id<GREYMatcher> ModalEditButtonMatcher() {
return grey_allOf(grey_accessibilityID(kInfobarSaveAddressModalEditButton),
grey_accessibilityTrait(UIAccessibilityTraitButton), nil);
}
// Matcher for the migration button in modal view.
id<GREYMatcher> ModalMigrationButtonMatcher() {
return chrome_test_util::ButtonWithAccessibilityLabel(l10n_util::GetNSString(
IDS_AUTOFILL_ADDRESS_MIGRATION_TO_ACCOUNT_PROMPT_OK_BUTTON_LABEL));
}
// Matcher for a country entry with the given accessibility label.
id<GREYMatcher> CountryEntry(NSString* label) {
return grey_allOf(chrome_test_util::ButtonWithAccessibilityLabel(label),
grey_userInteractionEnabled(), grey_sufficientlyVisible(),
nil);
}
// Matcher for the search bar.
id<GREYMatcher> SearchBar() {
// Match using the accessibility trait for a search field.
return grey_allOf(grey_accessibilityTrait(UIAccessibilityTraitSearchField),
grey_sufficientlyVisible(), nil);
}
// Matcher for the search bar's scrim.
id<GREYMatcher> SearchBarScrim() {
return grey_accessibilityID(kAutofillCountrySelectionSearchScrimId);
}
id<GREYMatcher> TextFieldWithLabel(NSString* textFieldLabel) {
return grey_allOf(grey_accessibilityID(
[textFieldLabel stringByAppendingString:@"_textField"]),
grey_kindOfClass([UITextField class]), nil);
}
id<GREYMatcher> EditProfileBottomSheet() {
return grey_accessibilityID(kEditProfileBottomSheetViewIdentfier);
}
// Slowly type characters using the keyboard by waiting between each tap.
void SlowlyTypeText(NSString* text) {
for (NSUInteger i = 0; i < [text length]; ++i) {
// Wait some time before typing the character.
base::test::ios::SpinRunLoopWithMinDelay(kTypingCoolDownPeriod);
// Type a single character so the user input can be effective.
[ChromeEarlGrey
simulatePhysicalKeyboardEvent:[text
substringWithRange:NSMakeRange(i, 1)]
flags:0];
}
// Give some cooldown period so the character has the time to be typed
// before doing something else on the page.
base::test::ios::SpinRunLoopWithMinDelay(kTypingCoolDownPeriod);
}
void TypeTextInXframeField(NSString* fieldID, NSString* text) {
// Focus on the field that corresponds to `fieldID` to pop up the keyboard.
NSString* script = [NSString
stringWithFormat:@"document.querySelector('iframe')"
".contentDocument.getElementById('%@').focus()",
fieldID];
[ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
// Type the `text` on the field.
SlowlyTypeText(text);
}
} // namespace
@interface SaveProfileEGTest : ChromeTestCase
@end
@implementation SaveProfileEGTest
- (void)setUp {
[super setUp];
chrome_test_util::GREYAssertErrorNil(
[MetricsAppInterface setupHistogramTester]);
}
- (void)tearDownHelper {
chrome_test_util::GREYAssertErrorNil(
[MetricsAppInterface releaseHistogramTester]);
// Clear existing profile.
[AutofillAppInterface clearProfilesStore];
[super tearDownHelper];
}
// TODO(crbug.com/391826905): Re-enable this test on simulator.
#if TARGET_OS_SIMULATOR
#define MAYBE_testEditBottomSheetAlertBySwipingDown \
FLAKY_testEditBottomSheetAlertBySwipingDown
#else
#define MAYBE_testEditBottomSheetAlertBySwipingDown \
testEditBottomSheetAlertBySwipingDown
#endif
- (AppLaunchConfiguration)appConfigurationForTestCase {
AppLaunchConfiguration config;
if ([self isRunningTest:@selector(testUserData_AccountSave)] ||
[self
isRunningTest:@selector(testUserData_LocalHideBottomSheetOnCancel)]) {
// These test cases need a badge.
config.features_disabled.push_back(kAutofillBadgeRemoval);
}
config.features_disabled.push_back(
autofill::features::debug::kAutofillServerCommunication);
if ([self isRunningTest:@selector(testStickySavePromptJourney)]) {
config.features_enabled.push_back(kAutofillStickyInfobarIos);
}
if ([self isRunningTest:@selector
(testUserData_AccountSave_AutofillAcrossIframe_XHR)]) {
config.features_enabled.push_back(
autofill::features::kAutofillAcrossIframesIos);
config.features_enabled.push_back(kAutofillFixXhrForXframe);
}
if ([self isRunningTest:@selector(testSubmissionErrorReporting_Enabled)]) {
config.features_enabled.push_back(kAutofillReportFormSubmissionErrors);
}
if ([self isRunningTest:@selector(testSubmissionErrorReporting_Disabled)]) {
config.features_disabled.push_back(kAutofillReportFormSubmissionErrors);
}
if ([self isRunningTest:@selector(testUserData_LocalUpdate)]) {
config.features_enabled.push_back(
autofill::features::kAutofillEnableSupportForHomeAndWork);
}
// TODO(crbug.com/428189566): Re-enable after the test is fixed for
// ios-fieldtrial-rel.
if ([self isRunningTest:@selector
(DISABLED_testSubmissionCountReporting_ScheduledTask)] ||
[self isRunningTest:@selector
(DISABLED_testSubmissionCountReporting_UnloadPage)]) {
config.features_enabled.push_back(kAutofillCountFormSubmissionInRenderer);
}
return config;
}
#pragma mark - Test helper methods
// Fills the president profile in the form by clicking on the button, submits
// the form to the save address profile infobar.
- (void)fillPresidentProfileAndShowSaveInfobar {
GREYAssertTrue(self.testServer->Start(), @"Server did not start.");
[ChromeEarlGrey loadURL:self.testServer->GetURL(kProfileForm)];
// Ensure there are no saved profiles.
GREYAssertEqual(0U, [AutofillAppInterface profilesCount],
@"There should be no saved profile.");
[ChromeEarlGrey tapWebStateElementWithID:@"fill_profile_president"];
[ChromeEarlGrey tapWebStateElementWithID:@"submit_profile"];
[InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
}
// Triggers the save infobar via XHR submission.
- (void)triggerSaveInfobarViaXHRSubmission {
// Load the xframe address form page.
GREYAssertTrue(self.testServer->Start(), @"Server did not start.");
[ChromeEarlGrey loadURL:self.testServer->GetURL(kXframeFormPage)];
// Ensure there are no saved profiles at this point - make sure we start from
// a clean slate.
GREYAssertEqual(0U, [AutofillAppInterface profilesCount],
@"There should be no saved profile.");
// Manually type the text in the fields so profile saving can be triggered
// when autofill across iframes is enabled which require manually editing the
// fields.
TypeTextInXframeField(@"form_name", @"User");
TypeTextInXframeField(@"form_address", @"1234 Pkw Ave");
TypeTextInXframeField(@"form_city", @"MuteCity");
TypeTextInXframeField(@"form_zip", @"12345");
// Trigger XHR submission in the child frame using the dedicated button in the
// main frame.
[ChromeEarlGrey tapWebStateElementWithID:@"do-xhr-submit"];
[InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
}
- (void)fillPresidentProfileAndShowSaveModal {
[self fillPresidentProfileAndShowSaveInfobar];
// Accept the banner.
[[EarlGrey selectElementWithMatcher:BannerButtonMatcher()]
performAction:grey_tap()];
[InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
}
// Fills, submits the form and saves the address profile.
- (void)fillFormAndSaveProfile {
[self fillPresidentProfileAndShowSaveModal];
// Save the profile.
[[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
performAction:grey_tap()];
// Ensure profile is saved locally.
GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
@"Profile should have been saved.");
}
// Loads, fills, and submits the full address form.
- (void)loadFullAddressFormWithParams:(FullAddressFormPageParams)params {
// Start server.
GREYAssertTrue(self.testServer->Start(), @"Server did not start.");
auto makeQueryString = [](FullAddressFormPageParams params) -> std::string {
std::vector<std::string_view> queryParameters;
if (params.default_prevented) {
queryParameters.push_back("preventDefault");
}
if (params.redirect) {
queryParameters.push_back("redirectWhenSubmissionPrevented");
}
if (params.stop_immediate_propagation) {
queryParameters.push_back("stopImmediatePropagation");
}
if (params.multiple_submissions) {
queryParameters.push_back("triggerMultipleSubmissions");
}
if (params.multiple_submissions_skip_programmatic) {
queryParameters.push_back("triggerMultipleSubmissionsNoProgrammatic");
}
return base::JoinString(queryParameters, "&");
};
// Get the URL for the served test page with the query parameters for setting
// it up.
const GURL baseURL = self.testServer->GetURL(kFullAddressFormPage);
GURL::Replacements replacements;
std::string query = makeQueryString(params).c_str();
replacements.SetQueryStr(query);
const GURL fullURL = baseURL.ReplaceComponents(replacements);
// Load the URL and wait for its content to be loaded.
[ChromeEarlGrey loadURL:fullURL];
// Wait until the expected content is loaded in the DOM. If the page is in an
// error state this verification will fail.
NSString* wait_content_script =
@"document.body.innerText.includes('Address Form Test Page')";
[ChromeEarlGrey waitForJavaScriptCondition:wait_content_script];
// Call the helper function embedded in the page content to fill the form.
[ChromeEarlGrey evaluateJavaScriptForSideEffect:@"FillForm();"];
}
- (void)loadAndSubmitFullAddressFormWithParams:
(FullAddressFormPageParams)params {
[self loadFullAddressFormWithParams:params];
// Submit the form via the dedicated <button>.
[ChromeEarlGrey tapWebStateElementWithID:@"submit-button"];
}
// Focuses on the name field and initiates autofill on the form with the saved
// profile.
- (void)focusOnNameAndAutofill {
// Ensure there is a saved local profile.
GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
@"There should a saved local profile.");
// Tap on a field to trigger form activity.
[[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
performAction:chrome_test_util::TapWebElementWithId(kFormElementName)];
// Wait for the keyboard to appear.
[ChromeEarlGrey waitForKeyboardToAppear];
// Wait for suggestions as it may take some time because of form fetch
// throttling or other delays.
[ChromeEarlGrey
waitForMatcher:chrome_test_util::AutofillSuggestionViewMatcher()];
// Tap on the suggestion.
[[EarlGrey selectElementWithMatcher:chrome_test_util::
AutofillSuggestionViewMatcher()]
performAction:grey_tap()];
// Verify Web Content was filled.
NSString* javaScriptCondition = [NSString
stringWithFormat:@"document.getElementById('%s').value.length > 0",
kFormElementName];
[ChromeEarlGrey waitForJavaScriptCondition:javaScriptCondition];
}
#pragma mark - Tests
// Ensures that the profile is updated after submitting the form.
- (void)testUserData_LocalUpdate {
// Save a profile.
[self fillFormAndSaveProfile];
// Load the form.
[ChromeEarlGrey loadURL:self.testServer->GetURL(kProfileForm)];
// Autofill the form.
[self focusOnNameAndAutofill];
// Tap on email field.
[[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
performAction:chrome_test_util::TapWebElementWithId(kFormElementEmail)];
// Wait for the keyboard to appear.
[ChromeEarlGrey waitForKeyboardToAppear];
// Populate the email field.
// TODO(crbug.com/40916974): This should use grey_typeText when fixed.
for (char letter : kEmail) {
if (letter == '@') {
[ChromeEarlGrey simulatePhysicalKeyboardEvent:@"@"
flags:UIKeyModifierShift];
continue;
}
[ChromeEarlGrey
simulatePhysicalKeyboardEvent:[NSString stringWithFormat:@"%c", letter]
flags:0];
}
// Submit the form.
[[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
performAction:chrome_test_util::TapWebElementWithId(kFormElementSubmit)];
[InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
// Accept the banner.
[[EarlGrey selectElementWithMatcher:UpdateBannerButtonMatcher()]
performAction:grey_tap()];
[InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
// Update the profile.
[[EarlGrey selectElementWithMatcher:UpdateModalButtonMatcher()]
performAction:grey_tap()];
// Ensure profile is updated locally.
GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
@"Profile should have been updated.");
}
// Ensures that the profile is saved to Account after submitting the form.
- (void)testUserData_AccountSave {
[SigninEarlGrey signinWithFakeIdentity:[FakeSystemIdentity fakeIdentity1]];
[self fillPresidentProfileAndShowSaveModal];
// Verify that the address badge is displayed.
[[EarlGrey selectElementWithMatcher:
grey_accessibilityID(
kBadgeButtonSaveAddressProfileAccessibilityIdentifier)]
assertWithMatcher:grey_notNil()];
id<GREYMatcher> footerMatcher = grey_text(
l10n_util::GetNSStringF(IDS_IOS_AUTOFILL_SAVE_ADDRESS_IN_ACCOUNT_FOOTER,
base::UTF8ToUTF16(kEmail)));
[[EarlGrey selectElementWithMatcher:footerMatcher]
assertWithMatcher:grey_sufficientlyVisible()];
// Save the profile.
[[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
performAction:grey_tap()];
// Ensure profile is saved locally.
GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
@"Profile should have been saved.");
[SigninEarlGrey signOut];
}
// Test that the profile can be saved for the edge case where the address form
// is hosted in a frame and is submitted there via XHR - when autofill across
// iframes is enabled.
- (void)testUserData_AccountSave_AutofillAcrossIframe_XHR {
// Sign-in so the profile can be saved into the account.
[SigninEarlGrey signinWithFakeIdentity:[FakeSystemIdentity fakeIdentity1]];
// Trigger the save infobar via XHR submission in the child frame.
[self triggerSaveInfobarViaXHRSubmission];
// Accept the banner to save the profile.
[[EarlGrey selectElementWithMatcher:BannerButtonMatcher()]
performAction:grey_tap()];
[InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
// Save the profile.
[[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
performAction:grey_tap()];
// Ensure profile is saved locally.
GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
@"Profile should have been saved.");
[SigninEarlGrey signOut];
}
// Ensures that the profile is saved to Account after submitting and editing the
// form.
- (void)testUserData_AccountEdit {
[SigninEarlGrey signinWithFakeIdentity:[FakeSystemIdentity fakeIdentity1]];
[self fillPresidentProfileAndShowSaveModal];
// Edit the profile.
[[EarlGrey selectElementWithMatcher:ModalEditButtonMatcher()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:TextFieldWithLabel(@"City")]
performAction:grey_replaceText(@"New York")];
id<GREYMatcher> footerMatcher = grey_text(
l10n_util::GetNSStringF(IDS_IOS_AUTOFILL_SAVE_ADDRESS_IN_ACCOUNT_FOOTER,
base::UTF8ToUTF16(kEmail)));
[[EarlGrey selectElementWithMatcher:footerMatcher]
assertWithMatcher:grey_sufficientlyVisible()];
// Save the profile.
[[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
performAction:grey_tap()];
// Ensure profile is saved locally.
GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
@"Profile should have been saved.");
[SigninEarlGrey signOut];
}
// Ensures that if a local profile is filled in a form and submitted, the user
// is asked for a migration prompt and the profile is moved to the Account.
- (void)testUserData_MigrationToAccount {
[AutofillAppInterface clearProfilesStore];
// Store one local address.
[AutofillAppInterface saveExampleProfile];
[SigninEarlGrey signinWithFakeIdentity:[FakeSystemIdentity fakeIdentity1]];
GREYAssertTrue(self.testServer->Start(), @"Server did not start.");
[ChromeEarlGrey loadURL:self.testServer->GetURL(kProfileForm)];
[self focusOnNameAndAutofill];
// Submit the form.
[[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
performAction:chrome_test_util::TapWebElementWithId(kFormElementSubmit)];
[InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
// Accept the banner.
[[EarlGrey selectElementWithMatcher:BannerButtonMatcher()]
performAction:grey_tap()];
[InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
id<GREYMatcher> footerMatcher = grey_text(l10n_util::GetNSStringF(
IDS_IOS_AUTOFILL_ADDRESS_MIGRATE_IN_ACCOUNT_FOOTER,
base::UTF8ToUTF16(kEmail)));
// Check if there is footer suggesting it's a migration prompt.
[[EarlGrey selectElementWithMatcher:footerMatcher]
assertWithMatcher:grey_sufficientlyVisible()];
// Confirm to migrate the profile.
[[EarlGrey selectElementWithMatcher:ModalMigrationButtonMatcher()]
performAction:grey_tap()];
// Ensure the count of profiles saved remains unchanged.
GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
@"Profile should have been saved.");
[SigninEarlGrey signOut];
}
// Tests that the user can edit a field in the edit via in the save address
// flow.
- (void)testUserData_LocalEditViaBottomSheet {
// Fill and submit the form.
[self fillPresidentProfileAndShowSaveModal];
// Edit the profile.
[[EarlGrey selectElementWithMatcher:ModalEditButtonMatcher()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:TextFieldWithLabel(@"City")]
performAction:grey_replaceText(@"New York")];
[[EarlGrey
selectElementWithMatcher:grey_allOf(grey_accessibilityLabel(
l10n_util::GetNSString(
IDS_IOS_AUTOFILL_COUNTRY)),
grey_userInteractionEnabled(), nil)]
performAction:grey_tap()];
// Focus the search bar.
[[EarlGrey selectElementWithMatcher:SearchBar()] performAction:grey_tap()];
// Verify the scrim is visible when search bar is focused but not typed in.
[[EarlGrey selectElementWithMatcher:SearchBarScrim()]
assertWithMatcher:grey_notNil()];
// Verify the cancel button is visible and unfocuses search bar when tapped.
[ChromeEarlGreyUI clearAndDismissSearchBar];
// Verify countries are searchable using their name in the current
// locale.
[[EarlGrey selectElementWithMatcher:SearchBar()] performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:SearchBar()]
performAction:grey_replaceText(@"Germany")];
// Verify that scrim is not visible anymore.
[[EarlGrey selectElementWithMatcher:SearchBarScrim()]
assertWithMatcher:grey_nil()];
[[EarlGrey selectElementWithMatcher:CountryEntry(@"Germany")]
performAction:grey_tap()];
// Save the profile.
[[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
performAction:grey_tap()];
// Ensure profile is saved locally.
GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
@"Profile should have been saved.");
}
// Tests that the bottom sheet to edit address is just hidden on Cancel.
- (void)testUserData_LocalHideBottomSheetOnCancel {
// Fill and submit the form.
[self fillPresidentProfileAndShowSaveModal];
// Edit the profile.
[[EarlGrey selectElementWithMatcher:ModalEditButtonMatcher()]
performAction:grey_tap()];
// Tap "Cancel"
[[EarlGrey
selectElementWithMatcher:grey_allOf(
grey_accessibilityID(
kEditProfileBottomSheetCancelButton),
grey_accessibilityTrait(
UIAccessibilityTraitButton),
nil)] performAction:grey_tap()];
// Open modal by selecting the badge that shouldn't be accepted.
[[EarlGrey selectElementWithMatcher:
grey_accessibilityID(
kBadgeButtonSaveAddressProfileAccessibilityIdentifier)]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
assertWithMatcher:grey_sufficientlyVisible()];
// Save the profile.
[[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
performAction:grey_tap()];
}
// Tests that the save address flow is still working correctly when the address
// badge is removed.
- (void)FLAKY_testSaveWithoutBadge {
[SigninEarlGrey signinWithFakeIdentity:[FakeSystemIdentity fakeIdentity1]];
[self fillPresidentProfileAndShowSaveModal];
// Verify that the address badge is not displayed.
[[EarlGrey selectElementWithMatcher:
grey_accessibilityID(
kBadgeButtonSaveAddressProfileAccessibilityIdentifier)]
assertWithMatcher:grey_nil()];
id<GREYMatcher> footerMatcher = grey_text(
l10n_util::GetNSStringF(IDS_IOS_AUTOFILL_SAVE_ADDRESS_IN_ACCOUNT_FOOTER,
base::UTF8ToUTF16(kEmail)));
[[EarlGrey selectElementWithMatcher:footerMatcher]
assertWithMatcher:grey_sufficientlyVisible()];
// Save the profile.
[[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
performAction:grey_tap()];
// Ensure profile is saved locally.
GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
@"Profile should have been saved.");
[SigninEarlGrey signOut];
}
// Tests the sticky address prompt journey where the prompt remains there when
// navigating without an explicit user gesture, and then the prompt is dismissed
// when navigating with a user gesture. Test with the address save prompt but
// the type of address prompt doesn't matter in this test case.
- (void)testStickySavePromptJourney {
[self fillPresidentProfileAndShowSaveInfobar];
{
// Reloading page from script shouldn't dismiss the infobar.
NSString* script = @"location.reload();";
[ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
}
{
// Assigning url from script to the page aka open an url shouldn't dismiss
// the infobar.
NSString* script = @"window.location.assign(window.location.href);";
[ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
}
{
// Pushing new history entry without reloading content shouldn't dismiss the
// infobar.
NSString* script = @"history.pushState({}, '', 'destination2.html');";
[ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
}
{
// Replacing history entry without reloading content shouldn't dismiss the
// infobar.
NSString* script = @"history.replaceState({}, '', 'destination3.html');";
[ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
}
// Wait some time for things to settle.
base::test::ios::SpinRunLoopWithMinDelay(base::Milliseconds(200));
// Verify that the prompt is still there after the non-user initiated
// navigations.
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(
kInfobarBannerViewIdentifier)]
assertWithMatcher:grey_sufficientlyVisible()];
// Navigate with an emulated user gesture and verify that dismisses the
// prompt.
[ChromeEarlGrey loadURL:self.testServer->GetURL(kProfileForm)];
[InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
}
// Tests that there is an alert shown if the user tries to dismiss an alert
// after they edited a field in the edit prompt without saving.
// Note that this test is defined above.
- (void)MAYBE_testEditBottomSheetAlertBySwipingDown {
// TODO(crbug.com/377270834): Fix implementation on iPad.
if ([ChromeEarlGrey isIPadIdiom]) {
EARL_GREY_TEST_SKIPPED(@"Test fails on iPad currently.");
}
// TODO(crbug.com/443713676): Re-enable the test.
#if !TARGET_OS_SIMULATOR
if (base::ios::IsRunningOnIOS26OrLater()) {
if (![ChromeEarlGrey isIPadIdiom]) {
EARL_GREY_TEST_DISABLED(@"Test disabled on iOS 26.");
}
}
#endif
// Fill and submit the form.
[self fillPresidentProfileAndShowSaveModal];
// Edit the profile.
[[EarlGrey selectElementWithMatcher:ModalEditButtonMatcher()]
performAction:grey_tap()];
// Replace city field value.
[[EarlGrey selectElementWithMatcher:TextFieldWithLabel(@"City")]
performAction:grey_replaceText(@"New York")];
// Swipe down the sheet.
[[EarlGrey selectElementWithMatcher:EditProfileBottomSheet()]
performAction:grey_swipeFastInDirection(kGREYDirectionDown)];
id<GREYMatcher> keepEditingAlert = grey_text(
l10n_util::GetNSString(IDS_IOS_VIEW_CONTROLLER_DISMISS_CANCEL_CHANGES));
// Ensure the error alert is shown.
[ChromeEarlGrey waitForUIElementToAppearWithMatcher:keepEditingAlert];
// Keep editing.
[[EarlGrey selectElementWithMatcher:keepEditingAlert]
performAction:grey_tap()];
// Swipe down the sheet again.
[[EarlGrey selectElementWithMatcher:EditProfileBottomSheet()]
performAction:grey_swipeFastInDirection(kGREYDirectionDown)];
// Check that the save changes button exists.
id<GREYMatcher> saveChangesAlert = grey_text(
l10n_util::GetNSString(IDS_IOS_VIEW_CONTROLLER_DISMISS_SAVE_CHANGES));
[ChromeEarlGrey waitForUIElementToAppearWithMatcher:saveChangesAlert];
[[EarlGrey selectElementWithMatcher:saveChangesAlert]
performAction:grey_tap()];
}
// Tests that the 'Save' button is only enabled when all the required fields are
// filled.
// TODO(crbug.com/407573862): Re-enable after the test is fixed for
// ios-fieldtrial-rel.
- (void)DISABLED_testSaveButtonEnabledStateDependingOnRequiredFields {
[SigninEarlGreyUI signinWithFakeIdentity:[FakeSystemIdentity fakeIdentity1]];
[ChromeEarlGrey waitForSyncTransportStateActiveWithTimeout:base::Seconds(10)];
// Fill and submit the form.
[self fillPresidentProfileAndShowSaveModal];
// Edit the profile.
[[EarlGrey selectElementWithMatcher:ModalEditButtonMatcher()]
performAction:grey_tap()];
// Ensure the 'Save' button is initially enabled.
[[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
assertWithMatcher:grey_enabled()];
NSString* streetAddressLabel = base::SysUTF8ToNSString(
autofill::FieldTypeToDeveloperRepresentationString(
autofill::ADDRESS_HOME_STREET_ADDRESS));
// Empty the street address field, which is required.
[[EarlGrey selectElementWithMatcher:TextFieldWithLabel(streetAddressLabel)]
performAction:grey_replaceText(@"")];
// Scroll down to show the 'Save' button.
[[EarlGrey selectElementWithMatcher:EditProfileBottomSheet()]
performAction:grey_scrollToContentEdge(kGREYContentEdgeBottom)];
// Ensure the 'Save' button is disabled.
[[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
assertWithMatcher:grey_not(grey_enabled())];
// Re-fill the street address field.
[[EarlGrey selectElementWithMatcher:TextFieldWithLabel(streetAddressLabel)]
performAction:grey_replaceText(@"Street")];
// Ensure the 'Save' button is enabled.
[[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
assertWithMatcher:grey_enabled()];
// Sign out.
[SigninEarlGrey signOut];
}
// Tests that submission is detected hence the infobar is displayed when the
// "form" event behind the submission is `defaultPrevented` while the
// corresponding feature allows it.
- (void)testSubmissionDetection_defaultPrevented_whenAllowed {
// Sign-in so the profile can be saved into the account.
[SigninEarlGrey signinWithFakeIdentity:[FakeSystemIdentity fakeIdentity1]];
// Submit the form with `defaultPrevented` not considered.
FullAddressFormPageParams params{.default_prevented = true, .redirect = true};
[self loadAndSubmitFullAddressFormWithParams:params];
// Wait on the infobar to be displayed after submission.
[InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
// Accept the banner to save the profile.
[[EarlGrey selectElementWithMatcher:BannerButtonMatcher()]
performAction:grey_tap()];
// Wait for the save profile dialog to appear.
[ChromeEarlGrey waitForMatcher:ModalButtonMatcher()];
// Save the profile.
[[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
performAction:grey_tap()];
// Ensure profile is saved.
GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
@"Profile should have been saved.");
[SigninEarlGrey signOut];
}
// Tests that multiple submissions on the same form are deduped when deduping is
// enabled where only one submission per form element is allowed when.
- (void)testSubmissionDetectionWithDeduping {
// Submit the form with `defaultPrevented` not considered and without
// redirecting so the same form can be submitted multiple time.
FullAddressFormPageParams params{.default_prevented = true,
.redirect = false};
[self loadAndSubmitFullAddressFormWithParams:params];
// Wait on the infobar to be displayed after submission, meaning that
// submission was detected.
[InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
// Spam submissions.
for (int i = 0; i < 5; ++i) {
[ChromeEarlGrey tapWebStateElementWithID:@"submit-button"];
}
// Wait some time so the hypothetical form submission messages would have been
// sent over to the browser by then.
base::test::ios::SpinRunLoopWithMinDelay(base::Milliseconds(200));
// Verify that only one submission was actually recorded despite triggering
// multiple submissions on the same form.
chrome_test_util::GREYAssertErrorNil([MetricsAppInterface
expectTotalCount:1
forHistogram:@"Autofill.iOS.FormSubmission.OutcomeV2"]);
}
// Tests that the submission errors that occur in the renderer are reported to
// the browser.
- (void)testSubmissionErrorReporting_Enabled {
// Inject a bug that will trigger error when handling the form submission in
// the renderer.
constexpr char kInjectedBug[] = R"(
// Swizzle autofillSubmissionData() with an erroring function.
gcrweb.gCrWeb.fill.autofillSubmissionData = function() {
throw new Error("Oh no, something bad happened!");
};
// This is to give a return value to make the thing handling the JS
// execution happy.
true
)";
// Load page without submitting the form.
[self loadFullAddressFormWithParams:{}];
// Inject the bug in the submission handler so it triggers an error that will
// be reported to the browser.
[ChromeEarlGrey
evaluateJavaScriptInIsolatedWorldForSideEffect:base::SysUTF8ToNSString(
kInjectedBug)];
// Now that the submission handler is buggy, submit the form to trigger the
// error.
[ChromeEarlGrey tapWebStateElementWithID:@"submit-button"];
// Verify that no infobar is displayed when there is a submission error.
[InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
// Verify that the submission error was reported and recorded.
GREYAssertTrue(
base::test::ios::WaitUntilConditionOrTimeout(
base::Milliseconds(200),
^{
NSError* error = [MetricsAppInterface
expectUniqueSampleWithCount:1
forBucket:kRendererErrorHistogramBucket
forHistogram:
@"Autofill.iOS.FormSubmission.OutcomeV2"];
return error == nil;
}),
@"Timed out waiting for the submission error uma record.");
}
// Tests that the submission errors that occur in the renderer are not reported
// to the browser when the feature is disabled.
- (void)testSubmissionErrorReporting_Disabled {
// Inject a bug that will trigger error when handling the form submission in
// the renderer.
constexpr char kInjectedBug[] = R"(
// Swizzle autofillSubmissionData() with an erroring function.
gcrweb.gCrWeb.fill.autofillSubmissionData = function() {
throw new Error("Oh no, something bad happened!");
};
// This is to give a return value to make the thing handling the JS
// execution happy.
true
)";
// Load page without submitting the form.
[self loadFullAddressFormWithParams:{}];
// Inject the bug in the submission handler so it triggers an error that will
// be reported to the browser.
[ChromeEarlGrey
evaluateJavaScriptInIsolatedWorldForSideEffect:base::SysUTF8ToNSString(
kInjectedBug)];
// Now that the submission handler is buggy, submit the form to trigger the
// error.
[ChromeEarlGrey tapWebStateElementWithID:@"submit-button"];
// Verify for some time that no infobar is displayed when there is a
// submission error.
[InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
// Verify that no submission error was not reported and recorded. At this
// point there should have been enough time to hypothetically handle the
// submit event if there was no error.
chrome_test_util::GREYAssertErrorNil([MetricsAppInterface
expectTotalCount:0
forHistogram:@"Autofill.iOS.FormSubmission.OutcomeV2"]);
}
// Tests submission count reporting with the scheduled task for the 2 types of
// form submission, regular and programmatic.
// TODO(crbug.com/428189566): Re-enable after the test is fixed for
// ios-fieldtrial-rel.
- (void)DISABLED_testSubmissionCountReporting_ScheduledTask {
// Load the page without submitting the form.
[self loadFullAddressFormWithParams:{.default_prevented = true,
.multiple_submissions = true}];
// Submit the form which will trigger 4 extra submissions consisting of 3
// button click submissions and 1 `form.submit()` programmatic submission,
// for a total of 5 submissions.
[ChromeEarlGrey tapWebStateElementWithID:@"submit-button"];
// Navigate back so the scheduled reporting task can be completed which
// requires the frame to be "active".
[[EarlGrey selectElementWithMatcher:chrome_test_util::BackButton()]
performAction:grey_tap()];
[ChromeEarlGrey waitForWebStateContainingText:"Address Form Test Page"];
// Verify that that all the form click submissions were recorded. Wait enough
// time for the report to be sent from the renderer. The reporting period is 2
// seconds.
GREYAssertTrue(
base::test::ios::WaitUntilConditionOrTimeout(
base::Seconds(3),
^{
// Expect 5 submissions in total, 4 regulars and 1 programmatic.
NSError* error = [MetricsAppInterface
expectCount:4
forBucket:static_cast<int>(CountedSubmissionType::kHtmlEvent)
forHistogram:
@"Autofill.iOS.FormActivity."
@"SubmissionDetectedBeforeProcessing.PerType.FromAll"];
error = [MetricsAppInterface
expectCount:1
forBucket:static_cast<int>(
CountedSubmissionType::kProgrammatic)
forHistogram:
@"Autofill.iOS.FormActivity."
@"SubmissionDetectedBeforeProcessing.PerType.FromAll"];
// Expect 2 regular submissions from the instant reports. Which
// is the maximal number allowed by the quotas. Regular submissions
// are handled in the isolated world.
error = [MetricsAppInterface
expectCount:2
forBucket:static_cast<int>(CountedSubmissionType::kHtmlEvent)
forHistogram:
@"Autofill.iOS.FormActivity."
@"SubmissionDetectedBeforeProcessing.PerType.FromInstant"];
// Expect 1 programmatic submission from the instant reports. This
// can bust the limit of 2 because the content world quotas are used
// for the programmatic submission while the rest of Autofill is in
// the isolated world (which includes the detection of regular click
// submissions). Each world has its own quotas basically.
error = [MetricsAppInterface
expectCount:1
forBucket:static_cast<int>(
CountedSubmissionType::kProgrammatic)
forHistogram:
@"Autofill.iOS.FormActivity."
@"SubmissionDetectedBeforeProcessing.PerType.FromInstant"];
// Expect 2 regular submissions from the scheduled reports as all
// the quotas were used for the 2 first reports.
error = [MetricsAppInterface
expectCount:2
forBucket:static_cast<int>(CountedSubmissionType::kHtmlEvent)
forHistogram:@"Autofill.iOS.FormActivity."
@"SubmissionDetectedBeforeProcessing.PerType."
@"FromScheduledTask"];
// Expect 4 batches of reports, 3 from the instant reports (1
// report per batch) and 1 batch with the 2 reports that were
// reported by the scheduled task.
error = [MetricsAppInterface
expectCount:3
forBucket:1
forHistogram:@"Autofill.iOS.FormActivity."
@"SubmissionDetectedBeforeProcessing.BatchSize"];
error = [MetricsAppInterface
expectCount:1
forBucket:2
forHistogram:@"Autofill.iOS.FormActivity."
@"SubmissionDetectedBeforeProcessing.BatchSize"];
return error == nil;
}),
@"Timed out waiting for the form submission metrics.");
}
// Tests submission count reporting when unloading a page.
// TODO(crbug.com/428189566): Re-enable after the test is fixed for
// ios-fieldtrial-rel.
- (void)DISABLED_testSubmissionCountReporting_UnloadPage {
// Load page without submitting the form.
[self loadFullAddressFormWithParams:{.default_prevented = true,
.multiple_submissions_skip_programmatic =
true}];
// Submit the form which will trigger 4 extra submissions consisting of 3
// button click submissions and 1 `form.submit()` programmatic submission,
// for a total of 5 submissions.
[ChromeEarlGrey tapWebStateElementWithID:@"submit-button"];
// Reload the page content which triggers reporting.
[ChromeEarlGrey reload];
[ChromeEarlGrey waitForWebStateContainingText:"Address Form Test Page"];
// Verify that that all the submissions were reported. No need for a long
// timeout as the report was sent when reloading the page so all the metrics
// should have been recorded at this point.
GREYAssertTrue(
base::test::ios::WaitUntilConditionOrTimeout(
base::Milliseconds(200),
^{
// Expect 4 regular submission in total.
NSError* error = [MetricsAppInterface
expectCount:4
forBucket:static_cast<int>(CountedSubmissionType::kHtmlEvent)
forHistogram:
@"Autofill.iOS.FormActivity."
@"SubmissionDetectedBeforeProcessing.PerType.FromAll"];
// Expect 2 regular submissions from the instant reports. Which
// is the maximal number allowed by the quotas. Regular submissions
// are handled in the isolated world.
error = [MetricsAppInterface
expectCount:2
forBucket:static_cast<int>(CountedSubmissionType::kHtmlEvent)
forHistogram:
@"Autofill.iOS.FormActivity."
@"SubmissionDetectedBeforeProcessing.PerType.FromInstant"];
// Expect 2 regular submissions from the report triggered by
// unloading the page as all the quotas were used for the 2 first
// reports.
error = [MetricsAppInterface
expectCount:2
forBucket:static_cast<int>(CountedSubmissionType::kHtmlEvent)
forHistogram:@"Autofill.iOS.FormActivity."
@"SubmissionDetectedBeforeProcessing.PerType."
@"FromUnloadPage"];
// Expect 4 batches of reports, 2 from the instant reports (1
// report per batch) and 1 batch with the 2 reports that were
// reported when unloading the page.
error = [MetricsAppInterface
expectCount:2
forBucket:1
forHistogram:@"Autofill.iOS.FormActivity."
@"SubmissionDetectedBeforeProcessing.BatchSize"];
error = [MetricsAppInterface
expectCount:1
forBucket:2
forHistogram:@"Autofill.iOS.FormActivity."
@"SubmissionDetectedBeforeProcessing.BatchSize"];
return error == nil;
}),
@"Timed out waiting for the form submission metrics.");
}
@end