diff options
Diffstat (limited to 'browser/extensions/formautofill/test/browser/address')
19 files changed, 3059 insertions, 0 deletions
diff --git a/browser/extensions/formautofill/test/browser/address/browser.toml b/browser/extensions/formautofill/test/browser/address/browser.toml new file mode 100644 index 0000000000..bff24a88b0 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser.toml @@ -0,0 +1,47 @@ +[DEFAULT] +prefs = [ + "extensions.formautofill.addresses.enabled=true", + "extensions.formautofill.addresses.capture.requiredFields=''", + "toolkit.telemetry.ipcBatchTimeout=0", # lower the interval for event telemetry in the content process to update the parent process +] +support-files = [ + "../head.js", + "../../fixtures/autocomplete_address_basic.html", + "../../fixtures/capture_address_on_page_navigation.html", + "../../fixtures/without_autocomplete_address_basic.html", + "head_address.js", +] + +["browser_address_autofill_nimbus.js"] + +["browser_address_capture_form_removal.js"] + +["browser_address_capture_page_navigation.js"] + +["browser_address_doorhanger_confirmation_popup.js"] + +["browser_address_doorhanger_display.js"] + +["browser_address_doorhanger_invalid_fields.js"] + +["browser_address_doorhanger_multiple_tabs.js"] + +["browser_address_doorhanger_non_mergeable_fields.js"] + +["browser_address_doorhanger_not_shown.js"] + +["browser_address_doorhanger_state.js"] + +["browser_address_doorhanger_tel.js"] + +["browser_address_doorhanger_ui.js"] + +["browser_address_doorhanger_unsupported_region.js"] + +["browser_address_telemetry.js"] + +["browser_edit_address_doorhanger_display.js"] + +["browser_edit_address_doorhanger_display_state.js"] + +["browser_edit_address_doorhanger_save_edited_fields.js"] diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_autofill_nimbus.js b/browser/extensions/formautofill/test/browser/address/browser_address_autofill_nimbus.js new file mode 100644 index 0000000000..0ea100cf8f --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_autofill_nimbus.js @@ -0,0 +1,70 @@ +"use strict"; + +const { ExperimentAPI } = ChromeUtils.importESModule( + "resource://nimbus/ExperimentAPI.sys.mjs" +); + +const { ExperimentFakes } = ChromeUtils.importESModule( + "resource://testing-common/NimbusTestUtils.sys.mjs" +); + +const { FormAutofill } = ChromeUtils.importESModule( + "resource://autofill/FormAutofill.sys.mjs" +); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.experiments.enabled", false], + ["extensions.formautofill.addresses.supportedCountries", "FR"], + ], + }); +}); + +add_task(async function test_address_autofill_feature_enabled() { + await ExperimentAPI.ready(); + + const cleanupExperiment = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "address-autofill-feature", + value: { status: true }, + }); + + is( + NimbusFeatures["address-autofill-feature"].getVariable("status"), + true, + "Nimbus feature should be enabled" + ); + + is( + FormAutofill.isAutofillAddressesAvailable, + true, + "Address autofill should be available when feature is enabled in nimbus." + ); + + await cleanupExperiment(); +}); + +add_task(async function test_address_autofill_feature_disabled() { + await ExperimentAPI.ready(); + + const cleanupExperiment = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "address-autofill-feature", + value: { status: false }, + }); + + NimbusFeatures["address-autofill-feature"].recordExposureEvent(); + + is( + NimbusFeatures["address-autofill-feature"].getVariable("status"), + false, + "Nimbus feature shouldn't be enabled" + ); + + is( + FormAutofill.isAutofillAddressesAvailable, + false, + "Address autofill shouldn't be available when feature is off in nimbus." + ); + + await cleanupExperiment(); +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_capture_form_removal.js b/browser/extensions/formautofill/test/browser/address/browser_address_capture_form_removal.js new file mode 100644 index 0000000000..f94fc8241f --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_capture_form_removal.js @@ -0,0 +1,124 @@ +"use strict"; + +async function expectSavedAddresses(expectedAddresses) { + const addresses = await getAddresses(); + is( + addresses.length, + expectedAddresses.length, + `${addresses.length} address in the storage` + ); + + for (let i = 0; i < expectedAddresses.length; i++) { + for (const [key, value] of Object.entries(expectedAddresses[i])) { + is(addresses[i][key] ?? "", value, `field ${key} should be equal`); + } + } + return addresses; +} + +const ADDRESS_FIELD_VALUES = { + "given-name": "John", + organization: "Sesame Street", + "street-address": "123 Sesame Street", +}; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.capture.enabled", true], + ["extensions.formautofill.addresses.supported", "on"], + ["extensions.formautofill.heuristics.captureOnFormRemoval", true], + ], + }); + await removeAllRecords(); +}); + +/** + * Tests if the address is captured (address doorhanger is shown) after a + * successful xhr or fetch request followed by a form removal and + * that the stored address record has the right values. + */ +add_task(async function test_address_captured_after_form_removal() { + const onStorageChanged = waitForStorageChangedEvents("add"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + const onPopupShown = waitForPopupShown(); + + info("Update identified address fields"); + // We don't submit the form + await focusUpdateSubmitForm( + browser, + { + focusSelector: "#given-name", + newValues: { + "#given-name": ADDRESS_FIELD_VALUES["given-name"], + "#organization": ADDRESS_FIELD_VALUES.organization, + "#street-address": ADDRESS_FIELD_VALUES["street-address"], + }, + }, + false + ); + + info("Infer a successfull fetch request"); + await SpecialPowers.spawn(browser, [], async () => { + await content.fetch( + "https://example.org/browser/browser/extensions/formautofill/test/browser/empty.html" + ); + }); + + info("Infer form removal"); + await SpecialPowers.spawn(browser, [], async function () { + let form = content.document.getElementById("form"); + form.parentNode.remove(form); + }); + info("Wait for address doorhanger"); + await onPopupShown; + + info("Click Save in address doorhanger"); + await clickDoorhangerButton(MAIN_BUTTON, 0); + } + ); + + info("Wait for the address to be added to the storage."); + await onStorageChanged; + + info("Ensure that address record was captured and saved correctly."); + await expectSavedAddresses([ADDRESS_FIELD_VALUES]); + + await removeAllRecords(); +}); + +/** + * Tests that the address is not captured without a prior fetch or xhr request event + */ +add_task(async function test_address_not_captured_without_prior_fetch() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + info("Update identified address fields"); + // We don't submit the form + await focusUpdateSubmitForm( + browser, + { + focusSelector: "#given-name", + newValues: { + "#given-name": ADDRESS_FIELD_VALUES["given-name"], + "#organization": ADDRESS_FIELD_VALUES.organization, + "#street-address": ADDRESS_FIELD_VALUES["street-address"], + }, + }, + false + ); + + info("Infer form removal"); + await SpecialPowers.spawn(browser, [], async function () { + let form = content.document.getElementById("form"); + form.parentNode.remove(form); + }); + + info("Ensure that address doorhanger is not shown"); + await ensureNoDoorhanger(browser); + } + ); +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_capture_page_navigation.js b/browser/extensions/formautofill/test/browser/address/browser_address_capture_page_navigation.js new file mode 100644 index 0000000000..f68f89d4fe --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_capture_page_navigation.js @@ -0,0 +1,125 @@ +"use strict"; + +const ADDRESS_VALUES = { + "#given-name": "Test User", + "#organization": "Sesame Street", + "#street-address": "123 Sesame Street", +}; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.capture.enabled", true], + ["extensions.formautofill.addresses.supported", "on"], + ["extensions.formautofill.heuristics.captureOnPageNavigation", true], + ], + }); +}); + +/** + * Tests if the address is captured (address doorhanger is shown) + * after adding an entry to the browser's session history stack + */ +add_task(async function test_address_captured_after_changing_request_state() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_WITH_PAGE_NAVIGATION_BUTTONS }, + async function (browser) { + const onPopupShown = waitForPopupShown(); + + info("Update identified address fields"); + await focusUpdateSubmitForm( + browser, + { + focusSelector: "#given-name", + newValues: ADDRESS_VALUES, + }, + false // We don't submit the form + ); + + info("Change request state"); + await SpecialPowers.spawn(browser, [], () => { + const historyPushStateButton = + content.document.getElementById("historyPushState"); + historyPushStateButton.click(); + }); + + info("Wait for address doorhanger"); + await onPopupShown; + + ok(true, "Address doorhanger is shown"); + } + ); +}); + +/** + * Tests if the address is captured (address doorhanger is shown) + * after navigating by opening another resource + */ +add_task(async function test_address_captured_after_navigation_same_window() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_WITH_PAGE_NAVIGATION_BUTTONS }, + async function (browser) { + const onPopupShown = waitForPopupShown(); + + info("Update identified address fields"); + // We don't submit the form + await focusUpdateSubmitForm( + browser, + { + focusSelector: "#given-name", + newValues: ADDRESS_VALUES, + }, + false // We don't submit the form + ); + + info("Navigate with window.location"); + await SpecialPowers.spawn(browser, [], () => { + const windowLocationButton = + content.document.getElementById("windowLocation"); + windowLocationButton.click(); + }); + + info("Wait for address doorhanger"); + await onPopupShown; + + ok(true, "Address doorhanger is shown"); + } + ); +}); + +/** + * Test that a form submission is infered only once. + */ +add_task(async function test_form_submission_infered_only_once() { + await setStorage(TEST_ADDRESS_1); + + let onUsed = waitForStorageChangedEvents("notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_WITH_PAGE_NAVIGATION_BUTTONS }, + async function (browser) { + // Progress listener is added on address field identification + await openPopupOn(browser, "form #given-name"); + + info("Fill address input fields without changing the values"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + + info("Submit form"); + await SpecialPowers.spawn(browser, [], async function () { + // Progress listener is removed after form submission + let form = content.document.getElementById("form"); + form.querySelector("input[type=submit]").click(); + }); + } + ); + await onUsed; + + const addresses = await getAddresses(); + + is( + addresses[0].timesUsed, + 1, + "timesUsed field set to 1, so form submission was only infered once" + ); + await removeAllRecords(); +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_confirmation_popup.js b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_confirmation_popup.js new file mode 100644 index 0000000000..44fca889fd --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_confirmation_popup.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function expectSavedAddresses(expectedCount) { + const addresses = await getAddresses(); + is( + addresses.length, + expectedCount, + `${addresses.length} address in the storage` + ); + return addresses; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.capture.enabled", true], + ["extensions.formautofill.addresses.supported", "on"], + ], + }); +}); + +add_task(async function test_save_doorhanger_show_confirmation() { + await expectSavedAddresses(0); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + // Show the save doorhanger + const onSavePopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": "Test User", + "#organization": "Sesame Street", + "#street-address": "123 Sesame Street", + "#tel": "1-345-345-3456", + }, + }); + await onSavePopupShown; + + // click the main button and expect seeing the confirmation + const hintShownAndVerified = verifyConfirmationHint(browser, false); + await clickDoorhangerButton(MAIN_BUTTON, 0); + + info("waiting for verifyConfirmationHint"); + await hintShownAndVerified; + info("waiting for verifyConfirmationHint <<"); + } + ); + + await expectSavedAddresses(1); + await removeAllRecords(); +}); + +add_task(async function test_update_doorhanger_show_confirmation() { + await setStorage(TEST_ADDRESS_3); + await expectSavedAddresses(1); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + // Show the update doorhanger + const onUpdatePopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": TEST_ADDRESS_3["given-name"], + "#street-address": `${TEST_ADDRESS_3["street-address"]} 4F`, + "#postal-code": TEST_ADDRESS_3["postal-code"], + }, + }); + await onUpdatePopupShown; + + // click the main button and expect seeing the confirmation + const hintShownAndVerified = verifyConfirmationHint(browser, false); + await clickDoorhangerButton(MAIN_BUTTON, 0); + + info("waiting for verifyConfirmationHint"); + await hintShownAndVerified; + } + ); + + await expectSavedAddresses(1); + await removeAllRecords(); +}); + +add_task(async function test_edit_doorhanger_show_confirmation() { + await expectSavedAddresses(0); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + // Show the save doorhanger + const onSavePopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": "Test User", + "#organization": "Sesame Street", + "#street-address": "123 Sesame Street", + "#tel": "1-345-345-3456", + }, + }); + await onSavePopupShown; + + // Show the edit doorhanger + const onEditPopupShown = waitForPopupShown(); + await clickAddressDoorhangerButton(EDIT_ADDRESS_BUTTON); + await onEditPopupShown; + + // click the main button and expect seeing the confirmation + const hintShownAndVerified = verifyConfirmationHint(browser, false); + await clickDoorhangerButton(MAIN_BUTTON, 0); + + info("waiting for verifyConfirmationHint"); + await hintShownAndVerified; + } + ); + + await expectSavedAddresses(1); + await removeAllRecords(); +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_display.js b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_display.js new file mode 100644 index 0000000000..184f802b11 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_display.js @@ -0,0 +1,336 @@ +"use strict"; + +async function expectSavedAddresses(expectedCount) { + const addresses = await getAddresses(); + is( + addresses.length, + expectedCount, + `${addresses.length} address in the storage` + ); + return addresses; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.capture.enabled", true], + ["extensions.formautofill.addresses.supported", "on"], + ], + }); +}); + +add_task(async function test_save_doorhanger_shown_no_profile() { + await expectSavedAddresses(0); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": "John", + "#family-name": "Doe", + "#organization": "Sesame Street", + "#street-address": "123 Sesame Street", + "#tel": "1-345-345-3456", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON, 0); + } + ); + + await expectSavedAddresses(1); + await removeAllRecords(); +}); + +add_task(async function test_save_doorhanger_shown_different_address() { + await setStorage(TEST_ADDRESS_1); + await expectSavedAddresses(1); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": TEST_ADDRESS_2["given-name"], + "#street-address": TEST_ADDRESS_2["street-address"], + "#country": TEST_ADDRESS_2.country, + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON, 0); + } + ); + + await expectSavedAddresses(2); + await removeAllRecords(); +}); + +add_task( + async function test_update_doorhanger_shown_change_non_mergeable_given_name() { + await setStorage(TEST_ADDRESS_1); + await expectSavedAddresses(1); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": "John", + "#family-name": "Doe", + "#street-address": TEST_ADDRESS_1["street-address"], + "#country": TEST_ADDRESS_1.country, + "#email": TEST_ADDRESS_1.email, + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON, 0); + } + ); + + await expectSavedAddresses(2); + await removeAllRecords(); + } +); + +add_task(async function test_update_doorhanger_shown_add_email_field() { + // TEST_ADDRESS_2 doesn't contain email field + await setStorage(TEST_ADDRESS_2); + await expectSavedAddresses(1); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": TEST_ADDRESS_2["given-name"], + "#street-address": TEST_ADDRESS_2["street-address"], + "#country": TEST_ADDRESS_2.country, + "#email": "test@mozilla.org", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON, 0); + } + ); + + const addresses = await expectSavedAddresses(1); + is(addresses[0].email, "test@mozilla.org", "Email field is saved"); + + await removeAllRecords(); +}); + +add_task(async function test_doorhanger_not_shown_when_autofill_untouched() { + await setStorage(TEST_ADDRESS_2); + await expectSavedAddresses(1); + + let onUsed = waitForStorageChangedEvents("notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await openPopupOn(browser, "form #given-name"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill( + browser, + "#given-name", + TEST_ADDRESS_2["given-name"] + ); + + await SpecialPowers.spawn(browser, [], async function () { + let form = content.document.getElementById("form"); + form.querySelector("input[type=submit]").click(); + }); + + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + } + ); + await onUsed; + + const addresses = await expectSavedAddresses(1); + is(addresses[0].timesUsed, 1, "timesUsed field set to 1"); + await removeAllRecords(); +}); + +add_task(async function test_doorhanger_not_shown_when_fill_duplicate() { + await setStorage(TEST_ADDRESS_4); + await expectSavedAddresses(1); + + let onUsed = waitForStorageChangedEvents("notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": TEST_ADDRESS_4["given-name"], + "#family-name": TEST_ADDRESS_4["family-name"], + "#organization": TEST_ADDRESS_4.organization, + "#country": TEST_ADDRESS_4.country, + }, + }); + + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + } + ); + await onUsed; + + const addresses = await expectSavedAddresses(1); + is( + addresses[0]["given-name"], + TEST_ADDRESS_4["given-name"], + "Verify the name field" + ); + is(addresses[0].timesUsed, 1, "timesUsed field set to 1"); + await removeAllRecords(); +}); + +add_task( + async function test_doorhanger_not_shown_when_autofill_then_fill_everything_duplicate() { + await setStorage(TEST_ADDRESS_2, TEST_ADDRESS_3); + await expectSavedAddresses(2); + + let onUsed = waitForStorageChangedEvents("notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await openPopupOn(browser, "form #given-name"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill( + browser, + "#given-name", + TEST_ADDRESS_2["given-name"] + ); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + // Change number to the second credit card number + "#given-name": TEST_ADDRESS_3["given-name"], + "#street-address": TEST_ADDRESS_3["street-address"], + "#postal-code": TEST_ADDRESS_3["postal-code"], + "#country": "", + }, + }); + + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + } + ); + await onUsed; + + await expectSavedAddresses(2); + await removeAllRecords(); + } +); + +add_task( + async function test_doorhanger_shown_when_contain_all_required_fields() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "extensions.formautofill.addresses.capture.requiredFields", + "street-address,postal-code,address-level1,address-level2", + ], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#street-address", + newValues: { + "#street-address": "32 Vassar Street\nMIT Room 32-G524", + "#postal-code": "02139", + "#address-level2": "Cambridge", + "#address-level1": "MA", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON, 0); + } + ); + + await expectSavedAddresses(1); + await SpecialPowers.popPrefEnv(); + } +); + +add_task( + async function test_doorhanger_not_shown_when_contain_required_invalid_fields() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "extensions.formautofill.addresses.capture.requiredFields", + "street-address,postal-code,address-level1,address-level2", + ], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await focusUpdateSubmitForm(browser, { + focusSelector: "#street-address", + newValues: { + "#street-address": "32 Vassar Street\nMIT Room 32-G524", + "#postal-code": "000", // postal-code is invalid + "#address-level2": "Cambridge", + "#address-level1": "MA", + }, + }); + + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + } + ); + } +); + +add_task( + async function test_doorhanger_not_shown_when_not_contain_all_required_fields() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "extensions.formautofill.addresses.capture.requiredFields", + "street-address,postal-code,address-level1,address-level2", + ], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await focusUpdateSubmitForm(browser, { + focusSelector: "#street-address", + newValues: { + "#given-name": "John", + "#family-name": "Doe", + "#postal-code": "02139", + "#address-level2": "Cambridge", + "#address-level1": "MA", + }, + }); + + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + } + ); + } +); diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_invalid_fields.js b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_invalid_fields.js new file mode 100644 index 0000000000..0f0d302684 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_invalid_fields.js @@ -0,0 +1,224 @@ +"use strict"; + +const VALID_ADDRESS = { + "given-name": "John", + "street-address": "32 Vassar Street", + "address-level1": "California", + "address-level2": "Cambridge", + country: "US", +}; + +const INVALID_ADDRESS = { + "address-level1": "ZZ", // Invalid state + organization: "???", // Invalid: only contains punctuation + email: "john.doe@work@mozilla.org", // Invalid email format + tel: "2-800-555-1234", // Invalid: wrong country code + "postal-code": "1234", // Invalid: too short +}; + +async function expectSavedAddresses(expectedCount) { + const addresses = await getAddresses(); + is( + addresses.length, + expectedCount, + `${addresses.length} address in the storage` + ); + return addresses; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.capture.enabled", true], + ["extensions.formautofill.addresses.supported", "on"], + ], + }); +}); + +/** + * Submit a form with both valid and invalid fields, we should only + * save address fields that are valid + */ +add_task(async function test_do_not_save_invalid_fields() { + let addresses = await expectSavedAddresses(0); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async browser => { + const onSavePopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": VALID_ADDRESS["given-name"], + "#family-name": VALID_ADDRESS["family-name"], + "#street-address": VALID_ADDRESS["street-address"], + "#address-level1": VALID_ADDRESS["address-level1"], + "#address-level2": VALID_ADDRESS["address-level2"], + + // Invalid + "#organization": INVALID_ADDRESS.organization, + "#email": INVALID_ADDRESS.email, + "#tel": INVALID_ADDRESS.tel, + "#postal-code": INVALID_ADDRESS["postal-code"], + }, + }); + await onSavePopupShown; + await clickAddressDoorhangerButton(MAIN_BUTTON); + } + ); + + addresses = await expectSavedAddresses(1); + for (const [key, value] of Object.entries(VALID_ADDRESS)) { + Assert.equal(addresses[0][key] ?? "", value, `${key} field is saved`); + } + for (const [key, value] of Object.entries(INVALID_ADDRESS)) { + Assert.notEqual( + addresses[0][key] ?? "", + value, + `${key} field is not saved` + ); + } + await removeAllRecords(); +}); + +/** + * Submit a form with both valid and invalid fields, we should only + * update address fields that are valid + */ +add_task(async function test_do_not_update_invalid_fields() { + await setStorage(VALID_ADDRESS); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async browser => { + const onUpdatePopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": VALID_ADDRESS["given-name"], + "#family-name": "Doe", // To trigger an update + "#street-address": VALID_ADDRESS["street-address"], + + // Invalid + "#address-level1": INVALID_ADDRESS["address-level1"], + "#organization": INVALID_ADDRESS.organization, + "#email": INVALID_ADDRESS.email, + "#tel": INVALID_ADDRESS.tel, + "#postal-code": INVALID_ADDRESS["postal-code"], + }, + }); + await onUpdatePopupShown; + + await clickAddressDoorhangerButton(MAIN_BUTTON); + } + ); + + const addresses = await expectSavedAddresses(1); + + Assert.equal( + addresses[0]["family-name"], + "Doe", + `family-name field is update` + ); + for (const [key, value] of Object.entries(VALID_ADDRESS)) { + Assert.equal(addresses[0][key] ?? "", value, `${key} field is saved`); + } + for (const [key, value] of Object.entries(INVALID_ADDRESS)) { + Assert.notEqual( + addresses[0][key] ?? "", + value, + `${key} field is not saved` + ); + } + await removeAllRecords(); +}); + +/** + * it is possibile that existing records contain invalid fields (Users add those + * fields via preference page). When updating a record with invalid fields, we + * should still keep those invalid fields in the existing records + */ +add_task(async function test_do_not_remove_invalid_fields_of_exising_address() { + const STORED_ADDRESS = { + ...VALID_ADDRESS, + ...INVALID_ADDRESS, + }; + + // address-level1 in US cannot be invalid, remove it from the storage + delete STORED_ADDRESS["address-level1"]; + await setStorage(STORED_ADDRESS); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async browser => { + const onUpdatePopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": VALID_ADDRESS["given-name"], + "#family-name": "Doe", // To trigger an update + "#street-address": VALID_ADDRESS["street-address"], + "#address-level2": VALID_ADDRESS["address-level2"], + }, + }); + await onUpdatePopupShown; + await clickAddressDoorhangerButton(MAIN_BUTTON); + } + ); + + const addresses = await expectSavedAddresses(1); + Assert.equal( + addresses[0]["family-name"], + "Doe", + `family-name field is update` + ); + for (const [key, value] of Object.entries(STORED_ADDRESS)) { + Assert.equal(addresses[0][key] ?? "", value, `${key} field is saved`); + } + await removeAllRecords(); +}); + +/** + * Ensure the edit address doorhanger show the invalid fields of the existing record + */ +add_task(async function test_do_not_show_invalid_fields_in_edit_doorhanger() { + const STORED_ADDRESS = { + ...INVALID_ADDRESS, + ...VALID_ADDRESS, // valid address fields will overwrite invalid address fields + }; + await setStorage(STORED_ADDRESS); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async browser => { + const onUpdatePopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": VALID_ADDRESS["given-name"], + "#family-name": "Doe", // To trigger an update + "#street-address": VALID_ADDRESS["street-address"], + "#address-level2": VALID_ADDRESS["address-level2"], + }, + }); + await onUpdatePopupShown; + + const onEditPopupShown = waitForPopupShown(); + await clickAddressDoorhangerButton(EDIT_ADDRESS_BUTTON); + await onEditPopupShown; + + await clickAddressDoorhangerButton(MAIN_BUTTON); + } + ); + + const addresses = await expectSavedAddresses(1); + Assert.equal( + addresses[0]["family-name"], + "Doe", + `family-name field is update` + ); + for (const [key, value] of Object.entries(STORED_ADDRESS)) { + Assert.equal(addresses[0][key] ?? "", value, `${key} field is saved`); + } + await removeAllRecords(); +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_multiple_tabs.js b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_multiple_tabs.js new file mode 100644 index 0000000000..804a3c6478 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_multiple_tabs.js @@ -0,0 +1,55 @@ +"use strict"; + +async function expectedSavedAddresses(expectedCount) { + const addresses = await getAddresses(); + is( + addresses.length, + expectedCount, + `${addresses.length} address in the storage` + ); + return addresses; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.capture.enabled", true], + ["extensions.formautofill.addresses.supported", "on"], + ], + }); +}); + +add_task(async function test_address_doorhanger_multiple_tabs() { + const URL = ADDRESS_FORM_URL; + + expectedSavedAddresses(0); + + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL); + await showAddressDoorhanger(tab1.linkedBrowser, { + "#given-name": "John", + "#organization": "Mozilla", + "#street-address": "123 Sesame Street", + }); + + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL); + await showAddressDoorhanger(tab2.linkedBrowser, { + "#given-name": "Jane", + "#organization": "Mozilla", + "#street-address": "123 Sesame Street", + }); + + info(`Save an address in the second tab`); + await clickDoorhangerButton(MAIN_BUTTON, 0); + expectedSavedAddresses(1); + + info(`Switch to the first tab and save the address`); + gBrowser.selectedTab = tab1; + let anchor = document.getElementById("autofill-address-notification-icon"); + anchor.click(); + + await clickDoorhangerButton(MAIN_BUTTON, 0); + expectedSavedAddresses(2); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_non_mergeable_fields.js b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_non_mergeable_fields.js new file mode 100644 index 0000000000..4805f5e724 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_non_mergeable_fields.js @@ -0,0 +1,94 @@ +"use strict"; + +async function expectSavedAddresses(expectedCount) { + const addresses = await getAddresses(); + is( + addresses.length, + expectedCount, + `${addresses.length} address in the storage` + ); + return addresses; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.capture.enabled", true], + ["extensions.formautofill.addresses.supported", "on"], + ], + }); +}); + +/** + * Submit a form with both mergeable and non-mergeable fields, we should only + * update address fields that are mergeable + */ +add_task(async function test_do_not_update_non_mergeable_fields() { + const TEST_ADDRESS = { + "address-level1": "New York", + "address-level2": "New York City", + "street-address": "32 Vassar Street 3F", + email: "john.doe@mozilla.com", + "postal-code": "12345-1234", + organization: "MOZILLA", + country: "US", + }; + await setStorage(TEST_ADDRESS); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async browser => { + const onUpdatePopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": "John", + "#family-name": "Doe", + "#address-level1": "NY", // NY == New York + "#address-level2": "New York", // non-mergeable + "#street-address": "32 Vassar Street", // non-mergeable + "#email": "", // non-mergeable + "#postal-code": "12345", // non-mergeable + "#organization": TEST_ADDRESS.organization.toLowerCase(), + "#country": "US", + }, + }); + await onUpdatePopupShown; + + await clickAddressDoorhangerButton(MAIN_BUTTON); + } + ); + + const addresses = await expectSavedAddresses(1); + + Assert.equal(addresses[0]["given-name"], "John", `given-name is added`); + Assert.equal(addresses[0]["family-name"], "Doe", `family-name is added`); + Assert.equal( + addresses[0]["street-address"], + TEST_ADDRESS["street-address"], + `street-address is not updated` + ); + Assert.equal( + addresses[0]["address-level1"], + TEST_ADDRESS["address-level1"], + `address-level1 is not updated` + ); + Assert.equal( + addresses[0]["address-level2"], + TEST_ADDRESS["address-level2"], + `address-level2 is not updated` + ); + Assert.equal(addresses[0].email, TEST_ADDRESS.email, `email is not update`); + Assert.equal( + addresses[0]["postal-code"], + "12345-1234", + `postal-code not is update` + ); + Assert.equal( + addresses[0].organization, + TEST_ADDRESS.organization.toLowerCase(), + `organization is update` + ); + + await removeAllRecords(); +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_not_shown.js b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_not_shown.js new file mode 100644 index 0000000000..b1b8a6b9d2 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_not_shown.js @@ -0,0 +1,97 @@ +"use strict"; + +const DEFAULT_TEST_DOC = `<form id="form"> + <input id="street-addr" autocomplete="street-address"> + <select id="address-level1" autocomplete="address-level1"> + <option value=""></option> + <option value="AL">Alabama</option> + <option value="AK">Alaska</option> + <option value="AP">Armed Forces Pacific</option> + + <option value="ca">california</option> + <option value="AR">US-Arkansas</option> + <option value="US-CA">California</option> + <option value="CA">California</option> + <option value="US-AZ">US_Arizona</option> + <option value="Ariz">Arizonac</option> + </select> + <input id="city" autocomplete="address-level2"> + <input id="country" autocomplete="country"> + <input id="email" autocomplete="email"> + <input id="tel" autocomplete="tel"> + <input id="cc-name" autocomplete="cc-name"> + <input id="cc-number" autocomplete="cc-number"> + <input id="cc-exp-month" autocomplete="cc-exp-month"> + <input id="cc-exp-year" autocomplete="cc-exp-year"> + <select id="cc-type"> + <option value="">Select</option> + <option value="visa">Visa</option> + <option value="mastercard">Master Card</option> + <option value="amex">American Express</option> + </select> + <input id="submit" type="submit"> +</form>`; +const TARGET_ELEMENT_ID = "street-addr"; + +const TESTCASES = [ + { + description: + "Should not trigger address saving if the number of fields is less than 3", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "#street-addr": "331 E. Evelyn Avenue", + "#tel": "1-650-903-0800", + }, + }, + { + description: "Should not trigger the address save doorhanger when pref off", + document: DEFAULT_TEST_DOC, + targetElementId: TARGET_ELEMENT_ID, + formValue: { + "#street-addr": "331 E. Evelyn Avenue", + "#email": "test@mozilla.org", + "#tel": "1-650-903-0800", + }, + prefs: [["extensions.formautofill.addresses.capture.enabled", false]], + }, +]; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.capture.enabled", true], + ["extensions.formautofill.addresses.supported", "on"], + ], + }); +}); + +add_task(async function test_save_doorhanger_not_shown() { + for (const TEST of TESTCASES) { + info(`Test ${TEST.description}`); + if (TEST.prefs) { + await SpecialPowers.pushPrefEnv({ + set: TEST.prefs, + }); + } + + await BrowserTestUtils.withNewTab(EMPTY_URL, async function (browser) { + await SpecialPowers.spawn(browser, [TEST.document], doc => { + content.document.body.innerHTML = doc; + }); + + await SimpleTest.promiseFocus(browser); + + await focusUpdateSubmitForm(browser, { + focusSelector: `#${TEST.targetElementId}`, + newValues: TEST.formValue, + }); + + await ensureNoDoorhanger(browser); + }); + + if (TEST.prefs) { + await SpecialPowers.popPrefEnv(); + } + } +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_state.js b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_state.js new file mode 100644 index 0000000000..a247341fef --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_state.js @@ -0,0 +1,129 @@ +"use strict"; + +async function expectSavedAddresses(expectedAddresses) { + const addresses = await getAddresses(); + is( + addresses.length, + expectedAddresses.length, + `${addresses.length} address in the storage` + ); + + for (let i = 0; i < expectedAddresses.length; i++) { + for (const [key, value] of Object.entries(expectedAddresses[i])) { + is(addresses[i][key] ?? "", value, `field ${key} should be equal`); + } + } + return addresses; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.formautofill.addresses.capture.enabled", true]], + }); +}); + +add_task(async function test_save_doorhanger_state_invalid() { + const DEFAULT = { + "given-name": "John", + "family-name": "Doe", + organization: "Mozilla", + "street-address": "123 Sesame Street", + country: "US", + }; + + const TEST_CASES = [ + { + filled: { "address-level1": "floridaa" }, // typo + expected: { "address-level1": "" }, + }, + { + filled: { "address-level1": "AB" }, // non-exist region code + expected: { "address-level1": "" }, + }, + ]; + + for (const TEST of TEST_CASES) { + await expectSavedAddresses([]); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": DEFAULT["given-name"], + "#family-name": DEFAULT["family-name"], + "#organization": DEFAULT.organization, + "#street-address": DEFAULT["street-address"], + "#address-level1": TEST.filled["address-level1"], + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON, 0); + } + ); + + await expectSavedAddresses([Object.assign(DEFAULT, TEST.expected)]); + await removeAllRecords(); + } +}); + +add_task(async function test_save_doorhanger_state_valid() { + const DEFAULT = { + "given-name": "John", + "family-name": "Doe", + organization: "Mozilla", + "street-address": "123 Sesame Street", + country: "US", + }; + + const TEST_CASES = [ + { + filled: { "address-level1": "ca" }, + expected: { "address-level1": "ca" }, + }, + { + filled: { "address-level1": "CA" }, + expected: { "address-level1": "CA" }, + }, + { + filled: { "address-level1": "california" }, + expected: { "address-level1": "california" }, + }, + { + filled: { "address-level1": "California" }, + expected: { "address-level1": "California" }, + }, + ]; + + for (const TEST of TEST_CASES) { + await expectSavedAddresses([]); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": DEFAULT["given-name"], + "#family-name": DEFAULT["family-name"], + "#organization": DEFAULT.organization, + "#street-address": DEFAULT["street-address"], + "#address-level1": TEST.filled["address-level1"], + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON, 0); + } + ); + + await expectSavedAddresses([Object.assign(DEFAULT, TEST.expected)]); + await removeAllRecords(); + } +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_tel.js b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_tel.js new file mode 100644 index 0000000000..7701816b74 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_tel.js @@ -0,0 +1,119 @@ +"use strict"; + +async function expectSavedAddresses(expectedAddresses) { + const addresses = await getAddresses(); + is( + addresses.length, + expectedAddresses.length, + `${addresses.length} address in the storage` + ); + + for (let i = 0; i < expectedAddresses.length; i++) { + for (const [key, value] of Object.entries(expectedAddresses[i])) { + is(addresses[i][key] ?? "", value, `field ${key} should be equal`); + } + } + return addresses; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.formautofill.addresses.capture.enabled", true]], + }); +}); + +add_task(async function test_save_doorhanger_tel_invalid() { + const EXPECTED = [ + { + "given-name": "John", + "family-name": "Doe", + organization: "Mozilla", + "street-address": "123 Sesame Street", + tel: "", + }, + ]; + + const TEST_CASES = [ + "1234", // length is too short + "1234567890123456", // length is too long + "12345###!!", // contains invalid characters + ]; + + for (const TEST of TEST_CASES) { + await expectSavedAddresses([]); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": "John", + "#family-name": "Doe", + "#organization": "Mozilla", + "#street-address": "123 Sesame Street", + "#tel": TEST, + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON, 0); + } + ); + + await expectSavedAddresses(EXPECTED); + await removeAllRecords(); + } +}); + +add_task(async function test_save_doorhanger_tel_concatenated() { + const EXPECTED = [ + { + "given-name": "John", + "family-name": "Doe", + organization: "Mozilla", + tel: "+15202486621", + }, + ]; + + const MARKUP = `<form id="form"> + <input id="given-name" autocomplete="given-name"> + <input id="family-name" autocomplete="family-name"> + <input id="organization" autocomplete="organization"> + <input id="tel-country-code" autocomplete="tel-country-code"> + <input id="tel-national" autocomplete="tel-national"> + <input type="submit"> + </form>`; + + await expectSavedAddresses([]); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: EMPTY_URL }, + async function (browser) { + await SpecialPowers.spawn(browser, [MARKUP], doc => { + content.document.body.innerHTML = doc; + }); + + let onPopupShown = waitForPopupShown(); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": "John", + "#family-name": "Doe", + "#organization": "Mozilla", + "#tel-country-code": "+1", + "#tel-national": "5202486621", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON, 0); + } + ); + + await expectSavedAddresses(EXPECTED); + await removeAllRecords(); +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_ui.js b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_ui.js new file mode 100644 index 0000000000..9451054de9 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_ui.js @@ -0,0 +1,277 @@ +"use strict"; + +const { FormAutofill } = ChromeUtils.importESModule( + "resource://autofill/FormAutofill.sys.mjs" +); + +async function expectSavedAddresses(expectedCount) { + const addresses = await getAddresses(); + is( + addresses.length, + expectedCount, + `${addresses.length} address in the storage` + ); + return addresses; +} + +function verifyDoorhangerContent(saved, removed = {}) { + const rows = [ + ...getNotification().querySelectorAll(`.address-save-update-row-container`), + ]; + + let texts = rows.reduce((acc, cur) => acc + cur.textContent, ""); + for (const text of Object.values(saved)) { + ok(texts.includes(text), `Show ${text} in the doorhanger`); + texts = texts.replace(text, ""); + } + for (const text of Object.values(removed)) { + ok(texts.includes(text), `Show ${text} in the doorhanger (removed)`); + texts = texts.replace(text, ""); + } + is(texts.trim(), "", `Doorhanger shows all the submitted data`); +} + +function checkVisibility(element) { + return element.checkVisibility({ + checkOpacity: true, + checkVisibilityCSS: true, + }); +} + +function recordToFormSelector(record) { + let obj = {}; + for (const [key, value] of Object.entries(record)) { + obj[`#${key}`] = value; + } + return obj; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.capture.enabled", true], + ["extensions.formautofill.addresses.supported", "on"], + ], + }); +}); + +// Save address doorhanger should show description when users has no saved address +add_task(async function test_save_doorhanger_show_description() { + await expectSavedAddresses(0); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await showAddressDoorhanger(browser); + + const header = AutofillDoorhanger.header(getNotification()); + is(checkVisibility(header), true, "Should always show header"); + + const description = AutofillDoorhanger.description(getNotification()); + is( + checkVisibility(description), + true, + "Should show description when this is the first address saved" + ); + } + ); +}); + +// Save address doorhanger should not show description when users has at least one saved address +add_task(async function test_save_doorhanger_hide_description() { + await setStorage(TEST_ADDRESS_1); + await expectSavedAddresses(1); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await showAddressDoorhanger(browser); + + const header = AutofillDoorhanger.header(getNotification()); + is(checkVisibility(header), true, "Should always show header"); + + const description = AutofillDoorhanger.description(getNotification()); + is( + checkVisibility(description), + false, + "Should not show description when there is at least one saved address" + ); + } + ); + + await removeAllRecords(); +}); + +// Test open edit address popup and then click "learn more" button +add_task(async function test_click_learn_more_button_in_edit_doorhanger() { + await expectSavedAddresses(0); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await showAddressDoorhanger(browser); + + let tabOpenPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => + url.endsWith(AddressSaveDoorhanger.learnMoreURL) + ); + await clickAddressDoorhangerButton( + ADDRESS_MENU_BUTTON, + ADDRESS_MENU_LEARN_MORE + ); + const tab = await tabOpenPromise; + gBrowser.removeTab(tab); + } + ); +}); + +add_task(async function test_click_address_setting_button_in_edit_doorhanger() { + await expectSavedAddresses(0); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await showAddressDoorhanger(browser); + + let tabOpenPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + `about:preferences#${AddressSaveDoorhanger.preferenceURL}` + ); + await clickAddressDoorhangerButton( + ADDRESS_MENU_BUTTON, + ADDRESS_MENU_PREFENCE + ); + const tab = await tabOpenPromise; + gBrowser.removeTab(tab); + } + ); +}); + +add_task(async function test_address_display_in_save_doorhanger() { + await expectSavedAddresses(0); + + const TESTS = [ + { + description: "Test submit a form without email and tel fields", + form: { + "#given-name": "John", + "#family-name": "Doe", + "#organization": "Mozilla", + "#street-address": "123 Sesame Street", + }, + expectedSectionCount: 1, + }, + { + description: "Test submit a form with email field", + form: { + "#given-name": "John", + "#family-name": "Doe", + "#organization": "Mozilla", + "#street-address": "123 Sesame Street", + "#email": "test@mozilla.org", + }, + expectedSectionCount: 2, + }, + { + description: "Test submit a form with tel field", + form: { + "#given-name": "John", + "#family-name": "Doe", + "#organization": "Mozilla", + "#street-address": "123 Sesame Street", + "#tel": "+13453453456", + }, + expectedSectionCount: 2, + }, + ]; + + for (const TEST of TESTS) { + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + info(TEST.description); + await showAddressDoorhanger(browser, TEST.form); + + is( + getNotification().querySelectorAll( + `.address-save-update-row-container` + ).length, + TEST.expectedSectionCount, + `Should have ${TEST.expectedSectionCount} address section` + ); + + // When the form has no country field, doorhanger shows the default region + verifyDoorhangerContent({ + ...TEST.form, + country: FormAutofill.DEFAULT_REGION, + }); + + await clickAddressDoorhangerButton(SECONDARY_BUTTON); + } + ); + } + + await removeAllRecords(); +}); + +add_task(async function test_show_added_text_in_update_doorhanger() { + await setStorage(TEST_ADDRESS_2); + await expectSavedAddresses(1); + + const form = { + ...TEST_ADDRESS_2, + + email: "test@mozilla.org", // Add email field + "given-name": TEST_ADDRESS_2["given-name"] + " Doe", // Append + "street-address": TEST_ADDRESS_2["street-address"] + " 4F", // Append + }; + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await showAddressDoorhanger(browser, recordToFormSelector(form)); + + // When the form has no country field, doorhanger shows the default region + verifyDoorhangerContent({ + ...form, + country: FormAutofill.DEFAULT_REGION, + }); + + await clickAddressDoorhangerButton(SECONDARY_BUTTON); + } + ); + + await removeAllRecords(); +}); + +add_task(async function test_show_removed_text_in_update_doorhanger() { + const SAVED_ADDRESS = { + ...TEST_ADDRESS_2, + organization: "Mozilla", + }; + await setStorage(SAVED_ADDRESS); + await expectSavedAddresses(1); + + // We will ask whether users would like to update "Mozilla" to "mozilla" + const form = { + ...SAVED_ADDRESS, + + organization: SAVED_ADDRESS.organization.toLowerCase(), + }; + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await showAddressDoorhanger(browser, recordToFormSelector(form)); + + // When the form has no country field, doorhanger shows the default region + verifyDoorhangerContent( + { ...form, country: FormAutofill.DEFAULT_REGION }, + { organization: SAVED_ADDRESS.organization } + ); + + await clickAddressDoorhangerButton(SECONDARY_BUTTON); + } + ); + + await removeAllRecords(); +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_unsupported_region.js b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_unsupported_region.js new file mode 100644 index 0000000000..fc3d1c7cf4 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_unsupported_region.js @@ -0,0 +1,88 @@ +"use strict"; + +const { Region } = ChromeUtils.importESModule( + "resource://gre/modules/Region.sys.mjs" +); +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.capture.enabled", true], + ["extensions.formautofill.addresses.supported", "detect"], + ["extensions.formautofill.addresses.supportedCountries", "US,CA"], + ], + }); +}); + +add_task(async function test_save_doorhanger_supported_region() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + const onPopupShown = waitForPopupShown(); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": "John", + "#family-name": "Doe", + "#organization": "Mozilla", + "#street-address": "123 Sesame Street", + "#country": "US", + }, + }); + await onPopupShown; + } + ); +}); + +/** + * Do not display the address capture doorhanger if the country field of the + * submitted form is not on the supported list." + */ +add_task(async function test_save_doorhanger_unsupported_region_from_record() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": "John", + "#family-name": "Doe", + "#organization": "Mozilla", + "#street-address": "123 Sesame Street", + "#country": "DE", + }, + }); + + await ensureNoDoorhanger(browser); + } + ); +}); + +add_task(async function test_save_doorhanger_unsupported_region_from_pref() { + const initialHomeRegion = Region._home; + const initialCurrentRegion = Region._current; + + const region = "FR"; + Region._setCurrentRegion(region); + Region._setHomeRegion(region); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": "John", + "#family-name": "Doe", + "#organization": "Mozilla", + "#street-address": "123 Sesame Street", + }, + }); + + await ensureNoDoorhanger(browser); + } + ); + + Region._setCurrentRegion(initialHomeRegion); + Region._setHomeRegion(initialCurrentRegion); +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_telemetry.js b/browser/extensions/formautofill/test/browser/address/browser_address_telemetry.js new file mode 100644 index 0000000000..acd7a7c364 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_telemetry.js @@ -0,0 +1,741 @@ +"use strict"; + +requestLongerTimeout(3); + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const { AddressTelemetry } = ChromeUtils.importESModule( + "resource://autofill/AutofillTelemetry.sys.mjs" +); + +// Telemetry definitions +const EVENT_CATEGORY = "address"; + +const SCALAR_DETECTED_SECTION_COUNT = + "formautofill.addresses.detected_sections_count"; +const SCALAR_SUBMITTED_SECTION_COUNT = + "formautofill.addresses.submitted_sections_count"; +const SCALAR_AUTOFILL_PROFILE_COUNT = + "formautofill.addresses.autofill_profiles_count"; + +const HISTOGRAM_PROFILE_NUM_USES = "AUTOFILL_PROFILE_NUM_USES"; +const HISTOGRAM_PROFILE_NUM_USES_KEY = "address"; + +// Autofill UI +const MANAGE_DIALOG_URL = MANAGE_ADDRESSES_DIALOG_URL; +const EDIT_DIALOG_URL = EDIT_ADDRESS_DIALOG_URL; +const DIALOG_SIZE = "width=600,height=400"; +const MANAGE_RECORD_SELECTOR = "#addresses"; + +// Test specific definitions +const TEST_PROFILE = TEST_ADDRESS_1; +const TEST_PROFILE_1 = TEST_ADDRESS_1; +const TEST_PROFILE_2 = TEST_ADDRESS_2; +const TEST_PROFILE_3 = TEST_ADDRESS_3; + +const TEST_FOCUS_NAME_FIELD = "given-name"; +const TEST_FOCUS_NAME_FIELD_SELECTOR = "#" + TEST_FOCUS_NAME_FIELD; + +// Used for tests that update address fields after filling +const TEST_NEW_VALUES = { + "#given-name": "Test User", + "#organization": "Sesame Street", + "#street-address": "123 Sesame Street", + "#tel": "1-345-345-3456", +}; + +const TEST_UPDATE_PROFILE_2_VALUES = { + "#email": "profile2@mozilla.org", +}; +const TEST_BASIC_ADDRESS_FORM_URL = ADDRESS_FORM_URL; +const TEST_BASIC_ADDRESS_FORM_WITHOUT_AC_URL = + ADDRESS_FORM_WITHOUT_AUTOCOMPLETE_URL; +// This should be sync with the address fields that appear in TEST_BASIC_ADDRESS_FORM +const TEST_BASIC_ADDRESS_FORM_FIELDS = [ + "street_address", + "address_level1", + "address_level2", + "postal_code", + "country", + "given_name", + "family_name", + "organization", + "email", + "tel", +]; + +function buildFormExtra(list, fields, fieldValue, defaultValue, aExtra = {}) { + let extra = {}; + for (const field of list) { + if (aExtra[field]) { + extra[field] = aExtra[field]; + } else { + extra[field] = fields.includes(field) ? fieldValue : defaultValue; + } + } + return extra; +} + +/** + * Utility function to generate expected value for `address_form` and `address_form_ext` + * telemetry event. + * + * @param {string} method see `methods` in `address_form` event telemetry + * @param {object} defaultExtra default extra object, this will not be overwritten + * @param {object} fields address fields that will be set to `value` param + * @param {string} value value to set for fields list in `fields` argument + * @param {string} defaultValue value to set for address fields that are not listed in `fields` argument` + */ +function formArgs( + method, + defaultExtra, + fields = [], + value = undefined, + defaultValue = null +) { + if (["popup_shown", "filled_modified"].includes(method)) { + return [["address", method, "address_form", undefined, defaultExtra]]; + } + let extra = buildFormExtra( + AddressTelemetry.SUPPORTED_FIELDS_IN_FORM, + fields, + value, + defaultValue, + defaultExtra + ); + + let extraExt = buildFormExtra( + AddressTelemetry.SUPPORTED_FIELDS_IN_FORM_EXT, + fields, + value, + defaultValue, + defaultExtra + ); + + // The order here should sync with AutofillTelemetry. + return [ + ["address", method, "address_form", undefined, extra], + ["address", method, "address_form_ext", undefined, extraExt], + ]; +} + +function getProfiles() { + return getAddresses(); +} + +async function assertTelemetry(expected_content, expected_parent) { + let snapshots; + + info( + `Waiting for ${expected_content?.length ?? 0} content events and ` + + `${expected_parent?.length ?? 0} parent events` + ); + + await TestUtils.waitForCondition( + () => { + snapshots = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + ); + + return ( + (snapshots.parent?.length ?? 0) >= (expected_parent?.length ?? 0) && + (snapshots.content?.length ?? 0) >= (expected_content?.length ?? 0) + ); + }, + "Wait for telemetry to be collected", + 100, + 100 + ); + + info(JSON.stringify(snapshots, null, 2)); + + if (expected_content !== undefined) { + expected_content = expected_content.map( + ([category, method, object, value, extra]) => { + return { category, method, object, value, extra }; + } + ); + + let clear = expected_parent === undefined; + + TelemetryTestUtils.assertEvents( + expected_content, + { + category: EVENT_CATEGORY, + }, + { clear, process: "content" } + ); + } + + if (expected_parent !== undefined) { + expected_parent = expected_parent.map( + ([category, method, object, value, extra]) => { + return { category, method, object, value, extra }; + } + ); + TelemetryTestUtils.assertEvents( + expected_parent, + { + category: EVENT_CATEGORY, + }, + { process: "parent" } + ); + } +} + +function _assertHistogram(snapshot, expectedNonZeroRanges) { + // Compute the actual ranges in the format { range1: value1, range2: value2 }. + let actualNonZeroRanges = {}; + for (let [range, value] of Object.entries(snapshot.values)) { + if (value > 0) { + actualNonZeroRanges[range] = value; + } + } + + // These are stringified to visualize the differences between the values. + Assert.equal( + JSON.stringify(actualNonZeroRanges), + JSON.stringify(expectedNonZeroRanges) + ); +} + +function assertKeyedHistogram(histogramId, key, expected) { + let snapshot = Services.telemetry + .getKeyedHistogramById(histogramId) + .snapshot(); + + if (expected == undefined) { + Assert.deepEqual(snapshot, {}); + } else { + _assertHistogram(snapshot[key], expected); + } +} + +async function openTabAndUseAutofillProfile( + idx, + profile, + { closeTab = true, submitForm = true } = {} +) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_BASIC_ADDRESS_FORM_URL + ); + let browser = tab.linkedBrowser; + + await openPopupOn(browser, "form " + TEST_FOCUS_NAME_FIELD_SELECTOR); + + for (let i = 0; i <= idx; i++) { + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + } + + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill( + browser, + TEST_FOCUS_NAME_FIELD_SELECTOR, + profile[TEST_FOCUS_NAME_FIELD] + ); + await focusUpdateSubmitForm( + browser, + { + focusSelector: TEST_FOCUS_NAME_FIELD_SELECTOR, + newValues: {}, + }, + submitForm + ); + + if (!closeTab) { + return tab; + } + + await BrowserTestUtils.removeTab(tab); + return null; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + [ENABLED_AUTOFILL_ADDRESSES_PREF, true], + [AUTOFILL_ADDRESSES_AVAILABLE_PREF, "on"], + ["extensions.formautofill.addresses.capture.enabled", true], + ], + }); + + Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, true); + + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + + registerCleanupFunction(() => { + Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, false); + }); +}); + +add_task(async function test_popup_opened() { + await setStorage(TEST_PROFILE); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_BASIC_ADDRESS_FORM_URL }, + async function (browser) { + const focusInput = TEST_FOCUS_NAME_FIELD_SELECTOR; + + await openPopupOn(browser, focusInput); + + // Clean up + await closePopup(browser); + } + ); + + const fields = TEST_BASIC_ADDRESS_FORM_FIELDS; + await assertTelemetry([ + ...formArgs("detected", {}, fields, "true", "false"), + ...formArgs("popup_shown", { field_name: TEST_FOCUS_NAME_FIELD }), + ]); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_DETECTED_SECTION_COUNT, + 1, + "There should be 1 section detected." + ); + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_SUBMITTED_SECTION_COUNT, + 1 + ); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_popup_opened_form_without_autocomplete() { + await setStorage(TEST_PROFILE); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_BASIC_ADDRESS_FORM_WITHOUT_AC_URL }, + async function (browser) { + const focusInput = TEST_FOCUS_NAME_FIELD_SELECTOR; + await openPopupOn(browser, focusInput); + await closePopup(browser); + } + ); + + const fields = TEST_BASIC_ADDRESS_FORM_FIELDS; + await assertTelemetry([ + ...formArgs("detected", {}, fields, "0", "false"), + ...formArgs("popup_shown", { field_name: TEST_FOCUS_NAME_FIELD }), + ]); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_DETECTED_SECTION_COUNT, + 1, + "There should be 1 section detected." + ); + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_SUBMITTED_SECTION_COUNT + ); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_submit_autofill_profile_new() { + async function test_per_command( + command, + idx, + useCount = {}, + expectChanged = undefined + ) { + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_BASIC_ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + let onChanged; + if (expectChanged !== undefined) { + onChanged = TestUtils.topicObserved("formautofill-storage-changed"); + } + + await focusUpdateSubmitForm(browser, { + focusSelector: TEST_FOCUS_NAME_FIELD_SELECTOR, + newValues: TEST_NEW_VALUES, + }); + + await onPopupShown; + await clickDoorhangerButton(command, idx); + if (expectChanged !== undefined) { + await onChanged; + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("parent"), + SCALAR_AUTOFILL_PROFILE_COUNT, + expectChanged, + `There should be ${expectChanged} profile(s) stored.` + ); + } + } + ); + + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + useCount + ); + + await removeAllRecords(); + } + + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + Services.telemetry.getKeyedHistogramById(HISTOGRAM_PROFILE_NUM_USES).clear(); + + const fields = TEST_BASIC_ADDRESS_FORM_FIELDS; + let expected_content = [ + ...formArgs("detected", {}, fields, "true", "false"), + ...formArgs("submitted", {}, fields, "user_filled", "unavailable"), + ]; + + await test_per_command(MAIN_BUTTON, undefined, { 1: 1 }, 1); + await assertTelemetry(expected_content, [ + [EVENT_CATEGORY, "show", "capture_doorhanger"], + [EVENT_CATEGORY, "save", "capture_doorhanger"], + ]); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_DETECTED_SECTION_COUNT, + 1, + "There should be 1 sections detected." + ); + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_SUBMITTED_SECTION_COUNT, + 1, + "There should be 1 section submitted." + ); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_submit_autofill_profile_update() { + async function test_per_command( + command, + idx, + useCount = {}, + expectChanged = undefined + ) { + await setStorage(TEST_PROFILE_2); + let profiles = await getProfiles(); + Assert.equal(profiles.length, 1, "1 entry in storage"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_BASIC_ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + let onChanged; + if (expectChanged !== undefined) { + onChanged = TestUtils.topicObserved("formautofill-storage-changed"); + } + + await openPopupOn(browser, TEST_FOCUS_NAME_FIELD_SELECTOR); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + + await waitForAutofill( + browser, + TEST_FOCUS_NAME_FIELD_SELECTOR, + TEST_PROFILE_2[TEST_FOCUS_NAME_FIELD] + ); + await focusUpdateSubmitForm(browser, { + focusSelector: TEST_FOCUS_NAME_FIELD_SELECTOR, + newValues: TEST_UPDATE_PROFILE_2_VALUES, + }); + await onPopupShown; + await clickDoorhangerButton(command, idx); + if (expectChanged !== undefined) { + await onChanged; + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("parent"), + SCALAR_AUTOFILL_PROFILE_COUNT, + expectChanged, + `There should be ${expectChanged} profile(s) stored.` + ); + } + } + ); + + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + useCount + ); + + SpecialPowers.clearUserPref(ENABLED_AUTOFILL_ADDRESSES_PREF); + + await removeAllRecords(); + } + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + Services.telemetry.getKeyedHistogramById(HISTOGRAM_PROFILE_NUM_USES).clear(); + + const fields = TEST_BASIC_ADDRESS_FORM_FIELDS; + let expected_content = [ + ...formArgs("detected", {}, fields, "true", "false"), + ...formArgs("popup_shown", { field_name: TEST_FOCUS_NAME_FIELD }), + ...formArgs( + "filled", + { + given_name: "filled", + street_address: "filled", + country: "filled", + }, + fields, + "not_filled", + "unavailable" + ), + ...formArgs( + "submitted", + { + given_name: "autofilled", + street_address: "autofilled", + country: "autofilled", + email: "user_filled", + }, + fields, + "not_filled", + "unavailable" + ), + ]; + + await test_per_command(MAIN_BUTTON, undefined, { 1: 1 }, 1); + await assertTelemetry(expected_content, [ + [EVENT_CATEGORY, "show", "update_doorhanger"], + [EVENT_CATEGORY, "update", "update_doorhanger"], + ]); + + await test_per_command(SECONDARY_BUTTON, undefined, { 0: 1 }); + await assertTelemetry(expected_content, [ + [EVENT_CATEGORY, "show", "update_doorhanger"], + [EVENT_CATEGORY, "cancel", "update_doorhanger"], + ]); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_removingAutofillProfilesViaKeyboardDelete() { + await setStorage(TEST_PROFILE); + + let win = window.openDialog(MANAGE_DIALOG_URL, null, DIALOG_SIZE); + await waitForFocusAndFormReady(win); + + let selRecords = win.document.querySelector(MANAGE_RECORD_SELECTOR); + Assert.equal(selRecords.length, 1, "One entry"); + + EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win); + EventUtils.synthesizeKey("VK_DELETE", {}, win); + await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved"); + Assert.equal(selRecords.length, 0, "No entry left"); + + win.close(); + + await assertTelemetry(undefined, [ + [EVENT_CATEGORY, "show", "manage"], + [EVENT_CATEGORY, "delete", "manage"], + ]); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_saveAutofillProfile() { + Services.telemetry.clearEvents(); + + await testDialog(EDIT_DIALOG_URL, win => { + // TODP: Default to US because the layout will be different + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["given-name"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["family-name"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["street-address"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["address-level2"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["postal-code"], {}, win); + info("saving one entry"); + win.document.querySelector("#save").click(); + }); + + await assertTelemetry(undefined, [[EVENT_CATEGORY, "add", "manage"]]); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_editAutofillProfile() { + Services.telemetry.clearEvents(); + + await setStorage(TEST_PROFILE); + + let profiles = await getProfiles(); + Assert.equal(profiles.length, 1, "1 entry in storage"); + await testDialog( + EDIT_DIALOG_URL, + win => { + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_RIGHT", {}, win); + EventUtils.synthesizeKey("test", {}, win); + win.document.querySelector("#save").click(); + }, + { + record: profiles[0], + } + ); + + await assertTelemetry(undefined, [ + [EVENT_CATEGORY, "show_entry", "manage"], + [EVENT_CATEGORY, "edit", "manage"], + ]); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_histogram() { + Services.telemetry.getKeyedHistogramById(HISTOGRAM_PROFILE_NUM_USES).clear(); + + await setStorage(TEST_PROFILE_1, TEST_PROFILE_2, TEST_PROFILE_3); + let profiles = await getProfiles(); + Assert.equal(profiles.length, 3, "3 entry in storage"); + + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 3 } + ); + + await openTabAndUseAutofillProfile(0, TEST_PROFILE_1); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 2, 1: 1 } + ); + + await openTabAndUseAutofillProfile(1, TEST_PROFILE_2); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 1, 1: 2 } + ); + + await openTabAndUseAutofillProfile(0, TEST_PROFILE_2); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 1, 1: 1, 2: 1 } + ); + + await openTabAndUseAutofillProfile(1, TEST_PROFILE_1); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 1, 2: 2 } + ); + + await openTabAndUseAutofillProfile(2, TEST_PROFILE_3); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 1: 1, 2: 2 } + ); + + await removeAllRecords(); + + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + undefined + ); + + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_click_doorhanger_menuitems() { + const TESTS = [ + { + button: ADDRESS_MENU_BUTTON, + menuItem: ADDRESS_MENU_LEARN_MORE, + expectedEvt: "learn_more", + }, + { + button: ADDRESS_MENU_BUTTON, + menuItem: ADDRESS_MENU_PREFENCE, + expectedEvt: "pref", + }, + ]; + for (const TEST of TESTS) { + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_BASIC_ADDRESS_FORM_URL }, + async function (browser) { + await showAddressDoorhanger(browser); + + const tabOpenPromise = BrowserTestUtils.waitForNewTab(gBrowser); + await clickAddressDoorhangerButton(TEST.button, TEST.menuItem); + gBrowser.removeTab(await tabOpenPromise); + } + ); + + await assertTelemetry(undefined, [ + [EVENT_CATEGORY, "show", "capture_doorhanger"], + [EVENT_CATEGORY, TEST.expectedEvt, "capture_doorhanger"], + ]); + + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + } +}); + +add_task(async function test_show_edit_doorhanger() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_BASIC_ADDRESS_FORM_URL }, + async function (browser) { + await showAddressDoorhanger(browser); + + const onEditPopupShown = waitForPopupShown(); + await clickAddressDoorhangerButton(EDIT_ADDRESS_BUTTON); + await onEditPopupShown; + + await clickAddressDoorhangerButton(MAIN_BUTTON); + } + ); + + await assertTelemetry(undefined, [ + [EVENT_CATEGORY, "show", "capture_doorhanger"], + [EVENT_CATEGORY, "show", "edit_doorhanger"], + [EVENT_CATEGORY, "save", "edit_doorhanger"], + ]); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_clear_autofill_profile_autofill() { + // Address does not have clear pref. Keep the test so we know we should implement + // the test if we support clearing address via autocomplete. + Assert.ok(true); +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_display.js b/browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_display.js new file mode 100644 index 0000000000..9f82824746 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_display.js @@ -0,0 +1,216 @@ +"use strict"; + +async function expectSavedAddresses(expectedCount) { + const addresses = await getAddresses(); + is( + addresses.length, + expectedCount, + `${addresses.length} address in the storage` + ); + return addresses; +} + +function recordToFormSelector(record) { + let obj = {}; + for (const [key, value] of Object.entries(record)) { + obj[`#${key}`] = value; + } + return obj; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.capture.enabled", true], + ["extensions.formautofill.addresses.supported", "on"], + ], + }); +}); + +// Test different scenarios when we change something in the edit address dorhanger +add_task(async function test_save_edited_fields() { + await expectSavedAddresses(0); + + const initRecord = { + "given-name": "John", + "family-name": "Doe", + organization: "Mozilla", + "street-address": "123 Sesame Street", + tel: "+13453453456", + }; + + const TESTS = [ + { + description: "adding the email field", + editedFields: { + email: "test@mozilla.org", + }, + }, + { + description: "changing the given-name field", + editedFields: { + name: "Jane", + }, + }, + { + description: "appending the street-address field", + editedFields: { + "street-address": initRecord["street-address"] + " 4F", + }, + }, + { + description: "removing some fields", + editedFields: { + name: "", + tel: "", + }, + }, + { + description: "doing all kinds of stuff", + editedFields: { + organization: initRecord.organization.toLowerCase(), + "address-level1": "California", + tel: "", + "street-address": initRecord["street-address"] + " Apt.6", + name: "Jane Doe", + }, + }, + ]; + + for (const TEST of TESTS) { + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + info(`Test ${TEST.description}`); + + const onSavePopupShown = waitForPopupShown(); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: recordToFormSelector(initRecord), + }); + + await onSavePopupShown; + + const onEditPopupShown = waitForPopupShown(); + await clickAddressDoorhangerButton(EDIT_ADDRESS_BUTTON); + await onEditPopupShown; + fillEditDoorhanger(TEST.editedFields); + + await clickAddressDoorhangerButton(MAIN_BUTTON); + } + ); + + const expectedRecord = normalizeAddressFields({ + ...initRecord, + ...TEST.editedFields, + }); + + const addresses = await expectSavedAddresses(1); + for (const [key, value] of Object.entries(expectedRecord)) { + is(addresses[0][key] ?? "", value, `${key} field is saved`); + } + + await removeAllRecords(); + } +}); + +// This test tests edit doorhanger "save" & "cancel" buttons work correctly +// when the edit doorhanger is triggered in an save doorhanger +add_task(async function test_edit_doorhanger_triggered_by_save_doorhanger() { + for (const CLICKED_BUTTON of [MAIN_BUTTON, SECONDARY_BUTTON]) { + await expectSavedAddresses(0); + + const initRecord = { + "given-name": "John", + "family-name": "Doe", + organization: "Mozilla", + "street-address": "123 Sesame Street", + tel: "+13453453456", + }; + + const editRecord = { + email: "test@mozilla.org", + }; + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async browser => { + info( + `Test clicking ${CLICKED_BUTTON == MAIN_BUTTON ? "save" : "cancel"}` + ); + const onSavePopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: recordToFormSelector(initRecord), + }); + await onSavePopupShown; + + const onEditPopupShown = waitForPopupShown(); + await clickAddressDoorhangerButton(EDIT_ADDRESS_BUTTON); + await onEditPopupShown; + fillEditDoorhanger(editRecord); + + await clickAddressDoorhangerButton(CLICKED_BUTTON); + } + ); + + await expectSavedAddresses(CLICKED_BUTTON == MAIN_BUTTON ? 1 : 0); + await removeAllRecords(); + } +}); + +// This test tests edit doorhanger "save" & "cancel" buttons work correctly +// when the edit doorhanger is triggered in an update doorhnager +add_task(async function test_edit_doorhanger_triggered_by_update_doorhanger() { + for (const CLICKED_BUTTON of [MAIN_BUTTON, SECONDARY_BUTTON]) { + // TEST_ADDRESS_2 doesn't contain email field + await setStorage(TEST_ADDRESS_2); + await expectSavedAddresses(1); + + const initRecord = { + "given-name": TEST_ADDRESS_2["given-name"], + "street-address": TEST_ADDRESS_2["street-address"], + country: TEST_ADDRESS_2.country, + email: "test@mozilla.org", + }; + + const editRecord = { + email: "test@mozilla.org", + }; + + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async browser => { + info( + `Test clicking ${CLICKED_BUTTON == MAIN_BUTTON ? "save" : "cancel"}` + ); + const onUpdatePopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: recordToFormSelector(initRecord), + }); + await onUpdatePopupShown; + + const onEditPopupShown = waitForPopupShown(); + await clickAddressDoorhangerButton(EDIT_ADDRESS_BUTTON); + await onEditPopupShown; + fillEditDoorhanger(editRecord); + + await clickAddressDoorhangerButton(CLICKED_BUTTON); + } + ); + + const addresses = await expectSavedAddresses(1); + const expectedRecord = + CLICKED_BUTTON == MAIN_BUTTON + ? { ...initRecord, ...editRecord } + : TEST_ADDRESS_2; + + for (const [key, value] of Object.entries(expectedRecord)) { + is(addresses[0][key] ?? "", value, `${key} field is saved`); + } + + await removeAllRecords(); + } +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_display_state.js b/browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_display_state.js new file mode 100644 index 0000000000..1d8933ad31 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_display_state.js @@ -0,0 +1,77 @@ +"use strict"; +requestLongerTimeout(2); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.formautofill.addresses.capture.enabled", true]], + }); +}); + +add_task(async function test_edit_doorhanger_display_state() { + const DEFAULT = { + "given-name": "Test User", + organization: "Mozilla", + "street-address": "123 Sesame Street", + country: "US", + }; + + const TEST_CASES = [ + { + filled: { "address-level1": "floridaa" }, // typo + expected: { label: "" }, + }, + { + filled: { "address-level1": "AB" }, // non-exist region code + expected: { label: "" }, + }, + { + filled: { "address-level1": "CA" }, + expected: { label: "CA" }, + }, + { + filled: { "address-level1": "fl" }, + expected: { label: "FL" }, + }, + { + filled: { "address-level1": "New York" }, + expected: { label: "NY" }, + }, + { + filled: { "address-level1": "Washington" }, + expected: { label: "WA" }, + }, + ]; + + for (const TEST of TEST_CASES) { + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + const onSavePopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: { + "#given-name": DEFAULT["given-name"], + "#organization": DEFAULT.organization, + "#street-address": DEFAULT["street-address"], + "#address-level1": TEST.filled["address-level1"], + }, + }); + await onSavePopupShown; + + const onEditPopupShown = waitForPopupShown(); + await clickAddressDoorhangerButton(EDIT_ADDRESS_BUTTON); + await onEditPopupShown; + + const notification = getNotification(); + const id = AddressEditDoorhanger.getInputId("address-level1"); + const element = notification.querySelector(`#${id}`); + + is( + element.label, + TEST.expected.label, + "Edit address doorhanger shows the expected address-level1 select option" + ); + } + ); + } +}); diff --git a/browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_save_edited_fields.js b/browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_save_edited_fields.js new file mode 100644 index 0000000000..d146a3b722 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_save_edited_fields.js @@ -0,0 +1,114 @@ +"use strict"; + +const SUBMIT_RECORD = { + "given-name": "John", + "family-name": "Doe", + organization: "Mozilla", + "street-address": "123 Sesame Street", + tel: "+13453453456", +}; + +const TEST_CASE = [ + { + description: "adding the email field", + editedFields: { + email: "test@mozilla.org", + }, + }, + { + description: "changing the given-name field", + editedFields: { + name: "Jane", + }, + }, + { + description: "appending the street-address field", + editedFields: { + "street-address": SUBMIT_RECORD["street-address"] + " 4F", + }, + }, + { + description: "removing some fields", + editedFields: { + name: "", + tel: "", + }, + }, + { + description: "doing all kinds of stuff", + editedFields: { + organization: SUBMIT_RECORD.organization.toLowerCase(), + "address-level1": "California", + tel: "", + "street-address": SUBMIT_RECORD["street-address"] + " Apt.6", + name: "Jane Doe", + }, + }, +]; + +async function expectSavedAddresses(expectedCount) { + const addresses = await getAddresses(); + is( + addresses.length, + expectedCount, + `${addresses.length} address in the storage` + ); + return addresses; +} + +function recordToFormSelector(record) { + let obj = {}; + for (const [key, value] of Object.entries(record)) { + obj[`#${key}`] = value; + } + return obj; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.capture.enabled", true], + ["extensions.formautofill.addresses.supported", "on"], + ], + }); +}); + +// Test different scenarios when we change something in the edit address dorhanger +add_task(async function test_save_edited_fields() { + await expectSavedAddresses(0); + + for (const TEST of TEST_CASE) { + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_FORM_URL }, + async function (browser) { + info(`Test ${TEST.description}`); + + const onSavePopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#given-name", + newValues: recordToFormSelector(SUBMIT_RECORD), + }); + await onSavePopupShown; + + const onEditPopupShown = waitForPopupShown(); + await clickAddressDoorhangerButton(EDIT_ADDRESS_BUTTON); + await onEditPopupShown; + + fillEditDoorhanger(TEST.editedFields); + await clickAddressDoorhangerButton(MAIN_BUTTON); + } + ); + + const expectedRecord = normalizeAddressFields({ + ...SUBMIT_RECORD, + ...TEST.editedFields, + }); + + const addresses = await expectSavedAddresses(1); + for (const [key, value] of Object.entries(expectedRecord)) { + is(addresses[0][key] ?? "", value, `${key} field is saved`); + } + + await removeAllRecords(); + } +}); diff --git a/browser/extensions/formautofill/test/browser/address/head_address.js b/browser/extensions/formautofill/test/browser/address/head_address.js new file mode 100644 index 0000000000..42196e8422 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/head_address.js @@ -0,0 +1 @@ +/* import-globals-from ../head.js */ |