summaryrefslogtreecommitdiffstats
path: root/browser/extensions/formautofill/test/browser
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/formautofill/test/browser')
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser.toml47
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_autofill_nimbus.js70
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_capture_form_removal.js124
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_capture_page_navigation.js125
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_confirmation_popup.js125
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_display.js336
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_invalid_fields.js224
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_multiple_tabs.js55
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_non_mergeable_fields.js94
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_not_shown.js97
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_state.js129
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_tel.js119
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_ui.js277
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_unsupported_region.js88
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_telemetry.js741
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_display.js216
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_display_state.js77
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_save_edited_fields.js114
-rw-r--r--browser/extensions/formautofill/test/browser/address/head_address.js1
-rw-r--r--browser/extensions/formautofill/test/browser/browser.toml52
-rw-r--r--browser/extensions/formautofill/test/browser/browser_active_window_navigation.js365
-rw-r--r--browser/extensions/formautofill/test/browser/browser_autocomplete_footer.js131
-rw-r--r--browser/extensions/formautofill/test/browser/browser_autocomplete_marked_back_forward.js66
-rw-r--r--browser/extensions/formautofill/test/browser/browser_autocomplete_marked_detached_tab.js58
-rw-r--r--browser/extensions/formautofill/test/browser/browser_autofill_address_select.js89
-rw-r--r--browser/extensions/formautofill/test/browser/browser_autofill_duplicate_fields.js95
-rw-r--r--browser/extensions/formautofill/test/browser/browser_check_installed.js12
-rw-r--r--browser/extensions/formautofill/test/browser/browser_dropdown_layout.js53
-rw-r--r--browser/extensions/formautofill/test/browser/browser_editAddressDialog.js690
-rw-r--r--browser/extensions/formautofill/test/browser/browser_fathom_cc.js204
-rw-r--r--browser/extensions/formautofill/test/browser/browser_manageAddressesDialog.js105
-rw-r--r--browser/extensions/formautofill/test/browser/browser_privacyPreferences.js439
-rw-r--r--browser/extensions/formautofill/test/browser/browser_remoteiframe.js129
-rw-r--r--browser/extensions/formautofill/test/browser/browser_submission_in_private_mode.js37
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser.toml121
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js127
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_capture_form_removal.js119
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_capture_page_navigation.js92
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js170
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js311
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js198
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js103
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_logo.js238
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_not_shown.js92
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_sync.js117
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js97
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_fill_cancel_login.js37
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics.js164
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_autofill_name.js215
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_cc_type.js77
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_autodetect_type.js104
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_normalized.js109
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js1170
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_editCreditCardDialog.js422
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js145
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_manageCreditCardsDialog.js290
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/head_cc.js1
-rw-r--r--browser/extensions/formautofill/test/browser/empty.html8
-rwxr-xr-xbrowser/extensions/formautofill/test/browser/fathom/test-setup.sh39
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/1.svg3
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/10.svg1
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/11.pngbin0 -> 4968 bytes
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/12.gifbin0 -> 37 bytes
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/13.svg16
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/14.svg14
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/15.svg1
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/16.svg11
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/17.binbin0 -> 9594 bytes
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/18.svg1
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/2.svg8
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/3.svg1
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/4.svg6
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/5.svg6
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/6.svg8
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/7.woff2bin0 -> 15480 bytes
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/8.woff2bin0 -> 15784 bytes
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/9.woff2bin0 -> 15908 bytes
-rw-r--r--browser/extensions/formautofill/test/browser/fathom/testing/sample.html20
-rw-r--r--browser/extensions/formautofill/test/browser/focus-leak/browser.toml13
-rw-r--r--browser/extensions/formautofill/test/browser/focus-leak/browser_iframe_typecontent_input_focus.js56
-rw-r--r--browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus.xhtml7
-rw-r--r--browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus_frame.html6
-rw-r--r--browser/extensions/formautofill/test/browser/head.js1257
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser.toml39
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_form.js83
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_inputs.js111
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_basic.js78
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_capture_name.js149
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_cc_exp.js56
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_de_fields.js32
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_fr_fields.js24
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_ignore_unfocusable_fields.js159
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_label_rules.js58
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_multiple_section.js118
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_parse_address_fields.js55
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_parse_creditcard_expiry_fields.js198
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_parse_name_fields.js86
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_parse_street_address_fields.js148
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_section_validation_address.js79
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/browser_sections_by_name.js318
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser.toml40
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_BestBuy.js82
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CDW.js71
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CostCo.js194
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_DirectAsda.js25
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Ebay.js25
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Euronics.js54
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_GlobalDirectAsda.js24
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_HomeDepot.js64
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lufthansa.js28
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lush.js31
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Macys.js40
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_NewEgg.js109
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_OfficeDepot.js83
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_QVC.js96
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Sears.js81
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Staples.js78
-rw-r--r--browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Walmart.js93
118 files changed, 14594 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 */
diff --git a/browser/extensions/formautofill/test/browser/browser.toml b/browser/extensions/formautofill/test/browser/browser.toml
new file mode 100644
index 0000000000..2c9a995e67
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser.toml
@@ -0,0 +1,52 @@
+[DEFAULT]
+head = "head.js"
+support-files = [
+ "./fathom/**",
+ "../fixtures/autocomplete_basic.html",
+ "../fixtures/autocomplete_iframe.html",
+ "../fixtures/autocomplete_simple_basic.html",
+ "../fixtures/page_navigation.html",
+ "./empty.html",
+]
+
+["browser_active_window_navigation.js"]
+
+["browser_autocomplete_footer.js"]
+skip-if = ["verify"]
+
+["browser_autocomplete_marked_back_forward.js"]
+
+["browser_autocomplete_marked_detached_tab.js"]
+skip-if = [
+ "verify && os == 'win'",
+ "os == 'mac'",
+]
+
+["browser_autofill_address_select.js"]
+
+["browser_autofill_duplicate_fields.js"]
+
+["browser_check_installed.js"]
+
+["browser_dropdown_layout.js"]
+
+["browser_editAddressDialog.js"]
+skip-if = [
+ "verify",
+ "win11_2009", # Bug 1797751
+]
+
+["browser_fathom_cc.js"]
+
+["browser_manageAddressesDialog.js"]
+
+["browser_privacyPreferences.js"]
+skip-if = [
+ "os == 'mac'", # perma-fail see Bug 1600059
+ "os == 'linux'", # perma-fail see Bug 1600059
+ "os == 'win'", # perma-fail see Bug 1600059
+]
+
+["browser_remoteiframe.js"]
+
+["browser_submission_in_private_mode.js"]
diff --git a/browser/extensions/formautofill/test/browser/browser_active_window_navigation.js b/browser/extensions/formautofill/test/browser/browser_active_window_navigation.js
new file mode 100644
index 0000000000..17cda3137d
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_active_window_navigation.js
@@ -0,0 +1,365 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+const ADDRESS_VALUES = {
+ "#postal-code": "02139",
+ "#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],
+ [
+ "extensions.formautofill.addresses.capture.requiredFields",
+ "street-address,postal-code,organization",
+ ],
+ ],
+ });
+});
+
+/**
+ * Tests that formautofill fields are not captured (doorhanger is not shown)
+ * when the page navigation happens in a same-origin iframe and
+ * the active window is the parent window
+ */
+add_task(
+ async function test_active_parent_window_not_effected_when_same_origin_iframe_window_is_navigated() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: ADDRESS_FORM_URL,
+ },
+ async function (browser) {
+ info("Update address fields");
+ await focusUpdateSubmitForm(
+ browser,
+ {
+ focusSelector: "#street-address",
+ newValues: ADDRESS_VALUES,
+ },
+ false // We don't submit the form
+ );
+
+ info("Load same-origin iframe and infer page navigation");
+ await SpecialPowers.spawn(browser, [], async () => {
+ let doc = content.document;
+
+ let iframe = doc.createElement("iframe");
+ const iframeLoadPromise = new Promise(
+ resolve => (iframe.onload = resolve)
+ );
+ // same-origin iframe
+ iframe.setAttribute(
+ "src",
+ "https://example.org/browser/browser/extensions/formautofill/test/fixtures/page_navigation.html"
+ );
+ doc.body.appendChild(iframe);
+
+ await iframeLoadPromise;
+
+ // We can directly access the button because the iframe is of same origin
+ iframe.contentDocument.getElementById("windowLocationBtn").click();
+ });
+
+ info("Ensure address doorhanger not shown");
+ await ensureNoDoorhanger(browser);
+ }
+ );
+ }
+);
+
+/**
+ * Tests that formautofill fields are not captured (doorhanger is not shown)
+ * when the page navigation happens in a cross-origin iframe and
+ * the active window is the parent window
+ */
+add_task(
+ async function test_active_parent_window_not_effected_when_cross_origin_iframe_window_is_navigated() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: ADDRESS_FORM_URL,
+ },
+ async function (browser) {
+ info("Update address fields");
+ await focusUpdateSubmitForm(
+ browser,
+ {
+ focusSelector: "#street-address",
+ newValues: ADDRESS_VALUES,
+ },
+ false // We don't submit the form
+ );
+
+ info("Load cross-origin iframe");
+ await SpecialPowers.spawn(browser, [], async () => {
+ let doc = content.document;
+
+ let iframe = doc.createElement("iframe");
+ const iframeLoadPromise = new Promise(
+ resolve => (iframe.onload = resolve)
+ );
+ // cross-origin iframe
+ iframe.setAttribute(
+ "src",
+ "https://example.com/browser/browser/extensions/formautofill/test/fixtures/page_navigation.html"
+ );
+ doc.body.appendChild(iframe);
+
+ await iframeLoadPromise;
+ });
+
+ let iframeBC = browser.browsingContext.children[0];
+
+ info("Infer page navigation in cross-origin iframe window");
+ await SpecialPowers.spawn(iframeBC, [], async () => {
+ content.document.getElementById("windowLocationBtn").click();
+ });
+
+ info("Ensure address doorhanger not shown");
+ await ensureNoDoorhanger(browser);
+ }
+ );
+ }
+);
+
+/**
+ * Tests that formautofill fields are captured (doorhanger is shown)
+ * when active formautofill fields are in a same-origin iframe and
+ * the parent window is navigated
+ */
+add_task(
+ async function test_active_same_origin_window_is_effected_when_parent_window_is_navigated() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "https://example.org/browser/browser/extensions/formautofill/test/fixtures/page_navigation.html",
+ },
+ async function (browser) {
+ const onPopupShown = waitForPopupShown();
+
+ info("Load same-origin iframe with address form");
+ await SpecialPowers.spawn(browser, [], async () => {
+ let doc = content.document;
+
+ let iframe = doc.createElement("iframe");
+ const iframeLoadPromise = new Promise(
+ resolve => (iframe.onload = resolve)
+ );
+ // same-origin iframe
+ iframe.setAttribute(
+ "src",
+ "https://example.org/browser/browser/extensions/formautofill/test/fixtures/autocomplete_address_basic.html"
+ );
+ doc.body.appendChild(iframe);
+
+ await iframeLoadPromise;
+ });
+
+ let iframeBC = browser.browsingContext.children[0];
+
+ info("Update address fields");
+ await focusUpdateSubmitForm(
+ iframeBC,
+ {
+ focusSelector: "#street-address",
+ newValues: ADDRESS_VALUES,
+ },
+ false // We don't submit the form
+ );
+
+ info("Infer page navigation in parent window");
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document.getElementById("windowLocationBtn").click();
+ });
+
+ info("Wait for address doorhanger");
+ await onPopupShown;
+
+ ok(true, "Address doorhanger is shown");
+ }
+ );
+ }
+);
+
+/**
+ * Tests that formautofill fields are not captured (doorhanger is not shown)
+ * when the active formautofill fields are in a cross-origin iframe and
+ * the parent window is navigated
+ */
+add_task(
+ async function test_active_cross_origin_window_not_effected_when_parent_window_is_navigated() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "https://example.org/browser/browser/extensions/formautofill/test/fixtures/page_navigation.html",
+ },
+ async function (browser) {
+ info("Load cross-origin iframe with address form");
+ await SpecialPowers.spawn(browser, [], async () => {
+ let doc = content.document;
+
+ let iframe = doc.createElement("iframe");
+ const iframeLoadPromise = new Promise(
+ resolve => (iframe.onload = resolve)
+ );
+ // cross-origin iframe
+ iframe.setAttribute(
+ "src",
+ "https://example.com/browser/browser/extensions/formautofill/test/fixtures/autocomplete_address_basic.html"
+ );
+ doc.body.appendChild(iframe);
+
+ await iframeLoadPromise;
+ });
+
+ let iframeBC = browser.browsingContext.children[0];
+
+ info("Update address fields");
+ await focusUpdateSubmitForm(
+ iframeBC,
+ {
+ focusSelector: "#street-address",
+ newValues: ADDRESS_VALUES,
+ },
+ false // We don't submit the form
+ );
+
+ info("Infer page navigation in parent window");
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document.getElementById("windowLocationBtn").click();
+ });
+
+ info("Ensure address doorhanger not shown");
+ await ensureNoDoorhanger;
+
+ ok(true, "Address doorhanger is not shown");
+ }
+ );
+ }
+);
+
+/**
+ * Tests that formautofill fields are captured (doorhanger is shown)
+ * when active formautofill fields are in a same-origin iframe and
+ * the iframe's window is navigated
+ */
+add_task(async function test_active_same_origin_iframe_window_is_navigated() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: EMPTY_URL,
+ },
+ async function (browser) {
+ const onPopupShown = waitForPopupShown();
+
+ info(
+ "Load same-origin iframe with formautofill fields and page navigation button"
+ );
+ await SpecialPowers.spawn(browser, [], async () => {
+ let doc = content.document;
+
+ let iframe = doc.createElement("iframe");
+ const iframeLoadPromise = new Promise(
+ resolve => (iframe.onload = resolve)
+ );
+ // same-origin iframe
+ iframe.setAttribute(
+ "src",
+ "https://example.org/browser/browser/extensions/formautofill/test/fixtures/capture_address_on_page_navigation.html"
+ );
+ doc.body.appendChild(iframe);
+
+ await iframeLoadPromise;
+ });
+
+ let iframeBC = browser.browsingContext.children[0];
+
+ info("Update address fields");
+ await focusUpdateSubmitForm(
+ iframeBC,
+ {
+ focusSelector: "#street-address",
+ newValues: ADDRESS_VALUES,
+ },
+ false // We don't submit the form
+ );
+
+ info("Infer page navigation in same-origin iframe's window");
+ await SpecialPowers.spawn(iframeBC, [], async () => {
+ content.document.getElementById("windowLocation").click();
+ });
+
+ info("Wait for address doorhanger");
+ await onPopupShown;
+
+ ok(true, "Address doorhanger is shown");
+ }
+ );
+});
+
+// /**
+// * Tests that formautofill fields are captured (doorhanger is shown)
+// * when active formautofill fields are in a cross-origin iframe and
+// * the iframe's window is navigated
+// */
+add_task(async function test_active_cross_origin_iframe_window_is_navigated() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: EMPTY_URL,
+ },
+ async function (browser) {
+ const onPopupShown = waitForPopupShown();
+
+ info(
+ "Load same-origin iframe with formautofill fields and page navigation button"
+ );
+ await SpecialPowers.spawn(browser, [], async () => {
+ let doc = content.document;
+
+ let iframe = doc.createElement("iframe");
+ const iframeLoadPromise = new Promise(
+ resolve => (iframe.onload = resolve)
+ );
+ // cross-origin iframe
+ iframe.setAttribute(
+ "src",
+ "https://example.com/browser/browser/extensions/formautofill/test/fixtures/capture_address_on_page_navigation.html"
+ );
+ doc.body.appendChild(iframe);
+
+ await iframeLoadPromise;
+ });
+
+ let iframeBC = browser.browsingContext.children[0];
+
+ info("Update address fields");
+ await focusUpdateSubmitForm(
+ iframeBC,
+ {
+ focusSelector: "#street-address",
+ newValues: ADDRESS_VALUES,
+ },
+ false // We don't submit the form
+ );
+
+ info("Infer page navigation in cross-origin iframe's window");
+ await SpecialPowers.spawn(iframeBC, [], async () => {
+ content.document.getElementById("windowLocation").click();
+ });
+
+ info("Wait for address doorhanger");
+ await onPopupShown;
+
+ ok(true, "Address doorhanger is shown");
+ }
+ );
+});
diff --git a/browser/extensions/formautofill/test/browser/browser_autocomplete_footer.js b/browser/extensions/formautofill/test/browser/browser_autocomplete_footer.js
new file mode 100644
index 0000000000..fde34a0a32
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_autocomplete_footer.js
@@ -0,0 +1,131 @@
+"use strict";
+
+const URL = BASE_URL + "autocomplete_basic.html";
+
+add_setup(async function setup_storage() {
+ await setStorage(
+ TEST_ADDRESS_2,
+ TEST_ADDRESS_3,
+ TEST_ADDRESS_4,
+ TEST_ADDRESS_5
+ );
+});
+
+add_task(async function test_press_enter_on_footer() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: URL },
+ async function (browser) {
+ const {
+ autoCompletePopup: { richlistbox: itemsBox },
+ } = browser;
+
+ await openPopupOn(browser, "#organization");
+ // Navigate to the footer and press enter.
+ const listItemElems = itemsBox.querySelectorAll(
+ ".autocomplete-richlistitem"
+ );
+ const prefTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ PRIVACY_PREF_URL,
+ true
+ );
+ for (let i = 0; i < listItemElems.length; i++) {
+ if (!listItemElems[i].collapsed) {
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ }
+ }
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ info(`expecting tab: about:preferences#privacy opened`);
+ const prefTab = await prefTabPromise;
+ info(`expecting tab: about:preferences#privacy removed`);
+ BrowserTestUtils.removeTab(prefTab);
+ ok(
+ true,
+ "Tab: preferences#privacy was successfully opened by pressing enter on the footer"
+ );
+
+ await closePopup(browser);
+ }
+ );
+});
+
+add_task(async function test_click_on_footer() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: URL },
+ async function (browser) {
+ const {
+ autoCompletePopup: { richlistbox: itemsBox },
+ } = browser;
+
+ await openPopupOn(browser, "#organization");
+ // Click on the footer
+ let optionButton = itemsBox.querySelector(
+ ".autocomplete-richlistitem:last-child"
+ );
+ while (optionButton.collapsed) {
+ optionButton = optionButton.previousElementSibling;
+ }
+ optionButton = optionButton._optionButton;
+
+ const prefTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ PRIVACY_PREF_URL,
+ true
+ );
+ // Make sure dropdown is visible before continuing mouse synthesizing.
+ await BrowserTestUtils.waitForCondition(() =>
+ BrowserTestUtils.isVisible(optionButton)
+ );
+ await EventUtils.synthesizeMouseAtCenter(optionButton, {});
+ info(`expecting tab: about:preferences#privacy opened`);
+ const prefTab = await prefTabPromise;
+ info(`expecting tab: about:preferences#privacy removed`);
+ BrowserTestUtils.removeTab(prefTab);
+ ok(
+ true,
+ "Tab: preferences#privacy was successfully opened by clicking on the footer"
+ );
+
+ await closePopup(browser);
+ }
+ );
+});
+
+add_task(async function test_phishing_warning_single_category() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: URL },
+ async function (browser) {
+ const {
+ autoCompletePopup: { richlistbox: itemsBox },
+ } = browser;
+
+ await openPopupOn(browser, "#tel");
+ const warningBox = itemsBox.querySelector(
+ ".autocomplete-richlistitem:last-child"
+ )._warningTextBox;
+ ok(warningBox, "Got phishing warning box");
+ await expectWarningText(browser, "Also autofills address");
+ await closePopup(browser);
+ }
+ );
+});
+
+add_task(async function test_phishing_warning_complex_categories() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: URL },
+ async function (browser) {
+ await openPopupOn(browser, "#street-address");
+
+ await expectWarningText(browser, "Also autofills organization, email");
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ await expectWarningText(browser, "Autofills address");
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ await expectWarningText(browser, "Also autofills organization, email");
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ await expectWarningText(browser, "Also autofills organization, email");
+
+ await closePopup(browser);
+ }
+ );
+});
diff --git a/browser/extensions/formautofill/test/browser/browser_autocomplete_marked_back_forward.js b/browser/extensions/formautofill/test/browser/browser_autocomplete_marked_back_forward.js
new file mode 100644
index 0000000000..a1582776fd
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_autocomplete_marked_back_forward.js
@@ -0,0 +1,66 @@
+/**
+ * Test that autofill autocomplete works after back/forward navigation
+ */
+
+"use strict";
+
+const URL = BASE_URL + "autocomplete_basic.html";
+
+function checkPopup(autoCompletePopup) {
+ let first = autoCompletePopup.view.results[0];
+ const { primary, secondary } = JSON.parse(first.label);
+ ok(
+ primary.startsWith(TEST_ADDRESS_1["street-address"].split("\n")[0]),
+ "Check primary label is street address"
+ );
+ is(
+ secondary,
+ TEST_ADDRESS_1["address-level2"],
+ "Check secondary label is address-level2"
+ );
+}
+
+add_task(async function setup_storage() {
+ await setStorage(TEST_ADDRESS_1, TEST_ADDRESS_2, TEST_ADDRESS_3);
+});
+
+add_task(async function test_back_forward() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: URL },
+ async function (browser) {
+ const { autoCompletePopup } = browser;
+
+ // Check the page after the initial load
+ await openPopupOn(browser, "#street-address");
+ checkPopup(autoCompletePopup);
+
+ // Now navigate forward and make sure autofill autocomplete results are still attached
+ let loadPromise = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, `${URL}?load=2`);
+ info("expecting browser loaded");
+ await loadPromise;
+
+ // Check the second page
+ await openPopupOn(browser, "#street-address");
+ checkPopup(autoCompletePopup);
+
+ // Check after hitting back to the first page
+ let stoppedPromise = BrowserTestUtils.browserStopped(browser);
+ browser.goBack();
+ info("expecting browser stopped");
+ await stoppedPromise;
+ await openPopupOn(browser, "#street-address");
+ checkPopup(autoCompletePopup);
+
+ // Check after hitting forward to the second page
+ stoppedPromise = BrowserTestUtils.browserStopped(browser);
+ browser.goForward();
+ info("expecting browser stopped");
+ await stoppedPromise;
+ await openPopupOn(browser, "#street-address");
+ checkPopup(autoCompletePopup);
+
+ await closePopup(browser);
+ }
+ );
+});
diff --git a/browser/extensions/formautofill/test/browser/browser_autocomplete_marked_detached_tab.js b/browser/extensions/formautofill/test/browser/browser_autocomplete_marked_detached_tab.js
new file mode 100644
index 0000000000..399e7e16e5
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_autocomplete_marked_detached_tab.js
@@ -0,0 +1,58 @@
+/**
+ * Test that autofill autocomplete works after detaching a tab
+ */
+
+"use strict";
+
+const URL = BASE_URL + "autocomplete_basic.html";
+
+function checkPopup(autoCompletePopup) {
+ let first = autoCompletePopup.view.results[0];
+ const { primary, secondary } = JSON.parse(first.label);
+ ok(
+ primary.startsWith(TEST_ADDRESS_1["street-address"].split("\n")[0]),
+ "Check primary label is street address"
+ );
+ is(
+ secondary,
+ TEST_ADDRESS_1["address-level2"],
+ "Check secondary label is address-level2"
+ );
+}
+
+add_task(async function setup_storage() {
+ await setStorage(TEST_ADDRESS_1, TEST_ADDRESS_2, TEST_ADDRESS_3);
+});
+
+add_task(async function test_detach_tab_marked() {
+ let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser, url: URL });
+ let browser = tab.linkedBrowser;
+ const { autoCompletePopup } = browser;
+
+ // Check the page after the initial load
+ await openPopupOn(browser, "#street-address");
+ checkPopup(autoCompletePopup);
+ await closePopup(browser);
+
+ // Detach the tab to a new window
+ info("expecting tab replaced with new window");
+ let windowLoadedPromise = BrowserTestUtils.waitForNewWindow();
+ let newWin = gBrowser.replaceTabWithWindow(
+ gBrowser.getTabForBrowser(browser)
+ );
+ await windowLoadedPromise;
+
+ info("tab was detached");
+ let newBrowser = newWin.gBrowser.selectedBrowser;
+ ok(newBrowser, "Found new <browser>");
+ let newAutoCompletePopup = newBrowser.autoCompletePopup;
+ ok(newAutoCompletePopup, "Found new autocomplete popup");
+
+ await openPopupOn(newBrowser, "#street-address");
+ checkPopup(newAutoCompletePopup);
+
+ await closePopup(newBrowser);
+ let windowRefocusedPromise = BrowserTestUtils.waitForEvent(window, "focus");
+ await BrowserTestUtils.closeWindow(newWin);
+ await windowRefocusedPromise;
+});
diff --git a/browser/extensions/formautofill/test/browser/browser_autofill_address_select.js b/browser/extensions/formautofill/test/browser/browser_autofill_address_select.js
new file mode 100644
index 0000000000..4adee8addd
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_autofill_address_select.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PROFILE_US = {
+ email: "address_us@mozilla.org",
+ organization: "Mozilla",
+ country: "US",
+};
+
+const TEST_PROFILE_CA = {
+ email: "address_ca@mozilla.org",
+ organization: "Mozilla",
+ country: "CA",
+};
+
+const MARKUP_SELECT_COUNTRY = `
+ <html><body>
+ <input id="email" autocomplete="email">
+ <input id="organization" autocomplete="organization">
+ <select id="country" autocomplete="country">
+ <option value="">Select a country</option>
+ <option value="Germany">Germany</option>
+ <option value="Canada">Canada</option>
+ <option value="United States">United States</option>
+ <option value="France">France</option>
+ </select>
+ </body></html>
+`;
+
+// Strip any attributes that could help identify select as country field
+const MARKUP_SELECT_COUNTRY_WITHOUT_AUTOCOMPLETE =
+ MARKUP_SELECT_COUNTRY.replace(/<select[^>]*>/, "<select>");
+
+add_autofill_heuristic_tests([
+ {
+ fixtureData: MARKUP_SELECT_COUNTRY,
+ profile: TEST_PROFILE_US,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "email", autofill: TEST_PROFILE_US.email },
+ { fieldName: "organization", autofill: TEST_PROFILE_US.organization },
+ { fieldName: "country", autofill: "United States" },
+ ],
+ },
+ ],
+ },
+ {
+ fixtureData: MARKUP_SELECT_COUNTRY,
+ profile: TEST_PROFILE_CA,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "email", autofill: TEST_PROFILE_CA.email },
+ { fieldName: "organization", autofill: TEST_PROFILE_CA.organization },
+ { fieldName: "country", autofill: "Canada" },
+ ],
+ },
+ ],
+ },
+ {
+ fixtureData: MARKUP_SELECT_COUNTRY_WITHOUT_AUTOCOMPLETE,
+ profile: TEST_PROFILE_CA,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "email", autofill: TEST_PROFILE_CA.email },
+ { fieldName: "organization", autofill: TEST_PROFILE_CA.organization },
+ {
+ fieldName: "country",
+ autofill: "Canada",
+ reason: "regex-heuristic",
+ },
+ ],
+ },
+ ],
+ },
+]);
diff --git a/browser/extensions/formautofill/test/browser/browser_autofill_duplicate_fields.js b/browser/extensions/formautofill/test/browser/browser_autofill_duplicate_fields.js
new file mode 100644
index 0000000000..d7f80c2749
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_autofill_duplicate_fields.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PROFILE = {
+ "given-name": "Timothy",
+ "additional-name": "John",
+ "family-name": "Berners-Lee",
+ organization: "Mozilla",
+ "street-address": "331 E Evelyn Ave",
+ "address-level2": "Mountain View",
+ "address-level1": "CA",
+ "postal-code": "94041",
+ country: "US",
+ tel: "+16509030800",
+ email: "address@mozilla.org",
+};
+
+add_autofill_heuristic_tests([
+ {
+ fixtureData: `
+ <html><body>
+ <input id="email" autocomplete="email">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="country" autocomplete="country">
+ </body></html>
+ `,
+ profile: TEST_PROFILE,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "email", autofill: TEST_PROFILE.email },
+ { fieldName: "postal-code", autofill: TEST_PROFILE["postal-code"] },
+ { fieldName: "country", autofill: TEST_PROFILE.country },
+ ],
+ },
+ ],
+ },
+ {
+ description: "autofill multiple email fields(2)",
+ fixtureData: `
+ <html><body>
+ <input id="email" autocomplete="email">
+ <input id="email" autocomplete="email">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="country" autocomplete="country">
+ </body></html>
+ `,
+ profile: TEST_PROFILE,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "email", autofill: TEST_PROFILE.email },
+ { fieldName: "email", autofill: TEST_PROFILE.email },
+ { fieldName: "postal-code", autofill: TEST_PROFILE["postal-code"] },
+ { fieldName: "country", autofill: TEST_PROFILE.country },
+ ],
+ },
+ ],
+ },
+ {
+ description: "autofill multiple email fields(3)",
+ fixtureData: `
+ <html><body>
+ <input id="email" autocomplete="email">
+ <input id="email" autocomplete="email">
+ <input id="email" autocomplete="email">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="country" autocomplete="country">
+ </body></html>
+ `,
+ profile: TEST_PROFILE,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "email", autofill: TEST_PROFILE.email },
+ { fieldName: "email", autofill: TEST_PROFILE.email },
+ { fieldName: "email", autofill: TEST_PROFILE.email },
+ { fieldName: "postal-code", autofill: TEST_PROFILE["postal-code"] },
+ { fieldName: "country", autofill: TEST_PROFILE.country },
+ ],
+ },
+ ],
+ },
+]);
diff --git a/browser/extensions/formautofill/test/browser/browser_check_installed.js b/browser/extensions/formautofill/test/browser/browser_check_installed.js
new file mode 100644
index 0000000000..a93e64c209
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_check_installed.js
@@ -0,0 +1,12 @@
+"use strict";
+
+add_task(async function test_enabled() {
+ let addon = await AddonManager.getAddonByID("formautofill@mozilla.org");
+ isnot(addon, null, "Check addon exists");
+ is(addon.version, "1.0.1", "Check version");
+ is(addon.name, "Form Autofill", "Check name");
+ ok(addon.isCompatible, "Check application compatibility");
+ ok(!addon.appDisabled, "Check not app disabled");
+ ok(addon.isActive, "Check addon is active");
+ is(addon.type, "extension", "Check type is 'extension'");
+});
diff --git a/browser/extensions/formautofill/test/browser/browser_dropdown_layout.js b/browser/extensions/formautofill/test/browser/browser_dropdown_layout.js
new file mode 100644
index 0000000000..bc1d2fccab
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_dropdown_layout.js
@@ -0,0 +1,53 @@
+"use strict";
+
+const URL =
+ "http://example.org/browser/browser/extensions/formautofill/test/browser/autocomplete_basic.html";
+
+add_task(async function setup_storage() {
+ await setStorage(TEST_ADDRESS_1, TEST_ADDRESS_2, TEST_ADDRESS_3);
+});
+
+async function reopenPopupWithResizedInput(browser, selector, newSize) {
+ await closePopup(browser);
+ /* eslint no-shadow: ["error", { "allow": ["selector", "newSize"] }] */
+ await SpecialPowers.spawn(
+ browser,
+ [{ selector, newSize }],
+ async function ({ selector, newSize }) {
+ const input = content.document.querySelector(selector);
+
+ input.style.boxSizing = "border-box";
+ input.style.width = newSize + "px";
+ }
+ );
+ await openPopupOn(browser, selector);
+}
+
+add_task(async function test_address_dropdown() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: URL },
+ async function (browser) {
+ const focusInput = "#organization";
+ await openPopupOn(browser, focusInput);
+ const firstItem = getDisplayedPopupItems(browser)[0];
+
+ is(firstItem.getAttribute("ac-image"), "", "Should not show icon");
+
+ // The breakpoint of two-lines layout is 150px
+ await reopenPopupWithResizedInput(browser, focusInput, 140);
+ is(
+ firstItem._itemBox.getAttribute("size"),
+ "small",
+ "Show two-lines layout"
+ );
+ await reopenPopupWithResizedInput(browser, focusInput, 160);
+ is(
+ firstItem._itemBox.hasAttribute("size"),
+ false,
+ "Show one-line layout"
+ );
+
+ await closePopup(browser);
+ }
+ );
+});
diff --git a/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js b/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js
new file mode 100644
index 0000000000..62797739fc
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js
@@ -0,0 +1,690 @@
+"use strict";
+
+const { FormAutofillUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ Region: "resource://gre/modules/Region.sys.mjs",
+});
+
+requestLongerTimeout(6);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUPPORTED_COUNTRIES_PREF, "US,CA,DE"]],
+ });
+});
+
+add_task(async function test_cancelEditAddressDialog() {
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
+ win.document.querySelector("#cancel").click();
+ });
+});
+
+add_task(async function test_cancelEditAddressDialogWithESC() {
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ });
+});
+
+add_task(async function test_defaultCountry() {
+ Region._setHomeRegion("CA", false);
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
+ let doc = win.document;
+ is(
+ doc.querySelector("#country").value,
+ "CA",
+ "Default country set to Canada"
+ );
+ doc.querySelector("#cancel").click();
+ });
+ Region._setHomeRegion("DE", false);
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
+ let doc = win.document;
+ is(
+ doc.querySelector("#country").value,
+ "DE",
+ "Default country set to Germany"
+ );
+ doc.querySelector("#cancel").click();
+ });
+ // Test unsupported country
+ Region._setHomeRegion("XX", false);
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
+ let doc = win.document;
+ is(doc.querySelector("#country").value, "", "Default country set to empty");
+ doc.querySelector("#cancel").click();
+ });
+ Region._setHomeRegion("US", false);
+});
+
+add_task(async function test_saveAddress() {
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
+ let doc = win.document;
+ // Verify labels
+ is(
+ doc.querySelector("#address-level1-container > .label-text").textContent,
+ "State",
+ "US address-level1 label should be 'State'"
+ );
+ is(
+ doc.querySelector("#postal-code-container > .label-text").textContent,
+ "ZIP Code",
+ "US postal-code label should be 'ZIP Code'"
+ );
+ // Input address info and verify move through form with tab keys
+ let keypresses = [
+ "VK_TAB",
+ [
+ TEST_ADDRESS_1["given-name"],
+ TEST_ADDRESS_1["additional-name"],
+ TEST_ADDRESS_1["family-name"],
+ ].join(" "),
+ "VK_TAB",
+ TEST_ADDRESS_1.organization,
+ "VK_TAB",
+ TEST_ADDRESS_1["street-address"],
+ "VK_TAB",
+ TEST_ADDRESS_1["address-level2"],
+ "VK_TAB",
+ TEST_ADDRESS_1["address-level1"],
+ "VK_TAB",
+ TEST_ADDRESS_1["postal-code"],
+ "VK_TAB",
+ // TEST_ADDRESS_1.country, // Country is already US
+ "VK_TAB",
+ TEST_ADDRESS_1.tel,
+ "VK_TAB",
+ TEST_ADDRESS_1.email,
+ "VK_TAB",
+ ];
+ if (AppConstants.platform != "win") {
+ keypresses.push("VK_TAB", "VK_RETURN");
+ } else {
+ keypresses.push("VK_RETURN");
+ }
+ keypresses.forEach(keypress => {
+ if (
+ doc.activeElement.localName == "select" &&
+ !keypress.startsWith("VK_")
+ ) {
+ let field = doc.activeElement;
+ while (field.value != keypress) {
+ EventUtils.synthesizeKey(keypress[0], {}, win);
+ }
+ } else {
+ EventUtils.synthesizeKey(keypress, {}, win);
+ }
+ });
+ });
+ let addresses = await getAddresses();
+
+ is(addresses.length, 1, "only one address is in storage");
+ for (let [fieldName, fieldValue] of Object.entries(TEST_ADDRESS_1)) {
+ is(addresses[0][fieldName], fieldValue, "check " + fieldName);
+ }
+});
+
+add_task(async function test_editAddress() {
+ let addresses = await getAddresses();
+ await testDialog(
+ EDIT_ADDRESS_DIALOG_URL,
+ win => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_RIGHT", {}, win);
+ EventUtils.synthesizeKey("test", {}, win);
+
+ let stateSelect = win.document.querySelector("#address-level1");
+ is(
+ stateSelect.selectedOptions[0].value,
+ TEST_ADDRESS_1["address-level1"],
+ "address-level1 should be selected in the dropdown"
+ );
+
+ win.document.querySelector("#save").click();
+ },
+ {
+ record: addresses[0],
+ }
+ );
+ addresses = await getAddresses();
+
+ is(addresses.length, 1, "only one address is in storage");
+ const name = [
+ TEST_ADDRESS_1["given-name"],
+ TEST_ADDRESS_1["additional-name"],
+ TEST_ADDRESS_1["family-name"],
+ ].join(" ");
+ is(addresses[0].name, name + "test", "name changed");
+ await removeAddresses([addresses[0].guid]);
+
+ addresses = await getAddresses();
+ is(addresses.length, 0, "Address storage is empty");
+});
+
+add_task(
+ async function test_editAddressFrenchCanadianChangedToEnglishRepresentation() {
+ let addressClone = Object.assign({}, TEST_ADDRESS_CA_1);
+ addressClone["address-level1"] = "Colombie-Britannique";
+ await setStorage(addressClone);
+
+ let addresses = await getAddresses();
+ await testDialog(
+ EDIT_ADDRESS_DIALOG_URL,
+ win => {
+ let stateSelect = win.document.querySelector("#address-level1");
+ is(
+ stateSelect.selectedOptions[0].value,
+ "BC",
+ "address-level1 should have 'BC' selected in the dropdown"
+ );
+
+ win.document.querySelector("#save").click();
+ },
+ {
+ record: addresses[0],
+ }
+ );
+ addresses = await getAddresses();
+
+ is(addresses.length, 1, "only one address is in storage");
+ is(addresses[0]["address-level1"], "BC", "address-level1 changed");
+ await removeAddresses([addresses[0].guid]);
+
+ addresses = await getAddresses();
+ is(addresses.length, 0, "Address storage is empty");
+ }
+);
+
+add_task(async function test_editSparseAddress() {
+ let record = { ...TEST_ADDRESS_1 };
+ info("delete some usually required properties");
+ delete record["street-address"];
+ delete record["address-level1"];
+ delete record["address-level2"];
+ await testDialog(
+ EDIT_ADDRESS_DIALOG_URL,
+ win => {
+ is(
+ win.document.querySelectorAll(":user-invalid").length,
+ 0,
+ "Check no fields are visually invalid"
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_RIGHT", {}, win);
+ EventUtils.synthesizeKey("test", {}, win);
+ is(
+ win.document.querySelector("#save").disabled,
+ false,
+ "Save button should be enabled after an edit"
+ );
+ win.document.querySelector("#cancel").click();
+ },
+ {
+ record,
+ }
+ );
+});
+
+add_task(async function test_saveAddressCA() {
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
+ let doc = win.document;
+ // Change country to verify labels
+ doc.querySelector("#country").focus();
+ EventUtils.synthesizeKey("Canada", {}, win);
+
+ await TestUtils.waitForCondition(() => {
+ return (
+ doc.querySelector("#address-level1-container > .label-text")
+ .textContent == "Province"
+ );
+ }, "Wait for the mutation observer to change the labels");
+ is(
+ doc.querySelector("#address-level1-container > .label-text").textContent,
+ "Province",
+ "CA address-level1 label should be 'Province'"
+ );
+ is(
+ doc.querySelector("#postal-code-container > .label-text").textContent,
+ "Postal Code",
+ "CA postal-code label should be 'Postal Code'"
+ );
+ is(
+ doc.querySelector("#address-level3-container").style.display,
+ "none",
+ "CA address-level3 should be hidden"
+ );
+
+ // Input address info and verify move through form with tab keys
+ doc.querySelector("#name").focus();
+ let keyInputs = [
+ [
+ TEST_ADDRESS_CA_1["given-name"],
+ TEST_ADDRESS_CA_1["additional-name"],
+ TEST_ADDRESS_CA_1["family-name"],
+ ].join(" "),
+ "VK_TAB",
+ TEST_ADDRESS_CA_1.organization,
+ "VK_TAB",
+ TEST_ADDRESS_CA_1["street-address"],
+ "VK_TAB",
+ TEST_ADDRESS_CA_1["address-level2"],
+ "VK_TAB",
+ TEST_ADDRESS_CA_1["address-level1"],
+ "VK_TAB",
+ TEST_ADDRESS_CA_1["postal-code"],
+ "VK_TAB",
+ // TEST_ADDRESS_1.country, // Country is already selected above
+ "VK_TAB",
+ TEST_ADDRESS_CA_1.tel,
+ "VK_TAB",
+ TEST_ADDRESS_CA_1.email,
+ "VK_TAB",
+ ];
+ if (AppConstants.platform != "win") {
+ keyInputs.push("VK_TAB", "VK_RETURN");
+ } else {
+ keyInputs.push("VK_RETURN");
+ }
+ keyInputs.forEach(input => EventUtils.synthesizeKey(input, {}, win));
+ });
+ let addresses = await getAddresses();
+ for (let [fieldName, fieldValue] of Object.entries(TEST_ADDRESS_CA_1)) {
+ is(addresses[0][fieldName], fieldValue, "check " + fieldName);
+ }
+ await removeAllRecords();
+});
+
+add_task(async function test_saveAddressDE() {
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
+ let doc = win.document;
+ // Change country to verify labels
+ doc.querySelector("#country").focus();
+ EventUtils.synthesizeKey("Germany", {}, win);
+ await TestUtils.waitForCondition(() => {
+ return (
+ doc.querySelector("#postal-code-container > .label-text").textContent ==
+ "Postal Code"
+ );
+ }, "Wait for the mutation observer to change the labels");
+ is(
+ doc.querySelector("#postal-code-container > .label-text").textContent,
+ "Postal Code",
+ "DE postal-code label should be 'Postal Code'"
+ );
+ is(
+ doc.querySelector("#address-level1-container").style.display,
+ "none",
+ "DE address-level1 should be hidden"
+ );
+ is(
+ doc.querySelector("#address-level3-container").style.display,
+ "none",
+ "DE address-level3 should be hidden"
+ );
+ // Input address info and verify move through form with tab keys
+ doc.querySelector("#name").focus();
+ let keyInputs = [
+ [
+ TEST_ADDRESS_DE_1["given-name"],
+ TEST_ADDRESS_DE_1["additional-name"],
+ TEST_ADDRESS_DE_1["family-name"],
+ ].join(" "),
+ "VK_TAB",
+ TEST_ADDRESS_DE_1.organization,
+ "VK_TAB",
+ TEST_ADDRESS_DE_1["street-address"],
+ "VK_TAB",
+ TEST_ADDRESS_DE_1["postal-code"],
+ "VK_TAB",
+ TEST_ADDRESS_DE_1["address-level2"],
+ "VK_TAB",
+ // TEST_ADDRESS_1.country, // Country is already selected above
+ "VK_TAB",
+ TEST_ADDRESS_DE_1.tel,
+ "VK_TAB",
+ TEST_ADDRESS_DE_1.email,
+ "VK_TAB",
+ ];
+ if (AppConstants.platform != "win") {
+ keyInputs.push("VK_TAB", "VK_RETURN");
+ } else {
+ keyInputs.push("VK_RETURN");
+ }
+ keyInputs.forEach(input => EventUtils.synthesizeKey(input, {}, win));
+ });
+ let addresses = await getAddresses();
+ for (let [fieldName, fieldValue] of Object.entries(TEST_ADDRESS_DE_1)) {
+ is(addresses[0][fieldName], fieldValue, "check " + fieldName);
+ }
+ await removeAllRecords();
+});
+
+add_task(async function test_saveAddressIE() {
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
+ let doc = win.document;
+ // Change country to verify labels
+ doc.querySelector("#country").focus();
+ EventUtils.synthesizeKey("Ireland", {}, win);
+ await TestUtils.waitForCondition(() => {
+ return (
+ doc.querySelector("#postal-code-container > .label-text").textContent ==
+ "Eircode"
+ );
+ }, "Wait for the mutation observer to change the labels");
+ is(
+ doc.querySelector("#postal-code-container > .label-text").textContent,
+ "Eircode",
+ "IE postal-code label should be 'Eircode'"
+ );
+ is(
+ doc.querySelector("#address-level1-container > .label-text").textContent,
+ "County",
+ "IE address-level1 should be 'County'"
+ );
+ is(
+ doc.querySelector("#address-level3-container > .label-text").textContent,
+ "Townland",
+ "IE address-level3 should be 'Townland'"
+ );
+
+ // Input address info and verify move through form with tab keys
+ doc.querySelector("#name").focus();
+ let keyInputs = [
+ [
+ TEST_ADDRESS_IE_1["given-name"],
+ TEST_ADDRESS_IE_1["additional-name"],
+ TEST_ADDRESS_IE_1["family-name"],
+ ].join(" "),
+ "VK_TAB",
+ TEST_ADDRESS_IE_1.organization,
+ "VK_TAB",
+ TEST_ADDRESS_IE_1["street-address"],
+ "VK_TAB",
+ TEST_ADDRESS_IE_1["address-level3"],
+ "VK_TAB",
+ TEST_ADDRESS_IE_1["address-level2"],
+ "VK_TAB",
+ TEST_ADDRESS_IE_1["address-level1"],
+ "VK_TAB",
+ TEST_ADDRESS_IE_1["postal-code"],
+ "VK_TAB",
+ // TEST_ADDRESS_1.country, // Country is already selected above
+ "VK_TAB",
+ TEST_ADDRESS_IE_1.tel,
+ "VK_TAB",
+ TEST_ADDRESS_IE_1.email,
+ "VK_TAB",
+ ];
+ if (AppConstants.platform != "win") {
+ keyInputs.push("VK_TAB", "VK_RETURN");
+ } else {
+ keyInputs.push("VK_RETURN");
+ }
+ keyInputs.forEach(input => EventUtils.synthesizeKey(input, {}, win));
+ });
+
+ let addresses = await getAddresses();
+ for (let [fieldName, fieldValue] of Object.entries(TEST_ADDRESS_IE_1)) {
+ is(addresses[0][fieldName], fieldValue, "check " + fieldName);
+ }
+ await removeAllRecords();
+});
+
+add_task(async function test_countryAndStateFieldLabels() {
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
+ let doc = win.document;
+ // Change country to verify labels
+ doc.querySelector("#country").focus();
+
+ let mutableLabels = [
+ "postal-code-container",
+ "address-level1-container",
+ "address-level2-container",
+ "address-level3-container",
+ ].map(containerID =>
+ doc.getElementById(containerID).querySelector(":scope > .label-text")
+ );
+
+ for (let countryOption of doc.querySelector("#country").options) {
+ if (countryOption.value == "") {
+ info("Skipping the empty country option");
+ continue;
+ }
+
+ // Clear L10N textContent to not leave leftovers between country tests
+ for (let labelEl of mutableLabels) {
+ doc.l10n.setAttributes(labelEl, "");
+ labelEl.textContent = "";
+ }
+
+ info(`Selecting '${countryOption.label}' (${countryOption.value})`);
+ EventUtils.synthesizeKey(countryOption.label, {}, win);
+
+ let l10nResolve;
+ let l10nReady = new Promise(resolve => {
+ l10nResolve = resolve;
+ });
+ let verifyL10n = () => {
+ if (mutableLabels.every(labelEl => labelEl.textContent)) {
+ win.removeEventListener("MozAfterPaint", verifyL10n);
+ l10nResolve();
+ }
+ };
+ win.addEventListener("MozAfterPaint", verifyL10n);
+ await l10nReady;
+
+ // Check that the labels were filled
+ for (let labelEl of mutableLabels) {
+ isnot(
+ labelEl.textContent,
+ "",
+ "Ensure textContent is non-empty for: " + countryOption.value
+ );
+ }
+
+ let stateOptions = doc.querySelector("#address-level1").options;
+ /* eslint-disable max-len */
+ let expectedStateOptions = {
+ BS: {
+ // The Bahamas is an interesting testcase because they have some keys that are full names, and others are replaced with ISO IDs.
+ keys: "Abaco~AK~Andros~BY~BI~CI~Crooked Island~Eleuthera~EX~Grand Bahama~HI~IN~LI~MG~N.P.~RI~RC~SS~SW".split(
+ "~"
+ ),
+ names:
+ "Abaco Islands~Acklins~Andros Island~Berry Islands~Bimini~Cat Island~Crooked Island~Eleuthera~Exuma and Cays~Grand Bahama~Harbour Island~Inagua~Long Island~Mayaguana~New Providence~Ragged Island~Rum Cay~San Salvador~Spanish Wells".split(
+ "~"
+ ),
+ },
+ US: {
+ keys: "AL~AK~AS~AZ~AR~AA~AE~AP~CA~CO~CT~DE~DC~FL~GA~GU~HI~ID~IL~IN~IA~KS~KY~LA~ME~MH~MD~MA~MI~FM~MN~MS~MO~MT~NE~NV~NH~NJ~NM~NY~NC~ND~MP~OH~OK~OR~PW~PA~PR~RI~SC~SD~TN~TX~UT~VT~VI~VA~WA~WV~WI~WY".split(
+ "~"
+ ),
+ names:
+ "Alabama~Alaska~American Samoa~Arizona~Arkansas~Armed Forces (AA)~Armed Forces (AE)~Armed Forces (AP)~California~Colorado~Connecticut~Delaware~District of Columbia~Florida~Georgia~Guam~Hawaii~Idaho~Illinois~Indiana~Iowa~Kansas~Kentucky~Louisiana~Maine~Marshall Islands~Maryland~Massachusetts~Michigan~Micronesia~Minnesota~Mississippi~Missouri~Montana~Nebraska~Nevada~New Hampshire~New Jersey~New Mexico~New York~North Carolina~North Dakota~Northern Mariana Islands~Ohio~Oklahoma~Oregon~Palau~Pennsylvania~Puerto Rico~Rhode Island~South Carolina~South Dakota~Tennessee~Texas~Utah~Vermont~Virgin Islands~Virginia~Washington~West Virginia~Wisconsin~Wyoming".split(
+ "~"
+ ),
+ },
+ };
+ /* eslint-enable max-len */
+
+ if (expectedStateOptions[countryOption.value]) {
+ let { keys, names } = expectedStateOptions[countryOption.value];
+ is(
+ stateOptions.length,
+ keys.length + 1,
+ "stateOptions should list all options plus a blank entry"
+ );
+ is(stateOptions[0].value, "", "First State option should be blank");
+ for (let i = 1; i < stateOptions.length; i++) {
+ is(
+ stateOptions[i].value,
+ keys[i - 1],
+ "Each State should be listed in alphabetical name order (key)"
+ );
+ is(
+ stateOptions[i].text,
+ names[i - 1],
+ "Each State should be listed in alphabetical name order (name)"
+ );
+ }
+ }
+ // Dispatch a dummy key event so that <select>'s incremental search is cleared.
+ EventUtils.synthesizeKey("VK_ACCEPT", {}, win);
+ }
+
+ doc.querySelector("#cancel").click();
+ });
+});
+
+add_task(async function test_hiddenFieldNotSaved() {
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
+ let doc = win.document;
+ doc.querySelector("#address-level2").focus();
+ EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level2"], {}, win);
+ doc.querySelector("#address-level1").focus();
+ EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level1"], {}, win);
+ doc.querySelector("#country").focus();
+ EventUtils.synthesizeKey("Germany", {}, win);
+ doc.querySelector("#save").focus();
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ });
+ let addresses = await getAddresses();
+ is(addresses[0].country, "DE", "check country");
+ is(
+ addresses[0]["address-level2"],
+ TEST_ADDRESS_1["address-level2"],
+ "check address-level2"
+ );
+ is(
+ addresses[0]["address-level1"],
+ undefined,
+ "address-level1 should not be saved"
+ );
+
+ await removeAllRecords();
+});
+
+add_task(async function test_hiddenFieldRemovedWhenCountryChanged() {
+ let addresses = await getAddresses();
+ ok(!addresses.length, "no addresses at start of test");
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
+ let doc = win.document;
+ doc.querySelector("#address-level2").focus();
+ EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level2"], {}, win);
+ doc.querySelector("#address-level1").focus();
+ while (
+ doc.querySelector("#address-level1").value !=
+ TEST_ADDRESS_1["address-level1"]
+ ) {
+ EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level1"][0], {}, win);
+ }
+ doc.querySelector("#save").focus();
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ });
+ addresses = await getAddresses();
+ is(addresses[0].country, "US", "check country");
+ is(
+ addresses[0]["address-level2"],
+ TEST_ADDRESS_1["address-level2"],
+ "check address-level2"
+ );
+ is(
+ addresses[0]["address-level1"],
+ TEST_ADDRESS_1["address-level1"],
+ "check address-level1"
+ );
+
+ await testDialog(
+ EDIT_ADDRESS_DIALOG_URL,
+ win => {
+ let doc = win.document;
+ doc.querySelector("#country").focus();
+ EventUtils.synthesizeKey("Germany", {}, win);
+ win.document.querySelector("#save").click();
+ },
+ {
+ record: addresses[0],
+ }
+ );
+ addresses = await getAddresses();
+
+ is(addresses.length, 1, "only one address is in storage");
+ is(
+ addresses[0]["address-level2"],
+ TEST_ADDRESS_1["address-level2"],
+ "check address-level2"
+ );
+ is(
+ addresses[0]["address-level1"],
+ undefined,
+ "address-level1 should be removed"
+ );
+ is(addresses[0].country, "DE", "country changed");
+ await removeAllRecords();
+});
+
+add_task(async function test_countrySpecificFieldsGetRequiredness() {
+ Region._setHomeRegion("RO", false);
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
+ let doc = win.document;
+ is(
+ doc.querySelector("#country").value,
+ "RO",
+ "Default country set to Romania"
+ );
+ let provinceField = doc.getElementById("address-level1");
+ ok(
+ !provinceField.required,
+ "address-level1 should not be marked as required"
+ );
+ ok(provinceField.disabled, "address-level1 should be marked as disabled");
+ is(
+ provinceField.parentNode.style.display,
+ "none",
+ "address-level1 is hidden for Romania"
+ );
+
+ doc.querySelector("#country").focus();
+ EventUtils.synthesizeKey("United States", {}, win);
+
+ await TestUtils.waitForCondition(
+ () => {
+ provinceField = doc.getElementById("address-level1");
+ return provinceField.parentNode.style.display != "none";
+ },
+ "Wait for address-level1 to become visible",
+ 10
+ );
+
+ ok(provinceField.required, "address-level1 should be marked as required");
+ ok(
+ !provinceField.disabled,
+ "address-level1 should not be marked as disabled"
+ );
+
+ // Dispatch a dummy key event so that <select>'s incremental search is cleared.
+ EventUtils.synthesizeKey("VK_ACCEPT", {}, win);
+
+ doc.querySelector("#country").focus();
+ EventUtils.synthesizeKey("Romania", {}, win);
+
+ await TestUtils.waitForCondition(
+ () => {
+ provinceField = doc.getElementById("address-level1");
+ return provinceField.parentNode.style.display == "none";
+ },
+ "Wait for address-level1 to become hidden",
+ 10
+ );
+
+ ok(
+ provinceField.required,
+ "address-level1 will still be marked as required"
+ );
+ ok(provinceField.disabled, "address-level1 should be marked as disabled");
+
+ doc.querySelector("#cancel").click();
+ });
+});
diff --git a/browser/extensions/formautofill/test/browser/browser_fathom_cc.js b/browser/extensions/formautofill/test/browser/browser_fathom_cc.js
new file mode 100644
index 0000000000..2e465fd503
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_fathom_cc.js
@@ -0,0 +1,204 @@
+/**
+ * By default this test only tests 1 sample. This is to avoid publishing all samples we have
+ * to the codebase. If you update the Fathom CC model, please follow the instruction below
+ * and run the test. Doing this makes sure the optimized (Native implementation) CC fathom model produces
+ * exactly the same result as the non-optimized model (JS implementation, See CreditCardRuleset.sys.mjs).
+ *
+ * To test this:
+ * 1. Run the test setup script (fathom/test-setup.sh) to download all samples to the local
+ * directory. Note that you need to have the permission to access the fathom-form-autofill
+ * 2. Set `gTestAutofillRepoSample` to true
+ * 3. Run this test
+ */
+
+"use strict";
+
+const eligibleElementSelector =
+ "input:not([type]), input[type=text], input[type=textbox], input[type=email], input[type=tel], input[type=number], input[type=month], select, button";
+
+const skippedSamples = [
+ // TOOD: Crash while running the following testcases. Since this is not caused by the fathom CC
+ // model, we just skip those for now
+ "EN_B105b.html",
+ "EN_B312a.html",
+ "EN_B118b.html",
+ "EN_B48c.html",
+
+ // This sample is skipped because of Bug 1754256 (Support lookaround regex for native fathom CC implementation).
+ "DE_B378b.html",
+];
+
+async function run_test(path, dirs) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.formautofill.creditCards.heuristics.mode", 1]],
+ });
+
+ // Collect files we are going to test.
+ let files = [];
+ for (let dir of dirs) {
+ let entries = new FileUtils.File(getTestFilePath(path + dir))
+ .directoryEntries;
+
+ while (entries.hasMoreElements()) {
+ let entry = entries.nextFile;
+ if (skippedSamples.includes(entry.leafName)) {
+ continue;
+ }
+
+ if (entry.leafName.endsWith(".html")) {
+ files.push(path + dir + entry.leafName);
+ }
+ }
+ }
+
+ ok(files.length, "no sample files found");
+
+ let summary = {};
+ for (let file of files) {
+ info("Testing " + file + "...");
+
+ await BrowserTestUtils.withNewTab(BASE_URL + file, async browser => {
+ summary[file] = await SpecialPowers.spawn(
+ browser,
+ [{ eligibleElementSelector, file }],
+ obj => {
+ const { FormAutofillHeuristics } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillHeuristics.sys.mjs"
+ );
+ const { FormAutofillUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+ );
+
+ let eligibleFields = [];
+ let nodeList = content.document.querySelectorAll(
+ obj.eligibleElementSelector
+ );
+ for (let i = 0; i < nodeList.length; i++) {
+ if (FormAutofillUtils.isCreditCardOrAddressFieldType(nodeList[i])) {
+ eligibleFields.push(nodeList[i]);
+ }
+ }
+ let failedFields = [];
+
+ info("Running CC fathom model");
+ let nativeConfidencesKeyedByType =
+ ChromeUtils.getFormAutofillConfidences(eligibleFields);
+ let jsConfidencesKeyedByType =
+ FormAutofillHeuristics.getFormAutofillConfidences(eligibleFields);
+
+ if (eligibleFields.length != nativeConfidencesKeyedByType.length) {
+ ok(
+ false,
+ `Get the wrong number of confidence value from the native model`
+ );
+ }
+ if (eligibleFields.length != jsConfidencesKeyedByType.length) {
+ ok(
+ false,
+ `Get the wrong number of confidence value from the js model`
+ );
+ }
+
+ // This value should sync with the number of supported types in
+ // CreditCardRuleset.sys.mjs (See `get types()` in `this.creditCardRulesets`).
+ const EXPECTED_NUM_OF_CONFIDENCE = 2;
+ for (let i = 0; i < eligibleFields.length; i++) {
+ if (
+ Object.keys(nativeConfidencesKeyedByType[i]).length !=
+ EXPECTED_NUM_OF_CONFIDENCE
+ ) {
+ ok(
+ false,
+ `Native CC model doesn't get confidence value for all types`
+ );
+ }
+ if (
+ Object.keys(jsConfidencesKeyedByType[i]).length !=
+ EXPECTED_NUM_OF_CONFIDENCE
+ ) {
+ ok(
+ false,
+ `JS CC model doesn't get confidence value for all types`
+ );
+ }
+
+ for (let [type, confidence] of Object.entries(
+ nativeConfidencesKeyedByType[i]
+ )) {
+ // Fix to 10 digit to ignore rounding error between js and c++.
+ let nativeConfidence = confidence.toFixed(10);
+ let jsConfidence =
+ jsConfidencesKeyedByType[i][
+ FormAutofillUtils.formAutofillConfidencesKeyToCCFieldType(
+ type
+ )
+ ].toFixed(10);
+ if (jsConfidence != nativeConfidence) {
+ info(
+ `${obj.file}: Element(id=${eligibleFields[i].id} doesn't have the same confidence value when rule type is ${type}`
+ );
+ if (!failedFields.includes(i)) {
+ failedFields.push(i);
+ }
+ }
+ }
+ }
+ ok(
+ !failedFields.length,
+ `${obj.file}: has the same confidences value on both models`
+ );
+ return {
+ tested: eligibleFields.length,
+ passed: eligibleFields.length - failedFields.length,
+ };
+ }
+ );
+ });
+ }
+
+ // Generating summary report
+ let total_tested_samples = 0;
+ let total_passed_samples = 0;
+ let total_tested_fields = 0;
+ let total_passed_fields = 0;
+ info("=====Summary=====");
+ for (const [k, v] of Object.entries(summary)) {
+ total_tested_samples++;
+ if (v.tested == v.passed) {
+ total_passed_samples++;
+ } else {
+ info("Failed Case:" + k);
+ }
+ total_tested_fields += v.tested;
+ total_passed_fields += v.passed;
+ }
+ info(
+ "Passed Samples/Test Samples: " +
+ total_passed_samples +
+ "/" +
+ total_tested_samples
+ );
+ info(
+ "Passed Fields/Test Fields: " +
+ total_passed_fields +
+ "/" +
+ total_tested_fields
+ );
+}
+
+add_task(async function test_native_cc_model() {
+ const path = "fathom/";
+ const dirs = ["testing/"];
+ await run_test(path, dirs);
+});
+
+add_task(async function test_native_cc_model_autofill_repo() {
+ const path = "fathom/autofill-repo-samples/";
+ const dirs = ["validation/", "training/", "testing/"];
+ if (await IOUtils.exists(getTestFilePath(path))) {
+ // Just to ignore timeout failure while running the test on the local
+ requestLongerTimeout(10);
+
+ await run_test(path, dirs);
+ }
+});
diff --git a/browser/extensions/formautofill/test/browser/browser_manageAddressesDialog.js b/browser/extensions/formautofill/test/browser/browser_manageAddressesDialog.js
new file mode 100644
index 0000000000..b070df0fda
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_manageAddressesDialog.js
@@ -0,0 +1,105 @@
+"use strict";
+
+const TEST_SELECTORS = {
+ selRecords: "#addresses",
+ btnRemove: "#remove",
+ btnAdd: "#add",
+ btnEdit: "#edit",
+};
+
+const DIALOG_SIZE = "width=600,height=400";
+
+add_task(async function test_manageAddressesInitialState() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: MANAGE_ADDRESSES_DIALOG_URL },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [TEST_SELECTORS], args => {
+ let selRecords = content.document.querySelector(args.selRecords);
+ let btnRemove = content.document.querySelector(args.btnRemove);
+ let btnEdit = content.document.querySelector(args.btnEdit);
+ let btnAdd = content.document.querySelector(args.btnAdd);
+
+ is(selRecords.length, 0, "No address");
+ is(btnAdd.disabled, false, "Add button enabled");
+ is(btnRemove.disabled, true, "Remove button disabled");
+ is(btnEdit.disabled, true, "Edit button disabled");
+ });
+ }
+ );
+});
+
+add_task(async function test_cancelManageAddressDialogWithESC() {
+ let win = window.openDialog(MANAGE_ADDRESSES_DIALOG_URL);
+ await waitForFocusAndFormReady(win);
+ let unloadPromise = BrowserTestUtils.waitForEvent(win, "unload");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ await unloadPromise;
+ ok(true, "Manage addresses dialog is closed with ESC key");
+});
+
+add_task(async function test_removingSingleAndMultipleAddresses() {
+ await setStorage(TEST_ADDRESS_1, TEST_ADDRESS_2, TEST_ADDRESS_3);
+
+ let win = window.openDialog(MANAGE_ADDRESSES_DIALOG_URL, null, DIALOG_SIZE);
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+ let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove);
+ let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
+
+ is(selRecords.length, 3, "Three addresses");
+
+ EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+ is(btnRemove.disabled, false, "Remove button enabled");
+ is(btnEdit.disabled, false, "Edit button enabled");
+ EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
+ is(selRecords.length, 2, "Two addresses left");
+
+ EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+ EventUtils.synthesizeMouseAtCenter(
+ selRecords.children[1],
+ { shiftKey: true },
+ win
+ );
+ is(btnEdit.disabled, true, "Edit button disabled when multi-select");
+
+ EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
+ is(selRecords.length, 0, "All addresses are removed");
+
+ win.close();
+});
+
+add_task(async function test_removingAdressViaKeyboardDelete() {
+ await setStorage(TEST_ADDRESS_1);
+ let win = window.openDialog(MANAGE_ADDRESSES_DIALOG_URL, null, DIALOG_SIZE);
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+
+ is(selRecords.length, 1, "One address");
+
+ EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+ EventUtils.synthesizeKey("VK_DELETE", {}, win);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
+ is(selRecords.length, 0, "No addresses left");
+
+ win.close();
+});
+
+add_task(async function test_addressesDialogWatchesStorageChanges() {
+ let win = window.openDialog(MANAGE_ADDRESSES_DIALOG_URL, null, DIALOG_SIZE);
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+
+ await setStorage(TEST_ADDRESS_1);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
+ is(selRecords.length, 1, "One address is shown");
+
+ await removeAddresses([selRecords.options[0].value]);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
+ is(selRecords.length, 0, "Address is removed");
+ win.close();
+});
diff --git a/browser/extensions/formautofill/test/browser/browser_privacyPreferences.js b/browser/extensions/formautofill/test/browser/browser_privacyPreferences.js
new file mode 100644
index 0000000000..2d5759ca66
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_privacyPreferences.js
@@ -0,0 +1,439 @@
+"use strict";
+
+const PAGE_PREFS = "about:preferences";
+const PAGE_PRIVACY = PAGE_PREFS + "#privacy";
+const SELECTORS = {
+ group: "#formAutofillGroupBox",
+ addressAutofillCheckbox: "#addressAutofill checkbox",
+ creditCardAutofillCheckbox: "#creditCardAutofill checkbox",
+ savedAddressesBtn: "#addressAutofill button",
+ savedCreditCardsBtn: "#creditCardAutofill button",
+ addressAutofillLearnMore: "#addressAutofillLearnMore",
+ creditCardAutofillLearnMore: "#creditCardAutofillLearnMore",
+ reauthCheckbox: "#creditCardReauthenticate checkbox",
+};
+
+const { FormAutofill } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofill.sys.mjs"
+);
+
+// Visibility of form autofill group should be hidden when opening
+// preferences page.
+add_task(async function test_aboutPreferences() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PREFS },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ await SpecialPowers.spawn(browser, [SELECTORS], selectors => {
+ is(
+ content.document.querySelector(selectors.group).hidden,
+ true,
+ "Form Autofill group should be hidden"
+ );
+ });
+ }
+ );
+});
+
+// Visibility of form autofill group should be visible when opening
+// directly to privacy page. Checkbox is checked by default.
+add_task(async function test_aboutPreferencesPrivacy() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ await SpecialPowers.spawn(browser, [SELECTORS], selectors => {
+ is(
+ content.document.querySelector(selectors.group).hidden,
+ false,
+ "Form Autofill group should be visible"
+ );
+ is(
+ content.document.querySelector(selectors.addressAutofillCheckbox)
+ .checked,
+ true,
+ "Autofill addresses checkbox should be checked"
+ );
+ is(
+ content.document.querySelector(selectors.creditCardAutofillCheckbox)
+ .checked,
+ true,
+ "Autofill credit cards checkbox should be checked"
+ );
+ ok(
+ content.document
+ .querySelector(selectors.addressAutofillLearnMore)
+ .href.includes("autofill-card-address"),
+ "Autofill addresses learn more link should contain autofill-card-address"
+ );
+ ok(
+ content.document
+ .querySelector(selectors.creditCardAutofillLearnMore)
+ .href.includes("credit-card-autofill"),
+ "Autofill credit cards learn more link should contain credit-card-autofill"
+ );
+ });
+ }
+ );
+});
+
+add_task(async function test_openManageAutofillDialogs() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ const args = [
+ SELECTORS,
+ MANAGE_ADDRESSES_DIALOG_URL,
+ MANAGE_CREDIT_CARDS_DIALOG_URL,
+ ];
+ await SpecialPowers.spawn(
+ browser,
+ [args],
+ ([selectors, addrUrl, ccUrl]) => {
+ function testManageDialogOpened(expectedUrl) {
+ return {
+ open: openUrl => is(openUrl, expectedUrl, "Manage dialog called"),
+ };
+ }
+
+ let realgSubDialog = content.window.gSubDialog;
+ content.window.gSubDialog = testManageDialogOpened(addrUrl);
+ content.document.querySelector(selectors.savedAddressesBtn).click();
+ content.window.gSubDialog = testManageDialogOpened(ccUrl);
+ content.document.querySelector(selectors.savedCreditCardsBtn).click();
+ content.window.gSubDialog = realgSubDialog;
+ }
+ );
+ }
+ );
+});
+
+add_task(async function test_autofillCheckboxes() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_ADDRESSES_PREF, false]],
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, false]],
+ });
+ let finalPrefPaneLoaded = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+ // Checkbox should be unchecked when form autofill addresses and credit cards are disabled.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ await SpecialPowers.spawn(browser, [SELECTORS], selectors => {
+ is(
+ content.document.querySelector(selectors.group).hidden,
+ false,
+ "Form Autofill group should be visible"
+ );
+ is(
+ content.document.querySelector(selectors.addressAutofillCheckbox)
+ .checked,
+ false,
+ "Checkbox should be unchecked when Autofill Addresses is disabled"
+ );
+ is(
+ content.document.querySelector(selectors.creditCardAutofillCheckbox)
+ .checked,
+ false,
+ "Checkbox should be unchecked when Autofill Credit Cards is disabled"
+ );
+ content.document
+ .querySelector(selectors.addressAutofillCheckbox)
+ .scrollIntoView({ block: "center", behavior: "instant" });
+ });
+
+ info("test toggling the checkboxes");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ SELECTORS.addressAutofillCheckbox,
+ {},
+ browser
+ );
+ is(
+ Services.prefs.getBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF),
+ true,
+ "Check address autofill is now enabled"
+ );
+
+ await SpecialPowers.spawn(browser, [SELECTORS], selectors => {
+ content.document
+ .querySelector(selectors.creditCardAutofillCheckbox)
+ .scrollIntoView({ block: "center", behavior: "instant" });
+ });
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ SELECTORS.creditCardAutofillCheckbox,
+ {},
+ browser
+ );
+ is(
+ Services.prefs.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF),
+ true,
+ "Check credit card autofill is now enabled"
+ );
+ }
+ );
+});
+
+add_task(async function test_creditCardNotAvailable() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[AUTOFILL_CREDITCARDS_AVAILABLE_PREF, false]],
+ });
+ let finalPrefPaneLoaded = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ await SpecialPowers.spawn(browser, [SELECTORS], selectors => {
+ is(
+ content.document.querySelector(selectors.group).hidden,
+ false,
+ "Form Autofill group should be visible"
+ );
+ ok(
+ !content.document.querySelector(selectors.creditCardAutofillCheckbox),
+ "Autofill credit cards checkbox should not exist"
+ );
+ });
+ }
+ );
+});
+
+add_task(async function test_reauth() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[AUTOFILL_CREDITCARDS_AVAILABLE_PREF, "on"]],
+ });
+ let { OSKeyStore } = ChromeUtils.importESModule(
+ "resource://gre/modules/OSKeyStore.sys.mjs"
+ );
+
+ let finalPrefPaneLoaded = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ await SpecialPowers.spawn(
+ browser,
+ [SELECTORS, OSKeyStore.canReauth()],
+ (selectors, canReauth) => {
+ is(
+ canReauth,
+ !!content.document.querySelector(selectors.reauthCheckbox),
+ "Re-authentication checkbox should be available if OSKeyStore.canReauth() is `true`"
+ );
+ }
+ );
+ }
+ );
+});
+
+add_task(async function test_addressAutofillNotAvailable() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[AUTOFILL_ADDRESSES_AVAILABLE_PREF, "off"]],
+ });
+
+ let autofillAddressEnabledValue = Services.prefs.getBoolPref(
+ ENABLED_AUTOFILL_ADDRESSES_PREF
+ );
+ let finalPrefPaneLoaded = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ await SpecialPowers.spawn(browser, [SELECTORS], selectors => {
+ is(
+ content.document.querySelector(selectors.group).hidden,
+ false,
+ "Form Autofill group should be visible"
+ );
+ is(
+ content.document.querySelector(selectors.addressAutofillCheckbox),
+ null,
+ "Address checkbox should not exist when address autofill is not enabled"
+ );
+ is(
+ content.document.querySelector(selectors.creditCardAutofillCheckbox)
+ .checked,
+ true,
+ "Checkbox should be checked when Autofill Credit Cards is enabled"
+ );
+ });
+ info("test toggling the credit card autofill checkbox");
+
+ await SpecialPowers.spawn(browser, [SELECTORS], selectors => {
+ content.document
+ .querySelector(selectors.creditCardAutofillCheckbox)
+ .scrollIntoView({ block: "center", behavior: "instant" });
+ });
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ SELECTORS.creditCardAutofillCheckbox,
+ {},
+ browser
+ );
+ is(
+ Services.prefs.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF),
+ false,
+ "Check credit card autofill is now disabled"
+ );
+ is(
+ Services.prefs.getBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF),
+ autofillAddressEnabledValue,
+ "Address autofill enabled's value should not change due to credit card checkbox interaction"
+ );
+ await SpecialPowers.spawn(browser, [SELECTORS], selectors => {
+ is(
+ content.document.querySelector(selectors.addressAutofillCheckbox),
+ null,
+ "Address checkbox should exist due to interaction with credit card checkbox"
+ );
+ });
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_addressAutofillNotAvailableViaRegion() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.addresses.supported", "detect"],
+ ["extensions.formautofill.creditCards.enabled", true],
+ ["browser.search.region", "FR"],
+ [ENABLED_AUTOFILL_ADDRESSES_SUPPORTED_COUNTRIES_PREF, "US,CA"],
+ ],
+ });
+
+ const addressAutofillEnabledPrefValue = Services.prefs.getBoolPref(
+ ENABLED_AUTOFILL_ADDRESSES_PREF
+ );
+ const addressAutofillAvailablePrefValue = Services.prefs.getCharPref(
+ AUTOFILL_ADDRESSES_AVAILABLE_PREF
+ );
+ is(
+ FormAutofill.isAutofillAddressesAvailable,
+ false,
+ "Address autofill should not be available due to unsupported region"
+ );
+ let finalPrefPaneLoaded = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ await SpecialPowers.spawn(browser, [SELECTORS], selectors => {
+ is(
+ content.document.querySelector(selectors.group).hidden,
+ false,
+ "Form Autofill group should be visible"
+ );
+ is(
+ content.document.querySelector(selectors.addressAutofillCheckbox),
+ null,
+ "Address checkbox should not exist due to address autofill not being available"
+ );
+ is(
+ content.document.querySelector(selectors.creditCardAutofillCheckbox)
+ .checked,
+ true,
+ "Checkbox should be checked when Autofill Credit Cards is enabled"
+ );
+ });
+ info("test toggling the credit card autofill checkbox");
+
+ await SpecialPowers.spawn(browser, [SELECTORS], selectors => {
+ content.document
+ .querySelector(selectors.creditCardAutofillCheckbox)
+ .scrollIntoView({ block: "center", behavior: "instant" });
+ });
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ SELECTORS.creditCardAutofillCheckbox,
+ {},
+ browser
+ );
+ is(
+ Services.prefs.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF),
+ false,
+ "Check credit card autofill is now disabled"
+ );
+ is(
+ Services.prefs.getCharPref(AUTOFILL_ADDRESSES_AVAILABLE_PREF),
+ addressAutofillAvailablePrefValue,
+ "Address autofill availability should not change due to interaction with credit card checkbox"
+ );
+ is(
+ addressAutofillEnabledPrefValue,
+ Services.prefs.getBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF),
+ "Address autofill enabled pref should not change due to credit card checkbox"
+ );
+ await SpecialPowers.spawn(browser, [SELECTORS], selectors => {
+ is(
+ content.document.querySelector(selectors.addressAutofillCheckbox),
+ null,
+ "Address checkbox should not exist due to address autofill not being available"
+ );
+ });
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Checkboxes should be disabled based on whether or not they are locked.
+add_task(async function test_aboutPreferencesPrivacy() {
+ Services.prefs.lockPref(ENABLED_AUTOFILL_ADDRESSES_PREF);
+ Services.prefs.lockPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
+ registerCleanupFunction(function () {
+ Services.prefs.unlockPref(ENABLED_AUTOFILL_ADDRESSES_PREF);
+ Services.prefs.unlockPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
+ });
+ let finalPrefPaneLoaded = TestUtils.topicObserved(
+ "sync-pane-loaded",
+ () => true
+ );
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ await SpecialPowers.spawn(browser, [SELECTORS], selectors => {
+ is(
+ content.document.querySelector(selectors.addressAutofillCheckbox)
+ .disabled,
+ true,
+ "Autofill addresses checkbox should be disabled"
+ );
+ is(
+ content.document.querySelector(selectors.creditCardAutofillCheckbox)
+ .disabled,
+ true,
+ "Autofill credit cards checkbox should be disabled"
+ );
+ });
+ }
+ );
+});
diff --git a/browser/extensions/formautofill/test/browser/browser_remoteiframe.js b/browser/extensions/formautofill/test/browser/browser_remoteiframe.js
new file mode 100644
index 0000000000..910cfdc2d0
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_remoteiframe.js
@@ -0,0 +1,129 @@
+"use strict";
+
+const IFRAME_URL_PATH = BASE_URL + "autocomplete_iframe.html";
+
+// Start by adding a few addresses to storage.
+add_task(async function setup_storage() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [AUTOFILL_ADDRESSES_AVAILABLE_PREF, "on"],
+ [ENABLED_AUTOFILL_ADDRESSES_PREF, true],
+ [ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF, true],
+ // set capture required fields to empty to make testcase simpler
+ ["extensions.formautofill.addresses.capture.requiredFields", ""],
+ ],
+ });
+ await setStorage(TEST_ADDRESS_2, TEST_ADDRESS_4, TEST_ADDRESS_5);
+});
+
+// Verify that form fillin works in a remote iframe, and that changing
+// a field updates storage.
+add_task(async function test_iframe_autocomplete() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ IFRAME_URL_PATH,
+ true
+ );
+ let browser = tab.linkedBrowser;
+ let iframeBC = browser.browsingContext.children[1];
+ await openPopupOnSubframe(browser, iframeBC, "#street-address");
+
+ // Highlight the first item in the list. We want to verify
+ // that the warning text is correct to ensure that the preview is
+ // performed properly.
+
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, iframeBC);
+ await expectWarningText(browser, "Autofills address");
+
+ // Highlight and select the second item in the list
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, iframeBC);
+ await expectWarningText(browser, "Also autofills organization, email");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ let onLoaded = BrowserTestUtils.browserLoaded(browser, true);
+ await SpecialPowers.spawn(iframeBC, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ content.document.getElementById("street-address").value ==
+ "32 Vassar Street MIT Room 32-G524" &&
+ content.document.getElementById("country").value == "US" &&
+ content.document.getElementById("organization").value ==
+ "World Wide Web Consortium"
+ );
+ });
+ });
+
+ const onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(iframeBC, {
+ focusSelector: "#organization",
+ newValues: {
+ "#tel": "+16172535702",
+ },
+ });
+ await onPopupShown;
+ await onLoaded;
+
+ let onUpdated = waitForStorageChangedEvents("update");
+ await clickDoorhangerButton(MAIN_BUTTON);
+ await onUpdated;
+
+ // Check that the tel number was updated properly.
+ let addresses = await getAddresses();
+ is(addresses.length, 3, "Still 3 address in storage");
+ is(addresses[1].tel, "+16172535702", "Verify the tel field");
+
+ // Fill in the details again and then clear the form from the dropdown.
+ await openPopupOnSubframe(browser, iframeBC, "#street-address");
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, iframeBC);
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ await waitForAutofill(iframeBC, "#tel", "+16172535702");
+
+ // Open the dropdown and select the Clear Form item.
+ await openPopupOnSubframe(browser, iframeBC, "#street-address");
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, iframeBC);
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ await SpecialPowers.spawn(iframeBC, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ content.document.getElementById("street-address").value == "" &&
+ content.document.getElementById("country").value == "" &&
+ content.document.getElementById("organization").value == ""
+ );
+ });
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// Choose preferences from the autocomplete dropdown within an iframe.
+add_task(async function test_iframe_autocomplete_preferences() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ IFRAME_URL_PATH,
+ true
+ );
+ let browser = tab.linkedBrowser;
+ let iframeBC = browser.browsingContext.children[1];
+ await openPopupOnSubframe(browser, iframeBC, "#organization");
+
+ await expectWarningText(browser, "Also autofills address, phone, email");
+
+ const prefTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ PRIVACY_PREF_URL
+ );
+
+ // Select the preferences item.
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ info(`expecting tab: about:preferences#privacy opened`);
+ const prefTab = await prefTabPromise;
+ info(`expecting tab: about:preferences#privacy removed`);
+ BrowserTestUtils.removeTab(prefTab);
+
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/extensions/formautofill/test/browser/browser_submission_in_private_mode.js b/browser/extensions/formautofill/test/browser/browser_submission_in_private_mode.js
new file mode 100644
index 0000000000..05adf40935
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/browser_submission_in_private_mode.js
@@ -0,0 +1,37 @@
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.addresses.capture.enabled", true],
+ ["extensions.formautofill.addresses.supported", "on"],
+ ],
+ });
+});
+
+add_task(async function test_add_address() {
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ let addresses = await getAddresses();
+
+ is(addresses.length, 0, "No address in storage");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: privateWin.gBrowser, url: FORM_URL },
+ async function (privateBrowser) {
+ await focusUpdateSubmitForm(privateBrowser, {
+ focusSelector: "#organization",
+ newValues: {
+ "#organization": "Mozilla",
+ "#street-address": "331 E. Evelyn Avenue",
+ "#tel": "1-650-903-0800",
+ },
+ });
+ }
+ );
+
+ await ensureNoAddressSaved();
+
+ await BrowserTestUtils.closeWindow(privateWin);
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser.toml b/browser/extensions/formautofill/test/browser/creditCard/browser.toml
new file mode 100644
index 0000000000..710cdbafb4
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser.toml
@@ -0,0 +1,121 @@
+[DEFAULT]
+prefs = [
+ "extensions.formautofill.creditCards.enabled=true",
+ "extensions.formautofill.reauth.enabled=true",
+ "toolkit.telemetry.ipcBatchTimeout=0", # lower the interval for event telemetry in the content process to update the parent process
+]
+support-files = [
+ "../head.js",
+ "!/browser/extensions/formautofill/test/fixtures/autocomplete_basic.html",
+ "../../fixtures/autocomplete_creditcard_basic.html",
+ "../../fixtures/autocomplete_creditcard_iframe.html",
+ "../../fixtures/autocomplete_creditcard_cc_exp_field.html",
+ "../../fixtures/capture_creditCard_on_page_navigation.html",
+ "../../fixtures/without_autocomplete_creditcard_basic.html",
+
+ "head_cc.js",
+]
+
+["browser_anti_clickjacking.js"]
+skip-if = ["!debug && os == 'mac'"] # perma-fail see Bug 1600059
+
+["browser_creditCard_capture_form_removal.js"]
+
+["browser_creditCard_capture_page_navigation.js"]
+
+["browser_creditCard_doorhanger_action.js"]
+skip-if = [
+ "!debug && os == 'mac'", # perma-fail see Bug 1655601
+ "os == 'win' && ccov", # Bug 1655600
+]
+
+["browser_creditCard_doorhanger_display.js"]
+skip-if = [
+ "!debug && os == 'mac'", # perma-fail see Bug 1655601
+ "os == 'win' && ccov", # Bug 1655600
+]
+
+["browser_creditCard_doorhanger_fields.js"]
+skip-if = [
+ "!debug && os == 'mac'", # perma-fail see Bug 1655601
+ "os == 'win' && ccov", # Bug 1655600
+]
+
+["browser_creditCard_doorhanger_iframe.js"]
+skip-if = [
+ "!debug && os == 'mac'", # perma-fail see Bug 1655601
+ "os == 'win' && ccov", # Bug 1655600
+]
+
+["browser_creditCard_doorhanger_logo.js"]
+skip-if = [
+ "!debug && os == 'mac'", # perma-fail see Bug 1655601
+ "os == 'win' && ccov", # Bug 1655600
+]
+
+["browser_creditCard_doorhanger_not_shown.js"]
+skip-if = [
+ "!debug && os == 'mac'", # perma-fail see Bug 1655601
+ "os == 'win' && ccov", # Bug 1655600
+]
+
+["browser_creditCard_doorhanger_sync.js"]
+skip-if = [
+ "!debug && os == 'mac'", # perma-fail see Bug 1655601
+ "os == 'win' && ccov", # Bug 1655600
+]
+
+["browser_creditCard_dropdown_layout.js"]
+skip-if = [
+ "os == 'mac'",
+ "os == 'linux'",
+ "os == 'win'",
+]
+
+["browser_creditCard_fill_cancel_login.js"]
+skip-if = [
+ "!debug && os == 'mac'",
+ "os == 'linux'",
+ "os == 'win'",
+]
+
+["browser_creditCard_heuristics.js"]
+skip-if = ["apple_silicon && !debug"] # Bug 1714221
+
+["browser_creditCard_heuristics_autofill_name.js"]
+skip-if = ["apple_silicon && !debug"] # Bug 1714221
+
+["browser_creditCard_heuristics_cc_type.js"]
+skip-if = ["apple_silicon && !debug"] # Bug 1714221
+
+["browser_creditCard_submission_autodetect_type.js"]
+skip-if = ["apple_silicon && !debug"]
+
+["browser_creditCard_submission_normalized.js"]
+skip-if = ["apple_silicon && !debug"]
+
+["browser_creditCard_telemetry.js"]
+skip-if = [
+ "apple_silicon && !debug", # Bug 1714221
+]
+
+["browser_editCreditCardDialog.js"]
+skip-if = [
+ "os == 'mac'", # perma-fail see Bug 1600059
+ "os == 'linux'", # perma-fail see Bug 1600059
+ "os == 'win'", # perma-fail see Bug 1600059
+]
+
+["browser_insecure_form.js"]
+skip-if = [
+ "os == 'mac'", # bug 1456284
+ "os == 'linux'", # bug 1456284
+ "os == 'win'", # bug 1456284
+]
+
+["browser_manageCreditCardsDialog.js"]
+skip-if = [
+ "os == 'mac'",
+ "os == 'linux'",
+ "os == 'win'",
+]
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js b/browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js
new file mode 100644
index 0000000000..f7fc731e54
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js
@@ -0,0 +1,127 @@
+"use strict";
+
+const ADDRESS_URL =
+ "http://example.org/browser/browser/extensions/formautofill/test/browser/autocomplete_basic.html";
+const CC_URL =
+ "https://example.org/browser/browser/extensions/formautofill/test/browser/creditCard/autocomplete_creditcard_basic.html";
+
+add_task(async function setup_storage() {
+ await setStorage(
+ TEST_ADDRESS_1,
+ TEST_ADDRESS_2,
+ TEST_ADDRESS_3,
+ TEST_CREDIT_CARD_1,
+ TEST_CREDIT_CARD_2,
+ TEST_CREDIT_CARD_3
+ );
+});
+
+add_task(async function test_active_delay() {
+ // This is a workaround for the fact that we don't have a way
+ // to know when the popup was opened exactly and this makes our test
+ // racy when ensuring that we first test for disabled items before
+ // the delayed enabling happens.
+ //
+ // In the future we should consider adding an event when a popup
+ // gets opened and listen for it in this test before we check if the item
+ // is disabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.notification_enable_delay", 1000],
+ ["extensions.formautofill.reauth.enabled", false],
+ ],
+ });
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CC_URL },
+ async function (browser) {
+ const focusInput = "#cc-number";
+
+ // Open the popup -- we don't use openPopupOn() because there
+ // are things we need to check between these steps.
+ await SimpleTest.promiseFocus(browser);
+ const start = performance.now();
+ await runAndWaitForAutocompletePopupOpen(browser, async () => {
+ await focusAndWaitForFieldsIdentified(browser, focusInput);
+ });
+ const firstItem = getDisplayedPopupItems(browser)[0];
+ ok(firstItem.disabled, "Popup should be disbled upon opening.");
+ is(
+ browser.autoCompletePopup.selectedIndex,
+ -1,
+ "No item selected at first"
+ );
+
+ // Check that clicking on menu doesn't do anything while
+ // it is disabled
+ firstItem.click();
+ is(
+ browser.autoCompletePopup.selectedIndex,
+ -1,
+ "No item selected after clicking on disabled item"
+ );
+
+ // Check that the delay before enabling is as long as expected
+ await waitForPopupEnabled(browser);
+ const delta = performance.now() - start;
+ info(`Popup was disabled for ${delta} ms`);
+ Assert.greaterOrEqual(
+ delta,
+ 1000,
+ "Popup was disabled for at least 1000 ms"
+ );
+
+ // Check the clicking on the menu works now
+ firstItem.click();
+ is(
+ browser.autoCompletePopup.selectedIndex,
+ 0,
+ "First item selected after clicking on enabled item"
+ );
+
+ // Clean up
+ await closePopup(browser);
+ }
+ );
+});
+
+add_task(async function test_no_delay() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.notification_enable_delay", 1000],
+ ["extensions.formautofill.reauth.enabled", false],
+ ],
+ });
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: ADDRESS_URL },
+ async function (browser) {
+ const focusInput = "#organization";
+
+ // Open the popup -- we don't use openPopupOn() because there
+ // are things we need to check between these steps.
+ await SimpleTest.promiseFocus(browser);
+ await runAndWaitForAutocompletePopupOpen(browser, async () => {
+ await focusAndWaitForFieldsIdentified(browser, focusInput);
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ });
+ const firstItem = getDisplayedPopupItems(browser)[0];
+ ok(!firstItem.disabled, "Popup should be enabled upon opening.");
+ is(
+ browser.autoCompletePopup.selectedIndex,
+ -1,
+ "No item selected at first"
+ );
+
+ // Check that clicking on menu doesn't do anything while
+ // it is disabled
+ firstItem.click();
+ is(
+ browser.autoCompletePopup.selectedIndex,
+ 0,
+ "First item selected after clicking on enabled item"
+ );
+
+ // Clean up
+ await closePopup(browser);
+ }
+ );
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_capture_form_removal.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_capture_form_removal.js
new file mode 100644
index 0000000000..a6c76aa675
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_capture_form_removal.js
@@ -0,0 +1,119 @@
+"use strict";
+
+const CC_VALUES = {
+ "cc-name": "User",
+ "cc-number": "5577000055770004",
+ "cc-exp-month": 12,
+ "cc-exp-year": 2017,
+};
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.creditCards.supported", "on"],
+ ["extensions.formautofill.creditCards.enabled", true],
+ ["extensions.formautofill.heuristics.captureOnFormRemoval", true],
+ ],
+ });
+ await removeAllRecords();
+});
+
+/**
+ * Tests if the credit card is captured (cc doorhanger is shown) after a
+ * successful xhr or fetch request followed by a form removal and
+ * that the stored credit card record has the right values.
+ */
+add_task(async function test_credit_card_captured_after_form_removal() {
+ const onStorageChanged = waitForStorageChangedEvents("add");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ const onPopupShown = waitForPopupShown();
+ info("Update identified credit card fields");
+ // We don't submit the form
+ await focusUpdateSubmitForm(
+ browser,
+ {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": CC_VALUES["cc-name"],
+ "#cc-number": CC_VALUES["cc-number"],
+ "#cc-exp-month": CC_VALUES["cc-exp-month"],
+ "#cc-exp-year": CC_VALUES["cc-exp-year"],
+ },
+ },
+ 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 credit card doorhanger");
+ await onPopupShown;
+
+ info("Click Save in credit card doorhanger");
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+
+ info("Wait for the credit card to be added to the storage.");
+ await onStorageChanged;
+
+ const storedCreditCards = await getCreditCards();
+ let actualCreditCard = storedCreditCards[0];
+ actualCreditCard["cc-number"] = await OSKeyStore.decrypt(
+ actualCreditCard["cc-number-encrypted"]
+ );
+
+ for (let key in CC_VALUES) {
+ let expected = CC_VALUES[key];
+ let actual = actualCreditCard[key];
+ is(expected, actual, `${key} should be equal`);
+ }
+ await removeAllRecords();
+});
+
+/**
+ * Tests that the credit card is not captured without a prior fetch or xhr request event
+ */
+add_task(async function test_credit_card_not_captured_without_prior_fetch() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ info("Update identified credit card fields");
+ // We don't submit the form
+ await focusUpdateSubmitForm(
+ browser,
+ {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": CC_VALUES["cc-name"],
+ "#cc-number": CC_VALUES["cc-number"],
+ "#cc-exp-month": CC_VALUES["cc-exp-month"],
+ "#cc-exp-year": CC_VALUES["cc-exp-year"],
+ },
+ },
+ false
+ );
+
+ info("Infer form removal");
+ await SpecialPowers.spawn(browser, [], async function () {
+ let form = content.document.getElementById("form");
+ form.parentNode.remove(form);
+ });
+
+ info("Ensure that credit card doorhanger is not shown");
+ await ensureNoDoorhanger(browser);
+ }
+ );
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_capture_page_navigation.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_capture_page_navigation.js
new file mode 100644
index 0000000000..77bb3e8f97
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_capture_page_navigation.js
@@ -0,0 +1,92 @@
+"use strict";
+
+const CC_VALUES = {
+ "#cc-name": "User",
+ "#cc-number": "5577000055770004",
+ "#cc-exp-month": 12,
+ "#cc-exp-year": 2017,
+};
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.creditCards.supported", "on"],
+ ["extensions.formautofill.creditCards.enabled", true],
+ ["extensions.formautofill.heuristics.captureOnPageNavigation", true],
+ ],
+ });
+});
+
+/**
+ * Tests if the credit card is captured (cc doorhanger is shown)
+ * after adding an entry to the browser's session history stack
+ */
+add_task(
+ async function test_creditCard_captured_after_changing_request_state() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_WITH_PAGE_NAVIGATION_BUTTONS },
+ async function (browser) {
+ const onPopupShown = waitForPopupShown();
+ info("Update identified credit card fields");
+ // We don't submit the form
+ await focusUpdateSubmitForm(
+ browser,
+ {
+ focusSelector: "#cc-name",
+ newValues: CC_VALUES,
+ },
+ false
+ );
+
+ info("Change request state");
+ await SpecialPowers.spawn(browser, [], () => {
+ const historyPushStateButton =
+ content.document.getElementById("historyPushState");
+ historyPushStateButton.click();
+ });
+
+ info("Wait for credit card doorhanger");
+ await onPopupShown;
+
+ ok(true, "Credit card doorhanger is shown");
+ }
+ );
+ }
+);
+
+/**
+ * Tests if the credit card is captured (cc doorhanger is shown) after a
+ * after navigating by opening another resource
+ */
+add_task(
+ async function test_creditCard_captured_after_navigation_same_window() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_WITH_PAGE_NAVIGATION_BUTTONS },
+ async function (browser) {
+ const onPopupShown = waitForPopupShown();
+ info("Update identified credit card fields");
+ // We don't submit the form
+ await focusUpdateSubmitForm(
+ browser,
+ {
+ focusSelector: "#cc-name",
+ newValues: CC_VALUES,
+ },
+ false
+ );
+
+ info("Change window location");
+ await SpecialPowers.spawn(browser, [], () => {
+ const windowLocationButton =
+ content.document.getElementById("windowLocation");
+ windowLocationButton.click();
+ });
+
+ info("Wait for credit card doorhanger");
+ await onPopupShown;
+
+ ok(true, "Credit card doorhanger is shown");
+ }
+ );
+ }
+);
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js
new file mode 100644
index 0000000000..82122925d7
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js
@@ -0,0 +1,170 @@
+"use strict";
+
+add_task(async function test_save_doorhanger_click_save() {
+ let onChanged = waitForStorageChangedEvents("add");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 1",
+ "#cc-number": "5577000055770004",
+ "#cc-exp-month": "12",
+ "#cc-exp-year": "2017",
+ "#cc-type": "mastercard",
+ },
+ });
+
+ await onPopupShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+ await onChanged;
+
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+ is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
+ is(creditCards[0]["cc-type"], "mastercard", "Verify the cc-type field");
+ await removeAllRecords();
+});
+
+add_task(async function test_save_doorhanger_click_never_save() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 0",
+ "#cc-number": "6387060366272981",
+ },
+ });
+
+ await onPopupShown;
+ await clickDoorhangerButton(MENU_BUTTON, 0);
+ }
+ );
+
+ await sleep(1000);
+ let creditCards = await getCreditCards();
+ let creditCardPref = SpecialPowers.getBoolPref(
+ ENABLED_AUTOFILL_CREDITCARDS_PREF
+ );
+ is(creditCards.length, 0, "No credit card in storage");
+ is(creditCardPref, false, "Credit card is disabled");
+ SpecialPowers.clearUserPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
+});
+
+add_task(async function test_save_doorhanger_click_cancel_save() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 1",
+ "#cc-number": "5038146897157463",
+ },
+ });
+
+ ok(
+ !SpecialPowers.Services.prefs.prefHasUserValue(SYNC_USERNAME_PREF),
+ "Sync account should not exist by default"
+ );
+ await onPopupShown;
+ let cb = getDoorhangerCheckbox();
+ ok(cb.hidden, "Sync checkbox should be hidden");
+ await clickDoorhangerButton(SECONDARY_BUTTON);
+ }
+ );
+
+ await sleep(1000);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 0, "No credit card saved");
+});
+
+add_task(async function test_update_doorhanger_click_update() {
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+
+ let onChanged = waitForStorageChangedEvents("update", "notifyUsed");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "Mark Smith",
+ "#cc-number": "4111111111111111",
+ "#cc-exp-month": "4",
+ "#cc-exp-year": new Date().getFullYear(),
+ },
+ });
+
+ await onPopupShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+ await onChanged;
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 1, "Still 1 credit card in storage");
+ is(creditCards[0]["cc-name"], "Mark Smith", "name field got updated");
+ await removeAllRecords();
+});
+
+add_task(async function test_update_doorhanger_click_save() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+ let onChanged = waitForStorageChangedEvents("add");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let osKeyStoreLoginShown =
+ OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let onPopupShown = waitForPopupShown();
+ await openPopupOn(browser, "form #cc-name");
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ await waitForAutofill(browser, "#cc-name", "John Doe");
+
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 1",
+ },
+ });
+
+ await onPopupShown;
+ await clickDoorhangerButton(SECONDARY_BUTTON);
+ await osKeyStoreLoginShown;
+ }
+ );
+ await onChanged;
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 2, "2 credit cards in storage");
+ is(
+ creditCards[0]["cc-name"],
+ TEST_CREDIT_CARD_1["cc-name"],
+ "Original record's cc-name field is unchanged"
+ );
+ is(creditCards[1]["cc-name"], "User 1", "cc-name field in the new record");
+ await removeAllRecords();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js
new file mode 100644
index 0000000000..715eceb3eb
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js
@@ -0,0 +1,311 @@
+"use strict";
+
+add_task(async function test_save_doorhanger_shown_no_profile() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 1",
+ "#cc-number": "5577000055770004",
+ "#cc-exp-month": "12",
+ "#cc-exp-year": "2017",
+ "#cc-type": "mastercard",
+ },
+ });
+
+ await onPopupShown;
+ }
+ );
+});
+
+add_task(async function test_save_doorhanger_shown_different_card_number() {
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-number": TEST_CREDIT_CARD_2["cc-number"],
+ },
+ });
+
+ await onPopupShown;
+ }
+ );
+ await removeAllRecords();
+});
+
+add_task(async function test_update_doorhanger_shown_different_card_name() {
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "Mark Smith",
+ "#cc-number": TEST_CREDIT_CARD_1["cc-number"],
+ "#cc-exp-month": TEST_CREDIT_CARD_1["cc-exp-month"],
+ "#cc-exp-year": TEST_CREDIT_CARD_1["cc-exp-year"],
+ },
+ });
+
+ await onPopupShown;
+ }
+ );
+ await removeAllRecords();
+});
+
+add_task(async function test_update_doorhanger_shown_different_card_expiry() {
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": TEST_CREDIT_CARD_1["cc-name"],
+ "#cc-number": TEST_CREDIT_CARD_1["cc-number"],
+ "#cc-exp-month": "12",
+ "#cc-exp-year": "2099",
+ },
+ });
+
+ await onPopupShown;
+ }
+ );
+ await removeAllRecords();
+});
+
+add_task(async function test_doorhanger_not_shown_when_autofill_untouched() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+
+ let onUsed = waitForStorageChangedEvents("notifyUsed");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let osKeyStoreLoginShown =
+ OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await openPopupOn(browser, "form #cc-name");
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ await osKeyStoreLoginShown;
+ await waitForAutofill(browser, "#cc-name", "John Doe");
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let form = content.document.getElementById("form");
+ form.querySelector("input[type=submit]").click();
+ });
+
+ await sleep(1000);
+ is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
+ }
+ );
+ await onUsed;
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 1, "Still 1 credit card");
+ is(creditCards[0].timesUsed, 1, "timesUsed field set to 1");
+ await removeAllRecords();
+});
+
+add_task(async function test_doorhanger_not_shown_when_fill_duplicate() {
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+
+ let onUsed = waitForStorageChangedEvents("notifyUsed");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "John Doe",
+ "#cc-number": "4111111111111111",
+ "#cc-exp-month": "4",
+ "#cc-exp-year": new Date().getFullYear(),
+ "#cc-type": "visa",
+ },
+ });
+
+ await sleep(1000);
+ is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
+ }
+ );
+ await onUsed;
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 1, "Still 1 credit card in storage");
+ is(
+ creditCards[0]["cc-name"],
+ TEST_CREDIT_CARD_1["cc-name"],
+ "Verify the name field"
+ );
+ is(creditCards[0].timesUsed, 1, "timesUsed field set to 1");
+ await removeAllRecords();
+});
+
+add_task(
+ async function test_doorhanger_not_shown_when_autofill_then_fill_everything_duplicate() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ await setStorage(TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 2, "2 credit card in storage");
+ let onUsed = waitForStorageChangedEvents("notifyUsed");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let osKeyStoreLoginShown =
+ OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await openPopupOn(browser, "form #cc-number");
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ await waitForAutofill(
+ browser,
+ "#cc-number",
+ TEST_CREDIT_CARD_1["cc-number"]
+ );
+
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ // Change number to the second credit card number
+ "#cc-number": TEST_CREDIT_CARD_2["cc-number"],
+ "#cc-name": TEST_CREDIT_CARD_2["cc-name"],
+ "#cc-exp-month": TEST_CREDIT_CARD_2["cc-exp-month"],
+ "#cc-exp-year": TEST_CREDIT_CARD_2["cc-exp-year"],
+ },
+ });
+
+ await sleep(1000);
+ is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
+ await osKeyStoreLoginShown;
+ }
+ );
+ await onUsed;
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 2, "Still 2 credit card");
+ await removeAllRecords();
+ }
+);
+
+add_task(
+ async function test_doorhanger_not_shown_when_autofill_then_fill_number_duplicate() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ await setStorage(TEST_CREDIT_CARD_1, {
+ ...TEST_CREDIT_CARD_1,
+ ...{ "cc-number": "5105105105105100" },
+ });
+
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 2, "2 credit card in storage");
+ let onUsed = waitForStorageChangedEvents("notifyUsed");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let osKeyStoreLoginShown =
+ OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await openPopupOn(browser, "form #cc-number");
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ await waitForAutofill(
+ browser,
+ "#cc-number",
+ TEST_CREDIT_CARD_1["cc-number"]
+ );
+
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ // Change number to the second credit card number
+ "#cc-number": "5105105105105100",
+ },
+ });
+
+ await sleep(1000);
+ is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
+ await osKeyStoreLoginShown;
+ }
+ );
+ await onUsed;
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 2, "Still 2 credit card");
+ await removeAllRecords();
+ }
+);
+
+add_task(async function test_update_doorhanger_shown_when_fill_mergeable() {
+ await setStorage(TEST_CREDIT_CARD_3);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+
+ let onChanged = waitForStorageChangedEvents("update", "notifyUsed");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 3",
+ "#cc-number": "5103059495477870",
+ "#cc-exp-month": "1",
+ "#cc-exp-year": "2000",
+ },
+ });
+
+ await onPopupShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+ await onChanged;
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 1, "Still 1 credit card in storage");
+ is(creditCards[0]["cc-name"], "User 3", "Verify the name field");
+ await removeAllRecords();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js
new file mode 100644
index 0000000000..c1ebef737e
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js
@@ -0,0 +1,198 @@
+"use strict";
+
+add_task(async function test_update_autofill_name_field() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+
+ let onChanged = waitForStorageChangedEvents("update", "notifyUsed");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let osKeyStoreLoginShown =
+ OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let onPopupShown = waitForPopupShown();
+
+ await openPopupOn(browser, "form #cc-name");
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ await osKeyStoreLoginShown;
+ await waitForAutofill(browser, "#cc-name", "John Doe");
+
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 1",
+ },
+ });
+
+ await onPopupShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+ await onChanged;
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 1, "Still 1 credit card");
+ is(creditCards[0]["cc-name"], "User 1", "cc-name field is updated");
+ is(
+ creditCards[0]["cc-number"],
+ "************1111",
+ "Verify the card number field"
+ );
+ await removeAllRecords();
+});
+
+add_task(async function test_update_autofill_exp_date_field() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+ let onChanged = waitForStorageChangedEvents("update", "notifyUsed");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let osKeyStoreLoginShown =
+ OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let onPopupShown = waitForPopupShown();
+ await openPopupOn(browser, "form #cc-name");
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ await osKeyStoreLoginShown;
+ await waitForAutofill(browser, "#cc-name", "John Doe");
+
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-exp-year": "2019",
+ },
+ });
+
+ await onPopupShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+ await onChanged;
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 1, "Still 1 credit card");
+ is(creditCards[0]["cc-exp-year"], 2019, "cc-exp-year field is updated");
+ is(
+ creditCards[0]["cc-number"],
+ "************1111",
+ "Verify the card number field"
+ );
+ await removeAllRecords();
+});
+
+add_task(async function test_submit_unnormailzed_field() {
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "John Doe",
+ "#cc-number": "4111111111111111",
+ "#cc-exp-month": "4",
+ "#cc-exp-year": new Date().getFullYear().toString().substr(2, 2),
+ "#cc-type": "visa",
+ },
+ });
+
+ await sleep(1000);
+ is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
+ }
+ );
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 1, "Still 1 credit card in storage");
+ is(
+ creditCards[0]["cc-exp-year"],
+ new Date().getFullYear(),
+ "Verify the expiry year field"
+ );
+ await removeAllRecords();
+});
+
+add_task(async function test_submit_invalid_network_field() {
+ let onChanged = waitForStorageChangedEvents("add");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 1",
+ "#cc-number": "5038146897157463",
+ "#cc-exp-month": "12",
+ "#cc-exp-year": "2017",
+ "#cc-type": "gringotts",
+ },
+ });
+
+ await onPopupShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+ await onChanged;
+
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+ is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
+ is(
+ creditCards[0]["cc-type"],
+ undefined,
+ "Invalid network/cc-type was not saved"
+ );
+
+ await removeAllRecords();
+});
+
+add_task(async function test_submit_combined_expiry_field() {
+ let onChanged = waitForStorageChangedEvents("add");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_COMBINED_EXPIRY_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "John Doe",
+ "#cc-number": "374542158116607",
+ "#cc-exp": "05/28",
+ },
+ });
+ await onPopupShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+ await onChanged;
+
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "Card should be added");
+ is(creditCards[0]["cc-exp"], "2028-05", "Verify cc-exp field");
+ is(creditCards[0]["cc-exp-month"], 5, "Verify cc-exp-month field");
+ is(creditCards[0]["cc-exp-year"], 2028, "Verify cc-exp-year field");
+ await removeAllRecords();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js
new file mode 100644
index 0000000000..2781e5acf6
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js
@@ -0,0 +1,103 @@
+"use strict";
+
+add_task(async function test_iframe_submit_untouched_creditCard_form() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+
+ // This test triggers two form submission events so cc 'timesUsed' count is 2.
+ // The first submission is triggered by standard form submission, and the
+ // second is triggered by page hiding.
+ const EXPECTED_ON_USED_COUNT = 2;
+ let notifyUsedCounter = EXPECTED_ON_USED_COUNT;
+ let onUsed = TestUtils.topicObserved(
+ "formautofill-storage-changed",
+ (subject, data) => {
+ if (data == "notifyUsed") {
+ notifyUsedCounter--;
+ }
+ return notifyUsedCounter == 0;
+ }
+ );
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_IFRAME_URL },
+ async function (browser) {
+ let osKeyStoreLoginShown =
+ OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let iframeBC = browser.browsingContext.children[0];
+ await openPopupOnSubframe(browser, iframeBC, "form #cc-name");
+
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ await osKeyStoreLoginShown;
+ await waitForAutofill(iframeBC, "#cc-name", "John Doe");
+
+ await SpecialPowers.spawn(iframeBC, [], async function () {
+ let form = content.document.getElementById("form");
+ form.querySelector("input[type=submit]").click();
+ });
+
+ await sleep(1000);
+ is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
+ }
+ );
+ await onUsed;
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 1, "Still 1 credit card");
+ is(
+ creditCards[0].timesUsed,
+ EXPECTED_ON_USED_COUNT,
+ "timesUsed field set to 2"
+ );
+ await removeAllRecords();
+});
+
+add_task(async function test_iframe_unload_save_card() {
+ let onChanged = waitForStorageChangedEvents("add");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_IFRAME_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ let iframeBC = browser.browsingContext.children[0];
+ await focusUpdateSubmitForm(
+ iframeBC,
+ {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 1",
+ "#cc-number": "4556194630960970",
+ "#cc-exp-month": "10",
+ "#cc-exp-year": "2024",
+ "#cc-type": "visa",
+ },
+ },
+ false
+ );
+
+ info("Removing iframe without submitting");
+ await SpecialPowers.spawn(browser, [], async function () {
+ let frame = content.document.querySelector("iframe");
+ frame.remove();
+ });
+
+ await onPopupShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+ await onChanged;
+
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+ is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
+ is(creditCards[0]["cc-type"], "visa", "Verify the cc-type field");
+ await removeAllRecords();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_logo.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_logo.js
new file mode 100644
index 0000000000..9c4f4e4825
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_logo.js
@@ -0,0 +1,238 @@
+"use strict";
+
+/*
+ The next four tests look very similar because if we try to do multiple
+ credit card operations in one test, there's a good chance the test will timeout
+ and produce an invalid result.
+ We mitigate this issue by having each test only deal with one credit card in storage
+ and one credit card operation.
+*/
+add_task(async function test_submit_third_party_creditCard_logo() {
+ const amexCard = {
+ "cc-number": "374542158116607",
+ "cc-type": "amex",
+ "cc-name": "John Doe",
+ };
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 1",
+ "#cc-number": amexCard["cc-number"],
+ },
+ });
+
+ await onPopupShown;
+ let doorhanger = getNotification();
+ let creditCardLogo = doorhanger.querySelector(".desc-message-box image");
+ let creditCardLogoWithoutExtension = creditCardLogo.src.split(".", 1)[0];
+
+ is(
+ creditCardLogoWithoutExtension,
+ "chrome://formautofill/content/third-party/cc-logo-amex",
+ "CC logo should be amex"
+ );
+
+ await clickDoorhangerButton(SECONDARY_BUTTON);
+ }
+ );
+ await removeAllRecords();
+});
+
+add_task(async function test_update_third_party_creditCard_logo() {
+ const amexCard = {
+ "cc-number": "374542158116607",
+ "cc-name": "John Doe",
+ };
+
+ await setStorage(amexCard);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+
+ let onChanged = waitForStorageChangedEvents("update");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "Mark Smith",
+ "#cc-number": amexCard["cc-number"],
+ "#cc-exp-month": "4",
+ "#cc-exp-year": new Date().getFullYear(),
+ },
+ });
+
+ await onPopupShown;
+
+ let doorhanger = getNotification();
+ let creditCardLogo = doorhanger.querySelector(".desc-message-box image");
+ let creditCardLogoWithoutExtension = creditCardLogo.src.split(".", 1)[0];
+ is(
+ creditCardLogoWithoutExtension,
+ `chrome://formautofill/content/third-party/cc-logo-amex`,
+ `CC Logo should be amex`
+ );
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+ await onChanged;
+ await removeAllRecords();
+});
+
+add_task(async function test_submit_generic_creditCard_logo() {
+ const genericCard = {
+ "cc-number": "937899583135",
+ "cc-name": "John Doe",
+ };
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 1",
+ "#cc-number": genericCard["cc-number"],
+ },
+ });
+
+ await onPopupShown;
+ let doorhanger = getNotification();
+ let creditCardLogo = doorhanger.querySelector(".desc-message-box image");
+ let creditCardLogoWithoutExtension = creditCardLogo.src.split(".", 1)[0];
+
+ is(
+ creditCardLogoWithoutExtension,
+ "chrome://formautofill/content/icon-credit-card-generic",
+ "CC logo should be generic"
+ );
+
+ await clickDoorhangerButton(SECONDARY_BUTTON);
+ }
+ );
+ await removeAllRecords();
+});
+
+add_task(async function test_update_generic_creditCard_logo() {
+ const genericCard = {
+ "cc-number": "937899583135",
+ "cc-name": "John Doe",
+ };
+
+ await setStorage(genericCard);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+
+ let onChanged = waitForStorageChangedEvents("update");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "Mark Smith",
+ "#cc-number": genericCard["cc-number"],
+ "#cc-exp-month": "4",
+ "#cc-exp-year": new Date().getFullYear(),
+ },
+ });
+
+ await onPopupShown;
+
+ let doorhanger = getNotification();
+ let creditCardLogo = doorhanger.querySelector(".desc-message-box image");
+ let creditCardLogoWithoutExtension = creditCardLogo.src.split(".", 1)[0];
+ is(
+ creditCardLogoWithoutExtension,
+ `chrome://formautofill/content/icon-credit-card-generic`,
+ `CC Logo should be generic`
+ );
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+ await onChanged;
+ await removeAllRecords();
+});
+
+add_task(async function test_save_panel_spaces_in_cc_number_logo() {
+ const amexCard = {
+ "cc-number": "37 4542 158116 607",
+ "cc-type": "amex",
+ "cc-name": "John Doe",
+ };
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-number": amexCard["cc-number"],
+ },
+ });
+
+ await onPopupShown;
+ let doorhanger = getNotification();
+ let creditCardLogo = doorhanger.querySelector(".desc-message-box image");
+ let creditCardLogoWithoutExtension = creditCardLogo.src.split(".", 1)[0];
+
+ is(
+ creditCardLogoWithoutExtension,
+ "chrome://formautofill/content/third-party/cc-logo-amex",
+ "CC logo should be amex"
+ );
+
+ await clickDoorhangerButton(SECONDARY_BUTTON);
+ }
+ );
+});
+
+add_task(async function test_update_panel_with_spaces_in_cc_number_logo() {
+ const amexCard = {
+ "cc-number": "374 54215 8116607",
+ "cc-name": "John Doe",
+ };
+
+ await setStorage(amexCard);
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+
+ let onChanged = waitForStorageChangedEvents("update", "notifyUsed");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "Mark Smith",
+ "#cc-number": amexCard["cc-number"],
+ "#cc-exp-month": "4",
+ "#cc-exp-year": new Date().getFullYear(),
+ },
+ });
+
+ await onPopupShown;
+
+ let doorhanger = getNotification();
+ let creditCardLogo = doorhanger.querySelector(".desc-message-box image");
+ let creditCardLogoWithoutExtension = creditCardLogo.src.split(".", 1)[0];
+ is(
+ creditCardLogoWithoutExtension,
+ `chrome://formautofill/content/third-party/cc-logo-amex`,
+ `CC Logo should be amex`
+ );
+ // We are not testing whether the cc-number is saved correctly,
+ // we only care that the logo in the update panel shows correctly.
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+ await onChanged;
+ await removeAllRecords();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_not_shown.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_not_shown.js
new file mode 100644
index 0000000000..f0752e0c22
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_not_shown.js
@@ -0,0 +1,92 @@
+"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 TESTCASES = [
+ {
+ description: "Should not trigger credit card saving if number is empty",
+ document: DEFAULT_TEST_DOC,
+ targetElementId: "cc-name",
+ formValue: {
+ "#cc-name": "John Doe",
+ "#cc-exp-month": 12,
+ "#cc-exp-year": 2000,
+ },
+ },
+ {
+ description:
+ "Should not trigger credit card saving if there is more than one cc-number field but less than four fields",
+ document: `<form id="form">
+ <input id="cc-type" autocomplete="cc-type">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-number1" maxlength="4">
+ <input id="cc-number2" maxlength="4">
+ <input id="cc-number3" maxlength="4">
+ <input id="cc-exp-month" autocomplete="cc-exp-month">
+ <input id="cc-exp-year" autocomplete="cc-exp-year">
+ <input id="submit" type="submit">
+ </form>
+ `,
+ targetElementId: "cc-name",
+ formValue: {
+ "#cc-name": "John Doe",
+ "#cc-number1": "3714",
+ "#cc-number2": "4963",
+ "#cc-number3": "5398",
+ "#cc-exp-month": 12,
+ "#cc-exp-year": 2000,
+ "#cc-type": "amex",
+ },
+ },
+];
+
+add_task(async function test_save_doorhanger_not_shown() {
+ for (const TEST of TESTCASES) {
+ info(`Test ${TEST.description}`);
+
+ 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);
+ });
+ }
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_sync.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_sync.js
new file mode 100644
index 0000000000..2064928820
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_sync.js
@@ -0,0 +1,117 @@
+"use strict";
+
+add_task(async function test_submit_creditCard_with_sync_account() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [SYNC_USERNAME_PREF, "foo@bar.com"],
+ [SYNC_CREDITCARDS_AVAILABLE_PREF, true],
+ [ENABLED_AUTOFILL_CREDITCARDS_PREF, true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 2",
+ "#cc-number": "6387060366272981",
+ },
+ });
+
+ await onPopupShown;
+ let cb = getDoorhangerCheckbox();
+ ok(!cb.hidden, "Sync checkbox should be visible");
+ is(
+ SpecialPowers.getBoolPref(SYNC_CREDITCARDS_PREF),
+ false,
+ "creditCards sync should be disabled by default"
+ );
+
+ // Verify if the checkbox and button state is changed.
+ let secondaryButton = getDoorhangerButton(SECONDARY_BUTTON);
+ let menuButton = getDoorhangerButton(MENU_BUTTON);
+ is(
+ cb.checked,
+ false,
+ "Checkbox state should match creditCards sync state"
+ );
+ is(
+ secondaryButton.disabled,
+ false,
+ "Not saving button should be enabled"
+ );
+ is(
+ menuButton.disabled,
+ false,
+ "Never saving menu button should be enabled"
+ );
+ // Click the checkbox to enable credit card sync.
+ cb.click();
+ is(
+ SpecialPowers.getBoolPref(SYNC_CREDITCARDS_PREF),
+ true,
+ "creditCards sync should be enabled after checked"
+ );
+ is(
+ secondaryButton.disabled,
+ true,
+ "Not saving button should be disabled"
+ );
+ is(
+ menuButton.disabled,
+ true,
+ "Never saving menu button should be disabled"
+ );
+ // Click the checkbox again to disable credit card sync.
+ cb.click();
+ is(
+ SpecialPowers.getBoolPref(SYNC_CREDITCARDS_PREF),
+ false,
+ "creditCards sync should be disabled after unchecked"
+ );
+ is(
+ secondaryButton.disabled,
+ false,
+ "Not saving button should be enabled again"
+ );
+ is(
+ menuButton.disabled,
+ false,
+ "Never saving menu button should be enabled again"
+ );
+ await clickDoorhangerButton(SECONDARY_BUTTON);
+ }
+ );
+});
+
+add_task(async function test_submit_creditCard_with_synced_already() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [SYNC_CREDITCARDS_PREF, true],
+ [SYNC_USERNAME_PREF, "foo@bar.com"],
+ [SYNC_CREDITCARDS_AVAILABLE_PREF, true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 2",
+ "#cc-number": "6387060366272981",
+ },
+ });
+
+ await onPopupShown;
+ let cb = getDoorhangerCheckbox();
+ ok(cb.hidden, "Sync checkbox should be hidden");
+ await clickDoorhangerButton(SECONDARY_BUTTON);
+ }
+ );
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js
new file mode 100644
index 0000000000..2b1fb9043c
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js
@@ -0,0 +1,97 @@
+"use strict";
+
+const { CreditCard } = ChromeUtils.importESModule(
+ "resource://gre/modules/CreditCard.sys.mjs"
+);
+
+const CC_URL =
+ "https://example.org/browser/browser/extensions/formautofill/test/browser/creditCard/autocomplete_creditcard_basic.html";
+
+const TEST_CREDIT_CARDS = [
+ TEST_CREDIT_CARD_1,
+ TEST_CREDIT_CARD_2,
+ TEST_CREDIT_CARD_3,
+];
+
+add_task(async function setup_storage() {
+ await setStorage(...TEST_CREDIT_CARDS);
+});
+
+async function reopenPopupWithResizedInput(browser, selector, newSize) {
+ await closePopup(browser);
+ /* eslint no-shadow: ["error", { "allow": ["selector", "newSize"] }] */
+ await SpecialPowers.spawn(
+ browser,
+ [{ selector, newSize }],
+ async function ({ selector, newSize }) {
+ const input = content.document.querySelector(selector);
+
+ input.style.boxSizing = "border-box";
+ input.style.width = newSize + "px";
+ }
+ );
+ await openPopupOn(browser, selector);
+}
+
+add_task(async function test_credit_card_dropdown() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CC_URL },
+ async function (browser) {
+ const focusInput = "#cc-number";
+ await openPopupOn(browser, focusInput);
+ const firstItem = getDisplayedPopupItems(browser)[0];
+
+ isnot(firstItem.getAttribute("ac-image"), "", "Should show icon");
+ ok(
+ firstItem.getAttribute("aria-label").startsWith("Visa "),
+ "aria-label should start with Visa"
+ );
+
+ // The breakpoint of two-lines layout is 185px
+ await reopenPopupWithResizedInput(browser, focusInput, 175);
+ is(
+ firstItem._itemBox.getAttribute("size"),
+ "small",
+ "Show two-lines layout"
+ );
+ await reopenPopupWithResizedInput(browser, focusInput, 195);
+ is(
+ firstItem._itemBox.hasAttribute("size"),
+ false,
+ "Show one-line layout"
+ );
+
+ await closePopup(browser);
+ }
+ );
+});
+
+add_task(async function test_credit_card_dropdown_icon_invalid_types_select() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CC_URL },
+ async function (browser) {
+ // Clear all options for cc-type select
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.document.getElementById("cc-type").innerHTML = "";
+ });
+
+ await openPopupOn(browser, "#cc-number");
+
+ const creditCardItems = getDisplayedPopupItems(
+ browser,
+ "[originaltype='autofill-profile']"
+ );
+
+ for (const [index, creditCardItem] of creditCardItems.entries()) {
+ const creditCardItemIcon = creditCardItem.getAttribute("ac-image");
+ ok(
+ creditCardItemIcon.includes(
+ CreditCard.getType(TEST_CREDIT_CARDS[index]["cc-number"])
+ ),
+ "Should use correct credit card type icon"
+ );
+ }
+ await closePopup(browser);
+ }
+ );
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_fill_cancel_login.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_fill_cancel_login.js
new file mode 100644
index 0000000000..6304e4699f
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_fill_cancel_login.js
@@ -0,0 +1,37 @@
+"use strict";
+
+add_task(async function test_fill_creditCard_but_cancel_login() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ await setStorage(TEST_CREDIT_CARD_2);
+
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false); // cancel
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ await openPopupOn(browser, "#cc-name");
+ const ccItem = getDisplayedPopupItems(browser)[0];
+ let popupClosePromise = BrowserTestUtils.waitForPopupEvent(
+ browser.autoCompletePopup,
+ "hidden"
+ );
+ await EventUtils.synthesizeMouseAtCenter(ccItem, {});
+ await Promise.all([osKeyStoreLoginShown, popupClosePromise]);
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ is(content.document.querySelector("#cc-name").value, "", "Check name");
+ is(
+ content.document.querySelector("#cc-number").value,
+ "",
+ "Check number"
+ );
+ });
+ }
+ );
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics.js
new file mode 100644
index 0000000000..b26ee7e28f
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TESTCASES = [
+ {
+ description: "@autocomplete - all fields in the same form",
+ document: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp" autocomplete="cc-exp">
+ </form>`,
+ idsToShowPopup: ["cc-number", "cc-name", "cc-exp"],
+ },
+ {
+ description: "without @autocomplete - all fields in the same form",
+ document: `<form>
+ <input id="cc-number" placeholder="credit card number">
+ <input id="cc-name" placeholder="credit card holder name">
+ <input id="cc-exp" placeholder="expiration date">
+ </form>`,
+ idsToShowPopup: ["cc-number", "cc-name", "cc-exp"],
+ },
+ {
+ description: "@autocomplete - each field in its own form",
+ document: `<form><input id="cc-number" autocomplete="cc-number"></form>
+ <form><input id="cc-name" autocomplete="cc-name"></form>
+ <form><input id="cc-exp" autocomplete="cc-exp"></form>`,
+ idsToShowPopup: ["cc-number", "cc-name", "cc-exp"],
+ },
+ {
+ description:
+ "without @autocomplete - each field in its own form (high-confidence cc-number & cc-name)",
+ document: `<form><input id="cc-number" placeholder="credit card number"></form>
+ <form><input id="cc-name" placeholder="credit card holder name"></form>
+ <form><input id="cc-exp" placeholder="expiration date"></form>`,
+ prefs: [
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold",
+ "0.9",
+ ],
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.testConfidence",
+ "0.95",
+ ],
+ ],
+ idsToShowPopup: ["cc-number", "cc-name"],
+ idsWithNoPopup: ["cc-exp"],
+ },
+ {
+ description:
+ "without @autocomplete - each field in its own form (normal-confidence cc-number & cc-name)",
+ document: `<form><input id="cc-number" placeholder="credit card number"></form>
+ <form><input id="cc-name" placeholder="credit card holder name"></form>
+ <form><input id="cc-exp" placeholder="expiration date"></form>`,
+ prefs: [
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold",
+ "0.9",
+ ],
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.testConfidence",
+ "0.8",
+ ],
+ ],
+ idsWithNoPopup: ["cc-number", "cc-name", "cc-exp"],
+ },
+ {
+ description:
+ "with @autocomplete - cc-number/cc-name and another <input> in a form",
+ document: `<form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="password" type="password">
+ </form>
+ <form>
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="password" type="password">
+ </form>`,
+ idsToShowPopup: ["cc-number", "cc-name"],
+ },
+ {
+ description:
+ "without @autocomplete - high-confidence cc-number/cc-name and another <input> in a form",
+ document: `<form>
+ <input id="cc-number" placeholder="credit card number">
+ <input id="password" type="password">
+ </form>
+ <form>
+ <input id="cc-name" placeholder="credit card holder name">
+ <input id="password" type="password">
+ </form>`,
+ idsWithNoPopup: ["cc-number", "cc-name"],
+ },
+ {
+ description:
+ "without @autocomplete - high-confidence cc-number/cc-name and another hidden <input> in a form",
+ document: `<form>
+ <input id="cc-number" placeholder="credit card number">
+ <input id="token" type="hidden">
+ </form>
+ <form>
+ <input id="cc-name" placeholder="credit card holder name">
+ <input id="token" type="hidden">
+ </form>`,
+ prefs: [
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold",
+ "0.9",
+ ],
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.testConfidence",
+ "0.95",
+ ],
+ ],
+ idsToShowPopup: ["cc-number", "cc-name"],
+ },
+];
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.creditCards.supported", "on"],
+ ["extensions.formautofill.creditCards.enabled", true],
+ ],
+ });
+
+ await setStorage(TEST_CREDIT_CARD_1);
+});
+
+add_task(async function test_heuristics() {
+ 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);
+
+ let ids = TEST.idsToShowPopup ?? [];
+ for (const id of ids) {
+ await runAndWaitForAutocompletePopupOpen(browser, async () => {
+ await focusAndWaitForFieldsIdentified(browser, `#${id}`);
+ });
+ ok(true, `popup is opened on <input id=${id}>`);
+ }
+
+ ids = TEST.idsWithNoPopup ?? [];
+ for (const id of ids) {
+ await focusAndWaitForFieldsIdentified(browser, `#${id}`);
+ await ensureNoAutocompletePopup(browser);
+ }
+ });
+
+ if (TEST.prefs) {
+ await SpecialPowers.popPrefEnv();
+ }
+ }
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_autofill_name.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_autofill_name.js
new file mode 100644
index 0000000000..f086fdb56f
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_autofill_name.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PROFILE = {
+ "cc-name": "John Doe",
+ "cc-number": "4111111111111111",
+ // "cc-type" should be remove from proile after fixing Bug 1834768.
+ "cc-type": "visa",
+ "cc-exp-month": 4,
+ "cc-exp-year": new Date().getFullYear(),
+};
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.creditCards.supported", "on"],
+ ["extensions.formautofill.creditCards.enabled", true],
+ ],
+ });
+});
+
+add_autofill_heuristic_tests([
+ {
+ description: `cc-number + first name with autocomplete attribute`,
+ fixtureData: `
+ <form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="name" autocomplete="given-name">
+ </form>`,
+ profile: TEST_PROFILE,
+ expectedResult: [
+ {
+ fields: [
+ {
+ fieldName: "cc-number",
+ reason: "autocomplete",
+ autofill: TEST_PROFILE["cc-number"],
+ },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ {
+ fieldName: "given-name",
+ reason: "autocomplete",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ description: `cc-number + first name`,
+ fixtureData: `
+ <form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="name" placeholder="given-name">
+ </form>`,
+ profile: TEST_PROFILE,
+ expectedResult: [
+ {
+ fields: [
+ {
+ fieldName: "cc-number",
+ reason: "autocomplete",
+ autofill: TEST_PROFILE["cc-number"],
+ },
+ {
+ fieldName: "cc-name",
+ reason: "update-heuristic",
+ autofill: TEST_PROFILE["cc-name"],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ description: `cc-exp + first name`,
+ fixtureData: `
+ <form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-exp" autocomplete="cc-exp">
+ <input id="name" placeholder="given-name">
+ </form>`,
+ profile: TEST_PROFILE,
+ expectedResult: [
+ {
+ fields: [
+ {
+ fieldName: "cc-number",
+ reason: "autocomplete",
+ autofill: TEST_PROFILE["cc-number"],
+ },
+ {
+ fieldName: "cc-exp",
+ reason: "autocomplete",
+ autofill: TEST_PROFILE["cc-exp"],
+ },
+ {
+ fieldName: "cc-name",
+ reason: "update-heuristic",
+ autofill: TEST_PROFILE["cc-name"],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ description: `cc-number + first and last name with autocomplete attribute`,
+ fixtureData: `
+ <form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="given" autocomplete="given-name">
+ <input id="family" autocomplete="family-name">
+ </form>`,
+ profile: TEST_PROFILE,
+ expectedResult: [
+ {
+ fields: [
+ {
+ fieldName: "cc-number",
+ reason: "autocomplete",
+ autofill: TEST_PROFILE["cc-number"],
+ },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ {
+ fieldName: "given-name",
+ reason: "autocomplete",
+ },
+ {
+ fieldName: "family-name",
+ reason: "autocomplete",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ description: `cc-number + first and last name`,
+ fixtureData: `
+ <form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="given" placeholder="First Name">
+ <input id="family" placeholder="Last Name">
+ </form>`,
+ profile: TEST_PROFILE,
+ expectedResult: [
+ {
+ fields: [
+ {
+ fieldName: "cc-number",
+ reason: "autocomplete",
+ autofill: TEST_PROFILE["cc-number"],
+ },
+ {
+ fieldName: "cc-given-name",
+ reason: "update-heuristic",
+ autofill: "John",
+ },
+ {
+ fieldName: "cc-family-name",
+ reason: "update-heuristic",
+ autofill: "Doe",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ description: `previous field is a credit card name field`,
+ fixtureData: `
+ <form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="given" placeholder="given-name">
+ <input id="family" placeholder="family-name">
+ <input id="address" placeholder="street-address">
+ <input id="country" placeholder="country">
+ </form>`,
+ profile: TEST_PROFILE,
+ expectedResult: [
+ {
+ fields: [
+ {
+ fieldName: "cc-number",
+ reason: "autocomplete",
+ autofill: TEST_PROFILE["cc-number"],
+ },
+ {
+ fieldName: "cc-name",
+ reason: "autocomplete",
+ autofill: TEST_PROFILE["cc-name"],
+ },
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "street-address" },
+ { fieldName: "country" },
+ ],
+ },
+ ],
+ },
+]);
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_cc_type.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_cc_type.js
new file mode 100644
index 0000000000..e3f12096c2
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_cc_type.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PROFILE = {
+ "cc-name": "John Doe",
+ "cc-number": "4111111111111111",
+ // "cc-type" should be remove from proile after fixing Bug 1834768.
+ "cc-type": "visa",
+ "cc-exp-month": 4,
+ "cc-exp-year": new Date().getFullYear(),
+};
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.creditCards.supported", "on"],
+ ["extensions.formautofill.creditCards.enabled", true],
+ ],
+ });
+});
+
+add_autofill_heuristic_tests([
+ {
+ description:
+ "cc-type select does not have any information in labels or attributes",
+ fixtureData: `
+ <form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <select id="test">
+ <option value="" selected="">0</option>
+ <option value="VISA">1</option>
+ <option value="MasterCard">2</option>
+ <option value="DINERS">3</option>
+ <option value="Discover">4</option>
+ </select>
+ </form>
+ <form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <select id="test">
+ <option value="0" selected="">Card Type</option>
+ <option value="1">Visa</option>
+ <option value="2">MasterCard</option>
+ <option value="3">Diners Club International</option>
+ <option value="4">Discover</option>
+ </select>
+ </form>`,
+ profile: TEST_PROFILE,
+ expectedResult: [
+ {
+ description: "cc-type option.value has the hint",
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
+ { fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
+ { fieldName: "cc-type", reason: "regex-heuristic", autofill: "VISA" },
+ ],
+ },
+ {
+ description: "cc-type option.text has the hint",
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] },
+ { fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] },
+ { fieldName: "cc-type", reason: "regex-heuristic", autofill: "1" },
+ ],
+ },
+ ],
+ },
+]);
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_autodetect_type.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_autodetect_type.js
new file mode 100644
index 0000000000..825a2978de
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_autodetect_type.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_autodetect_credit_not_set() {
+ const testCard = {
+ "cc-name": "John Doe",
+ "cc-number": "4012888888881881",
+ "cc-exp-month": "06",
+ "cc-exp-year": "2044",
+ };
+ const expectedData = {
+ ...testCard,
+ ...{ "cc-type": "visa" },
+ };
+ let onChanged = waitForStorageChangedEvents("add");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let promiseShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": testCard["cc-name"],
+ "#cc-number": testCard["cc-number"],
+ "#cc-exp-month": testCard["cc-exp-month"],
+ "#cc-exp-year": testCard["cc-exp-year"],
+ "#cc-type": testCard["cc-type"],
+ },
+ });
+
+ await promiseShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+
+ await onChanged;
+
+ let creditCards = await getCreditCards();
+ let savedCreditCard = creditCards[0];
+ let decryptedNumber = await OSKeyStore.decrypt(
+ savedCreditCard["cc-number-encrypted"]
+ );
+ savedCreditCard["cc-number"] = decryptedNumber;
+ for (let key in testCard) {
+ let expected = expectedData[key];
+ let actual = savedCreditCard[key];
+ Assert.equal(expected, actual, `${key} should match`);
+ }
+ await removeAllRecords();
+});
+
+add_task(async function test_autodetect_credit_overwrite() {
+ const testCard = {
+ "cc-name": "John Doe",
+ "cc-number": "4012888888881881",
+ "cc-exp-month": "06",
+ "cc-exp-year": "2044",
+ "cc-type": "master", // Wrong credit card type
+ };
+ const expectedData = {
+ ...testCard,
+ ...{ "cc-type": "visa" },
+ };
+ let onChanged = waitForStorageChangedEvents("add");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let promiseShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": testCard["cc-name"],
+ "#cc-number": testCard["cc-number"],
+ "#cc-exp-month": testCard["cc-exp-month"],
+ "#cc-exp-year": testCard["cc-exp-year"],
+ "#cc-type": testCard["cc-type"],
+ },
+ });
+
+ await promiseShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+
+ await onChanged;
+
+ let creditCards = await getCreditCards();
+ let savedCreditCard = creditCards[0];
+ let decryptedNumber = await OSKeyStore.decrypt(
+ savedCreditCard["cc-number-encrypted"]
+ );
+ savedCreditCard["cc-number"] = decryptedNumber;
+ for (let key in testCard) {
+ let expected = expectedData[key];
+ let actual = savedCreditCard[key];
+ Assert.equal(expected, actual, `${key} should match`);
+ }
+
+ await removeAllRecords();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_normalized.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_normalized.js
new file mode 100644
index 0000000000..76e125a196
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_normalized.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// We want to ensure that non-normalized credit card data is normalized
+// correctly as part of the save credit card flow
+add_task(async function test_new_submitted_card_is_normalized() {
+ const testCard = {
+ "cc-name": "Test User",
+ "cc-number": "5038146897157463",
+ "cc-exp-month": "4",
+ "cc-exp-year": "25",
+ };
+ const expectedData = {
+ "cc-name": "Test User",
+ "cc-number": "5038146897157463",
+ "cc-exp-month": "4",
+ // cc-exp-year should be normalized to 2025
+ "cc-exp-year": "2025",
+ };
+ let onChanged = waitForStorageChangedEvents("add");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let promiseShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": testCard["cc-name"],
+ "#cc-number": testCard["cc-number"],
+ "#cc-exp-month": testCard["cc-exp-month"],
+ "#cc-exp-year": testCard["cc-exp-year"],
+ },
+ });
+
+ await promiseShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+
+ await onChanged;
+
+ let creditCards = await getCreditCards();
+ let savedCreditCard = creditCards[0];
+ let decryptedNumber = await OSKeyStore.decrypt(
+ savedCreditCard["cc-number-encrypted"]
+ );
+ savedCreditCard["cc-number"] = decryptedNumber;
+ for (let key in testCard) {
+ let expected = expectedData[key];
+ let actual = savedCreditCard[key];
+ Assert.equal(expected, actual, `${key} should match`);
+ }
+ await removeAllRecords();
+});
+
+add_task(async function test_updated_card_is_normalized() {
+ const testCard = {
+ "cc-name": "Test User",
+ "cc-number": "5038146897157463",
+ "cc-exp-month": "11",
+ "cc-exp-year": "20",
+ };
+ await saveCreditCard(testCard);
+ const expectedData = {
+ "cc-name": "Test User",
+ "cc-number": "5038146897157463",
+ "cc-exp-month": "10",
+ // cc-exp-year should be normalized to 2027
+ "cc-exp-year": "2027",
+ };
+ let onChanged = waitForStorageChangedEvents("update");
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let promiseShown = waitForPopupShown();
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": testCard["cc-name"],
+ "#cc-number": testCard["cc-number"],
+ "#cc-exp-month": "10",
+ "#cc-exp-year": "27",
+ },
+ });
+
+ await promiseShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ }
+ );
+
+ await onChanged;
+
+ let creditCards = await getCreditCards();
+ let savedCreditCard = creditCards[0];
+ savedCreditCard["cc-number"] = await OSKeyStore.decrypt(
+ savedCreditCard["cc-number-encrypted"]
+ );
+
+ for (let key in testCard) {
+ let expected = expectedData[key];
+ let actual = savedCreditCard[key];
+ Assert.equal(expected, actual, `${key} should match`);
+ }
+ await removeAllRecords();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js
new file mode 100644
index 0000000000..7a4bff1e45
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js
@@ -0,0 +1,1170 @@
+"use strict";
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const CC_NUM_USES_HISTOGRAM = "CREDITCARD_NUM_USES";
+
+function ccFormArgsv1(method, extra) {
+ return ["creditcard", method, "cc_form", undefined, extra];
+}
+
+function ccFormArgsv2(method, extra) {
+ return ["creditcard", method, "cc_form_v2", undefined, extra];
+}
+
+function buildccFormv2Extra(extra, defaultValue) {
+ let defaults = {};
+ for (const field of [
+ "cc_name",
+ "cc_number",
+ "cc_type",
+ "cc_exp",
+ "cc_exp_month",
+ "cc_exp_year",
+ ]) {
+ defaults[field] = defaultValue;
+ }
+
+ return { ...defaults, ...extra };
+}
+
+function assertGleanTelemetry(events) {
+ let flow_ids = new Set();
+ events.forEach(({ event_name, expected_extra, event_count = 1 }) => {
+ const actual_events =
+ Glean.formautofillCreditcards[event_name].testGetValue() ?? [];
+
+ Assert.equal(
+ actual_events.length,
+ event_count,
+ `Expected to have ${event_count} event/s with the name "${event_name}"`
+ );
+
+ if (expected_extra) {
+ let actual_extra = actual_events[0].extra;
+ flow_ids.add(actual_extra.flow_id);
+ delete actual_extra.flow_id; // We don't want to test the specific flow_id value yet
+
+ Assert.deepEqual(actual_events[0].extra, expected_extra);
+ }
+ });
+
+ Assert.equal(
+ flow_ids.size,
+ 1,
+ `All events from the same user interaction session have the same flow id`
+ );
+}
+
+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: "creditcard",
+ },
+ { 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: "creditcard",
+ },
+ { process: "parent" }
+ );
+ }
+}
+
+async function assertHistogram(histogramId, expectedNonZeroRanges) {
+ let actualNonZeroRanges = {};
+ await TestUtils.waitForCondition(
+ () => {
+ const snapshot = Services.telemetry
+ .getHistogramById(histogramId)
+ .snapshot();
+ // Compute the actual ranges in the format { range1: value1, range2: value2 }.
+ for (let [range, value] of Object.entries(snapshot.values)) {
+ if (value > 0) {
+ actualNonZeroRanges[range] = value;
+ }
+ }
+
+ return (
+ JSON.stringify(actualNonZeroRanges) ==
+ JSON.stringify(expectedNonZeroRanges)
+ );
+ },
+ "Wait for telemetry to be collected",
+ 100,
+ 100
+ );
+
+ Assert.equal(
+ JSON.stringify(actualNonZeroRanges),
+ JSON.stringify(expectedNonZeroRanges)
+ );
+}
+
+async function openTabAndUseCreditCard(
+ idx,
+ creditCard,
+ { closeTab = true, submitForm = true } = {}
+) {
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ CREDITCARD_FORM_URL
+ );
+ let browser = tab.linkedBrowser;
+
+ await openPopupOn(browser, "form #cc-name");
+ for (let i = 0; i <= idx; i++) {
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ }
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ await osKeyStoreLoginShown;
+ await waitForAutofill(browser, "#cc-number", creditCard["cc-number"]);
+ await focusUpdateSubmitForm(
+ browser,
+ {
+ focusSelector: "#cc-number",
+ newValues: {},
+ },
+ submitForm
+ );
+
+ // flushing Glean data before tab removal (see Bug 1843178)
+ await Services.fog.testFlushAllChildren();
+
+ if (!closeTab) {
+ return tab;
+ }
+
+ await BrowserTestUtils.removeTab(tab);
+ return null;
+}
+
+add_setup(async function () {
+ Services.telemetry.setEventRecordingEnabled("creditcard", true);
+ registerCleanupFunction(async function () {
+ Services.telemetry.setEventRecordingEnabled("creditcard", false);
+ });
+ await clearGleanTelemetry();
+});
+
+add_task(async function test_popup_opened() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [ENABLED_AUTOFILL_CREDITCARDS_PREF, true],
+ [AUTOFILL_CREDITCARDS_AVAILABLE_PREF, "on"],
+ ],
+ });
+
+ Services.telemetry.clearEvents();
+ Services.telemetry.clearScalars();
+ await clearGleanTelemetry();
+
+ await setStorage(TEST_CREDIT_CARD_1);
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ await openPopupOn(browser, "#cc-number");
+ await closePopup(browser);
+
+ // flushing Glean data within withNewTab callback before tab removal (see Bug 1843178)
+ await Services.fog.testFlushAllChildren();
+ }
+ );
+
+ await assertTelemetry([
+ ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2("popup_shown", { field_name: "cc-number" }),
+ ccFormArgsv1("popup_shown"),
+ ]);
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.detected_sections_count",
+ 1,
+ "There should be 1 section detected."
+ );
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.submitted_sections_count"
+ );
+
+ await assertGleanTelemetry([
+ {
+ event_name: "formDetected",
+ expected_extra: buildccFormv2Extra(
+ { cc_exp: "undetected", cc_number_multi_parts: 1 },
+ "autocomplete"
+ ),
+ },
+ {
+ event_name: "formPopupShown",
+ expected_extra: { field_name: "cc-number" },
+ },
+ ]);
+
+ await removeAllRecords();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_popup_opened_form_without_autocomplete() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [ENABLED_AUTOFILL_CREDITCARDS_PREF, true],
+ [AUTOFILL_CREDITCARDS_AVAILABLE_PREF, "on"],
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.testConfidence",
+ "1",
+ ],
+ ],
+ });
+
+ Services.telemetry.clearEvents();
+ Services.telemetry.clearScalars();
+ await clearGleanTelemetry();
+
+ await setStorage(TEST_CREDIT_CARD_1);
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_WITHOUT_AUTOCOMPLETE_URL },
+ async function (browser) {
+ await openPopupOn(browser, "#cc-number");
+ await closePopup(browser);
+
+ // flushing Glean data within withNewTab callback before tab removal (see Bug 1843178)
+ await Services.fog.testFlushAllChildren();
+ }
+ );
+
+ await assertTelemetry([
+ ccFormArgsv2(
+ "detected",
+ buildccFormv2Extra({ cc_number: "1", cc_name: "1", cc_exp: "false" }, "0")
+ ),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2("popup_shown", { field_name: "cc-number" }),
+ ccFormArgsv1("popup_shown"),
+ ]);
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.detected_sections_count",
+ 1,
+ "There should be 1 section detected."
+ );
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.submitted_sections_count"
+ );
+
+ await assertGleanTelemetry([
+ {
+ event_name: "formDetected",
+ expected_extra: buildccFormv2Extra(
+ {
+ cc_number: "1",
+ cc_name: "1",
+ cc_exp: "undetected",
+ cc_number_multi_parts: 1,
+ },
+ "regexp"
+ ),
+ },
+ {
+ event_name: "formPopupShown",
+ expected_extra: { field_name: "cc-number" },
+ },
+ ]);
+
+ await removeAllRecords();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(
+ async function test_popup_opened_form_without_autocomplete_separate_cc_number() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [ENABLED_AUTOFILL_CREDITCARDS_PREF, true],
+ [AUTOFILL_CREDITCARDS_AVAILABLE_PREF, "on"],
+ [
+ "extensions.formautofill.creditCards.heuristics.fathom.testConfidence",
+ "1",
+ ],
+ ],
+ });
+
+ Services.telemetry.clearEvents();
+ Services.telemetry.clearScalars();
+ await clearGleanTelemetry();
+
+ await setStorage(TEST_CREDIT_CARD_1);
+
+ // Click on the cc-number field of the form that only contains a cc-number field
+ // (detected by Fathom)
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_WITHOUT_AUTOCOMPLETE_URL },
+ async function (browser) {
+ await openPopupOn(browser, "#form2-cc-number #cc-number");
+ await closePopup(browser);
+
+ // flushing Glean data within withNewTab callback before tab removal (see Bug 1843178)
+ await Services.fog.testFlushAllChildren();
+ }
+ );
+
+ await assertTelemetry([
+ ccFormArgsv2("detected", buildccFormv2Extra({ cc_number: "1" }, "false")),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2("popup_shown", { field_name: "cc-number" }),
+ ccFormArgsv1("popup_shown"),
+ ]);
+
+ await assertGleanTelemetry([
+ {
+ event_name: "formDetected",
+ expected_extra: buildccFormv2Extra(
+ { cc_number: "1", cc_number_multi_parts: 1 },
+ "undetected"
+ ),
+ },
+ {
+ event_name: "formPopupShown",
+ expected_extra: { field_name: "cc-number" },
+ },
+ ]);
+
+ await clearGleanTelemetry();
+
+ // Then click on the cc-name field of the form that doesn't have a cc-number field
+ // (detected by regexp-based heuristic)
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_WITHOUT_AUTOCOMPLETE_URL },
+ async function (browser) {
+ await openPopupOn(browser, "#form2-cc-other #cc-name");
+ await closePopup(browser);
+
+ // flushing Glean data within withNewTab callback before tab removal (see Bug 1843178)
+ await Services.fog.testFlushAllChildren();
+ }
+ );
+
+ await assertTelemetry([
+ ccFormArgsv2(
+ "detected",
+ buildccFormv2Extra(
+ { cc_name: "1", cc_type: "0", cc_exp_month: "0", cc_exp_year: "0" },
+ "false"
+ )
+ ),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2("popup_shown", { field_name: "cc-name" }),
+ ccFormArgsv1("popup_shown"),
+ ]);
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.detected_sections_count",
+ 2,
+ "There should be 1 section detected."
+ );
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.submitted_sections_count"
+ );
+
+ await assertGleanTelemetry([
+ {
+ event_name: "formDetected",
+ expected_extra: buildccFormv2Extra(
+ {
+ cc_name: "1",
+ cc_type: "regexp",
+ cc_exp_month: "regexp",
+ cc_exp_year: "regexp",
+ },
+ "undetected"
+ ),
+ },
+ {
+ event_name: "formPopupShown",
+ expected_extra: { field_name: "cc-name" },
+ },
+ ]);
+
+ await removeAllRecords();
+ await SpecialPowers.popPrefEnv();
+ }
+);
+
+add_task(async function test_submit_creditCard_new() {
+ async function test_per_command(
+ command,
+ idx,
+ useCount = {},
+ expectChanged = undefined
+ ) {
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ let onChanged;
+ if (expectChanged !== undefined) {
+ onChanged = TestUtils.topicObserved("formautofill-storage-changed");
+ }
+
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-name": "User 1",
+ "#cc-number": "5038146897157463",
+ "#cc-exp-month": "12",
+ "#cc-exp-year": "2017",
+ "#cc-type": "mastercard",
+ },
+ });
+
+ await onPopupShown;
+ await clickDoorhangerButton(command, idx);
+ if (expectChanged !== undefined) {
+ await onChanged;
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent"),
+ "formautofill.creditCards.autofill_profiles_count",
+ expectChanged,
+ `There should be ${expectChanged} profile(s) stored and recorded in Legacy Telemetry.`
+ );
+ Assert.equal(
+ expectChanged,
+ Glean.formautofillCreditcards.autofillProfilesCount.testGetValue(),
+ `There should be ${expectChanged} profile(s) stored and recorded in Glean.`
+ );
+ }
+
+ // flushing Glean data within withNewTab callback before tab removal (see Bug 1843178)
+ await Services.fog.testFlushAllChildren();
+ }
+ );
+
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, useCount);
+
+ await removeAllRecords();
+ SpecialPowers.popPrefEnv();
+ }
+
+ Services.telemetry.clearEvents();
+ Services.telemetry.clearScalars();
+ Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear();
+ await clearGleanTelemetry();
+
+ let expected_content = [
+ ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2(
+ "submitted",
+ buildccFormv2Extra({ cc_exp: "unavailable" }, "user_filled")
+ ),
+ ccFormArgsv1("submitted", {
+ // 5 fields plus submit button
+ fields_not_auto: "6",
+ fields_auto: "0",
+ fields_modified: "0",
+ }),
+ ];
+ let expected_glean_events = [
+ {
+ event_name: "formDetected",
+ expected_extra: buildccFormv2Extra(
+ { cc_exp: "undetected", cc_number_multi_parts: 1 },
+ "autocomplete"
+ ),
+ },
+ {
+ event_name: "formSubmitted",
+ expected_extra: buildccFormv2Extra(
+ { cc_exp: "unavailable" },
+ "user_filled"
+ ),
+ },
+ ];
+ await test_per_command(MAIN_BUTTON, undefined, { 1: 1 }, 1);
+
+ await assertTelemetry(expected_content, [
+ ["creditcard", "show", "capture_doorhanger"],
+ ["creditcard", "save", "capture_doorhanger"],
+ ]);
+
+ await assertGleanTelemetry(expected_glean_events);
+
+ await clearGleanTelemetry();
+
+ await test_per_command(SECONDARY_BUTTON);
+
+ await assertTelemetry(expected_content, [
+ ["creditcard", "show", "capture_doorhanger"],
+ ["creditcard", "cancel", "capture_doorhanger"],
+ ]);
+
+ await assertGleanTelemetry(expected_glean_events);
+
+ await clearGleanTelemetry();
+
+ await test_per_command(MENU_BUTTON, 0);
+
+ await assertTelemetry(expected_content, [
+ ["creditcard", "show", "capture_doorhanger"],
+ ["creditcard", "disable", "capture_doorhanger"],
+ ]);
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.detected_sections_count",
+ 3,
+ "There should be 3 sections detected."
+ );
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("content"),
+ "formautofill.creditCards.submitted_sections_count",
+ 3,
+ "There should be 1 section submitted."
+ );
+
+ await assertGleanTelemetry(expected_glean_events);
+});
+
+add_task(async function test_submit_creditCard_autofill() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ Services.telemetry.clearEvents();
+ Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear();
+ await clearGleanTelemetry();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ Assert.equal(creditCards.length, 1, "1 credit card in storage");
+
+ await openTabAndUseCreditCard(0, TEST_CREDIT_CARD_1);
+
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {
+ 1: 1,
+ });
+
+ SpecialPowers.clearUserPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
+
+ await assertTelemetry(
+ [
+ ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2("popup_shown", { field_name: "cc-name" }),
+ ccFormArgsv1("popup_shown"),
+ ccFormArgsv2(
+ "filled",
+ buildccFormv2Extra({ cc_exp: "unavailable" }, "filled")
+ ),
+ ccFormArgsv1("filled"),
+ ccFormArgsv2(
+ "submitted",
+ buildccFormv2Extra({ cc_exp: "unavailable" }, "autofilled")
+ ),
+ ccFormArgsv1("submitted", {
+ fields_not_auto: "3",
+ fields_auto: "5",
+ fields_modified: "0",
+ }),
+ ],
+ []
+ );
+
+ await assertGleanTelemetry([
+ {
+ event_name: "formDetected",
+ expected_extra: buildccFormv2Extra(
+ { cc_exp: "undetected", cc_number_multi_parts: 1 },
+ "autocomplete"
+ ),
+ },
+ {
+ event_name: "formPopupShown",
+ expected_extra: {
+ field_name: "cc-name",
+ },
+ },
+ {
+ event_name: "formFilled",
+ expected_extra: buildccFormv2Extra({ cc_exp: "unavailable" }, "filled"),
+ },
+ {
+ event_name: "formSubmitted",
+ expected_extra: buildccFormv2Extra(
+ { cc_exp: "unavailable" },
+ "autofilled"
+ ),
+ },
+ ]);
+
+ await removeAllRecords();
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_submit_creditCard_update() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ async function test_per_command(
+ command,
+ idx,
+ useCount = {},
+ expectChanged = undefined
+ ) {
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ Assert.equal(creditCards.length, 1, "1 credit card in storage");
+
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: CREDITCARD_FORM_URL },
+ async function (browser) {
+ let onPopupShown = waitForPopupShown();
+ let onChanged;
+ if (expectChanged !== undefined) {
+ onChanged = TestUtils.topicObserved("formautofill-storage-changed");
+ }
+
+ await openPopupOn(browser, "form #cc-name");
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ await osKeyStoreLoginShown;
+
+ await waitForAutofill(browser, "#cc-name", "John Doe");
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-name",
+ newValues: {
+ "#cc-exp-year": "2019",
+ },
+ });
+ await onPopupShown;
+ await clickDoorhangerButton(command, idx);
+ if (expectChanged !== undefined) {
+ await onChanged;
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent"),
+ "formautofill.creditCards.autofill_profiles_count",
+ expectChanged,
+ `There should be ${expectChanged} profile(s) stored and recorded in Legacy Telemetry.`
+ );
+ Assert.equal(
+ expectChanged,
+ Glean.formautofillCreditcards.autofillProfilesCount.testGetValue(),
+ `There should be ${expectChanged} profile(s) stored.`
+ );
+ }
+ // flushing Glean data within withNewTab callback before tab removal (see Bug 1843178)
+ await Services.fog.testFlushAllChildren();
+ }
+ );
+
+ await assertHistogram("CREDITCARD_NUM_USES", useCount);
+
+ SpecialPowers.clearUserPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
+
+ await removeAllRecords();
+ }
+
+ Services.telemetry.clearEvents();
+ Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear();
+ await clearGleanTelemetry();
+
+ let expected_content = [
+ ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2("popup_shown", { field_name: "cc-name" }),
+ ccFormArgsv1("popup_shown"),
+ ccFormArgsv2(
+ "filled",
+ buildccFormv2Extra({ cc_exp: "unavailable" }, "filled")
+ ),
+ ccFormArgsv1("filled"),
+ ccFormArgsv2("filled_modified", { field_name: "cc-exp-year" }),
+ ccFormArgsv1("filled_modified", { field_name: "cc-exp-year" }),
+ ccFormArgsv2(
+ "submitted",
+ buildccFormv2Extra(
+ { cc_exp: "unavailable", cc_exp_year: "user_filled" },
+ "autofilled"
+ )
+ ),
+ ccFormArgsv1("submitted", {
+ fields_not_auto: "3",
+ fields_auto: "5",
+ fields_modified: "1",
+ }),
+ ];
+ let expected_glean_events = [
+ {
+ event_name: "formDetected",
+ expected_extra: buildccFormv2Extra(
+ { cc_exp: "undetected", cc_number_multi_parts: 1 },
+ "autocomplete"
+ ),
+ },
+ {
+ event_name: "formPopupShown",
+ expected_extra: {
+ field_name: "cc-name",
+ },
+ },
+ {
+ event_name: "formFilled",
+ expected_extra: buildccFormv2Extra({ cc_exp: "unavailable" }, "filled"),
+ },
+ {
+ event_name: "formFilledModified",
+ expected_extra: {
+ field_name: "cc-exp-year",
+ },
+ },
+ {
+ event_name: "formSubmitted",
+ expected_extra: buildccFormv2Extra(
+ { cc_exp: "unavailable", cc_exp_year: "user_filled" },
+ "autofilled"
+ ),
+ },
+ ];
+
+ await clearGleanTelemetry();
+
+ await test_per_command(MAIN_BUTTON, undefined, { 1: 1 }, 1);
+
+ await assertTelemetry(expected_content, [
+ ["creditcard", "show", "update_doorhanger"],
+ ["creditcard", "update", "update_doorhanger"],
+ ]);
+
+ await assertGleanTelemetry(expected_glean_events);
+
+ await clearGleanTelemetry();
+
+ await test_per_command(SECONDARY_BUTTON, undefined, { 0: 1, 1: 1 }, 2);
+
+ await assertTelemetry(expected_content, [
+ ["creditcard", "show", "update_doorhanger"],
+ ["creditcard", "save", "update_doorhanger"],
+ ]);
+
+ await assertGleanTelemetry(expected_glean_events);
+});
+
+const TEST_SELECTORS = {
+ selRecords: "#credit-cards",
+ btnRemove: "#remove",
+ btnAdd: "#add",
+ btnEdit: "#edit",
+};
+
+const DIALOG_SIZE = "width=600,height=400";
+
+add_task(async function test_removingCreditCardsViaKeyboardDelete() {
+ Services.telemetry.clearEvents();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ let win = window.openDialog(
+ MANAGE_CREDIT_CARDS_DIALOG_URL,
+ null,
+ DIALOG_SIZE
+ );
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+
+ Assert.equal(selRecords.length, 1, "One credit card");
+
+ EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+ EventUtils.synthesizeKey("VK_DELETE", {}, win);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
+ Assert.equal(selRecords.length, 0, "No credit cards left");
+
+ win.close();
+
+ await assertTelemetry(undefined, [
+ ["creditcard", "show", "manage"],
+ ["creditcard", "delete", "manage"],
+ ]);
+
+ await removeAllRecords();
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_saveCreditCard() {
+ Services.telemetry.clearEvents();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-number"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ "0" + TEST_CREDIT_CARD_1["cc-exp-month"].toString(),
+ {},
+ win
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ TEST_CREDIT_CARD_1["cc-exp-year"].toString(),
+ {},
+ win
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-name"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ info("saving credit card");
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ });
+
+ await assertTelemetry(undefined, [["creditcard", "add", "manage"]]);
+
+ await removeAllRecords();
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_editCreditCard() {
+ Services.telemetry.clearEvents();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+
+ await setStorage(TEST_CREDIT_CARD_1);
+
+ let creditCards = await getCreditCards();
+ Assert.equal(creditCards.length, 1, "only one credit card is in storage");
+ await testDialog(
+ EDIT_CREDIT_CARD_DIALOG_URL,
+ win => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_RIGHT", {}, win);
+ EventUtils.synthesizeKey("test", {}, win);
+ win.document.querySelector("#save").click();
+ },
+ {
+ record: creditCards[0],
+ }
+ );
+
+ await assertTelemetry(undefined, [
+ ["creditcard", "show_entry", "manage"],
+ ["creditcard", "edit", "manage"],
+ ]);
+
+ await removeAllRecords();
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_histogram() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+
+ Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear();
+
+ await setStorage(
+ TEST_CREDIT_CARD_1,
+ TEST_CREDIT_CARD_2,
+ TEST_CREDIT_CARD_3,
+ TEST_CREDIT_CARD_5
+ );
+ let creditCards = await getCreditCards();
+ Assert.equal(creditCards.length, 4, "4 credit cards in storage");
+
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {
+ 0: 4,
+ });
+
+ await openTabAndUseCreditCard(0, TEST_CREDIT_CARD_1);
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {
+ 0: 3,
+ 1: 1,
+ });
+
+ await openTabAndUseCreditCard(1, TEST_CREDIT_CARD_2);
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {
+ 0: 2,
+ 1: 2,
+ });
+
+ await openTabAndUseCreditCard(0, TEST_CREDIT_CARD_2);
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {
+ 0: 2,
+ 1: 1,
+ 2: 1,
+ });
+
+ await openTabAndUseCreditCard(1, TEST_CREDIT_CARD_1);
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {
+ 0: 2,
+ 2: 2,
+ });
+
+ await openTabAndUseCreditCard(2, TEST_CREDIT_CARD_5);
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {
+ 0: 1,
+ 1: 1,
+ 2: 2,
+ });
+
+ await removeAllRecords();
+ SpecialPowers.popPrefEnv();
+
+ await assertHistogram(CC_NUM_USES_HISTOGRAM, {});
+});
+
+add_task(async function test_clear_creditCard_autofill() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ Services.telemetry.clearEvents();
+ Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear();
+ await clearGleanTelemetry();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]],
+ });
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ let creditCards = await getCreditCards();
+ Assert.equal(creditCards.length, 1, "1 credit card in storage");
+
+ let tab = await openTabAndUseCreditCard(0, TEST_CREDIT_CARD_1, {
+ closeTab: false,
+ submitForm: false,
+ });
+
+ let expected_content = [
+ ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")),
+ ccFormArgsv1("detected"),
+ ccFormArgsv2("popup_shown", { field_name: "cc-name" }),
+ ccFormArgsv1("popup_shown"),
+ ccFormArgsv2(
+ "filled",
+ buildccFormv2Extra({ cc_exp: "unavailable" }, "filled")
+ ),
+ ccFormArgsv1("filled"),
+ ];
+ await assertTelemetry(expected_content, []);
+
+ await assertGleanTelemetry([
+ {
+ event_name: "formDetected",
+ expected_extra: buildccFormv2Extra(
+ { cc_exp: "undetected", cc_number_multi_parts: 1 },
+ "autocomplete"
+ ),
+ },
+ {
+ event_name: "formPopupShown",
+ expected_extra: {
+ field_name: "cc-name",
+ },
+ },
+ {
+ event_name: "formFilled",
+ expected_extra: buildccFormv2Extra({ cc_exp: "unavailable" }, "filled"),
+ },
+ ]);
+
+ Services.telemetry.clearEvents();
+ await clearGleanTelemetry();
+
+ let browser = tab.linkedBrowser;
+
+ let popupShown = BrowserTestUtils.waitForPopupEvent(
+ browser.autoCompletePopup,
+ "shown"
+ );
+ // Already focus in "cc-number" field, press 'down' to bring to popup.
+ await BrowserTestUtils.synthesizeKey("KEY_ArrowDown", {}, browser);
+
+ await popupShown;
+
+ // flushing Glean data before tab removal (see Bug 1843178)
+ await Services.fog.testFlushAllChildren();
+
+ expected_content = [
+ ccFormArgsv2("popup_shown", { field_name: "cc-number" }),
+ ccFormArgsv1("popup_shown"),
+ ];
+ await assertTelemetry(expected_content, []);
+ await assertGleanTelemetry([
+ {
+ event_name: "formPopupShown",
+ expected_extra: {
+ field_name: "cc-number",
+ },
+ },
+ ]);
+ Services.telemetry.clearEvents();
+ await clearGleanTelemetry();
+
+ let popupHidden = BrowserTestUtils.waitForPopupEvent(
+ browser.autoCompletePopup,
+ "hidden"
+ );
+
+ // kPress Clear Form.
+ await BrowserTestUtils.synthesizeKey("KEY_ArrowDown", {}, browser);
+ await BrowserTestUtils.synthesizeKey("KEY_Enter", {}, browser);
+
+ await popupHidden;
+
+ popupShown = BrowserTestUtils.waitForPopupEvent(
+ browser.autoCompletePopup,
+ "shown"
+ );
+
+ await popupShown;
+
+ // flushing Glean data before tab removal (see Bug 1843178)
+ await Services.fog.testFlushAllChildren();
+
+ expected_content = [
+ ccFormArgsv2("filled_modified", { field_name: "cc-name" }),
+ ccFormArgsv1("filled_modified", { field_name: "cc-name" }),
+ ccFormArgsv2("filled_modified", { field_name: "cc-number" }),
+ ccFormArgsv1("filled_modified", { field_name: "cc-number" }),
+ ccFormArgsv2("filled_modified", { field_name: "cc-exp-month" }),
+ ccFormArgsv1("filled_modified", { field_name: "cc-exp-month" }),
+ ccFormArgsv2("filled_modified", { field_name: "cc-exp-year" }),
+ ccFormArgsv1("filled_modified", { field_name: "cc-exp-year" }),
+ ccFormArgsv2("filled_modified", { field_name: "cc-type" }),
+ ccFormArgsv1("filled_modified", { field_name: "cc-type" }),
+ ccFormArgsv2("cleared", { field_name: "cc-number" }),
+ // popup is shown again because when the field is cleared and is focused,
+ // we automatically triggers the popup.
+ ccFormArgsv2("popup_shown", { field_name: "cc-number" }),
+ ccFormArgsv1("popup_shown"),
+ ];
+
+ await assertTelemetry(expected_content, []);
+
+ await assertGleanTelemetry([
+ {
+ event_name: "formFilledModified",
+ event_count: 5,
+ },
+ {
+ event_name: "formCleared",
+ expected_extra: {
+ field_name: "cc-number",
+ },
+ },
+ {
+ event_name: "formPopupShown",
+ expected_extra: {
+ field_name: "cc-number",
+ },
+ },
+ ]);
+
+ Services.telemetry.clearEvents();
+ await clearGleanTelemetry();
+
+ await BrowserTestUtils.removeTab(tab);
+
+ await removeAllRecords();
+ SpecialPowers.popPrefEnv();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_editCreditCardDialog.js b/browser/extensions/formautofill/test/browser/creditCard/browser_editCreditCardDialog.js
new file mode 100644
index 0000000000..f532ea5da9
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_editCreditCardDialog.js
@@ -0,0 +1,422 @@
+"use strict";
+
+add_setup(async function () {
+ let { formAutofillStorage } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillStorage.sys.mjs"
+ );
+ await formAutofillStorage.initialize();
+});
+
+add_task(async function test_cancelEditCreditCardDialog() {
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => {
+ win.document.querySelector("#cancel").click();
+ });
+});
+
+add_task(async function test_cancelEditCreditCardDialogWithESC() {
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => {
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ });
+});
+
+add_task(async function test_saveCreditCard() {
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => {
+ ok(
+ win.document.documentElement
+ .querySelector("title")
+ .textContent.includes("Add"),
+ "Add card dialog title is correct"
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-number"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ "0" + TEST_CREDIT_CARD_1["cc-exp-month"].toString(),
+ {},
+ win
+ );
+ is(
+ win.document.activeElement.selectedOptions[0].text,
+ "04 - April",
+ "Displayed month should match number and name"
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ TEST_CREDIT_CARD_1["cc-exp-year"].toString(),
+ {},
+ win
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-name"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ info("saving credit card");
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ });
+ let creditCards = await getCreditCards();
+
+ is(creditCards.length, 1, "only one credit card is in storage");
+ for (let [fieldName, fieldValue] of Object.entries(TEST_CREDIT_CARD_1)) {
+ if (fieldName === "cc-number") {
+ fieldValue = "*".repeat(fieldValue.length - 4) + fieldValue.substr(-4);
+ }
+ is(creditCards[0][fieldName], fieldValue, "check " + fieldName);
+ }
+ is(creditCards[0].billingAddressGUID, undefined, "check billingAddressGUID");
+ ok(creditCards[0]["cc-number-encrypted"], "cc-number-encrypted exists");
+});
+
+add_task(async function test_saveCreditCardWithMaxYear() {
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD_2["cc-number"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ TEST_CREDIT_CARD_2["cc-exp-month"].toString(),
+ {},
+ win
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ TEST_CREDIT_CARD_2["cc-exp-year"].toString(),
+ {},
+ win
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD_2["cc-name"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ info("saving credit card");
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ });
+ let creditCards = await getCreditCards();
+
+ is(creditCards.length, 2, "Two credit cards are in storage");
+ for (let [fieldName, fieldValue] of Object.entries(TEST_CREDIT_CARD_2)) {
+ if (fieldName === "cc-number") {
+ fieldValue = "*".repeat(fieldValue.length - 4) + fieldValue.substr(-4);
+ }
+ is(creditCards[1][fieldName], fieldValue, "check " + fieldName);
+ }
+ ok(creditCards[1]["cc-number-encrypted"], "cc-number-encrypted exists");
+ await removeCreditCards([creditCards[1].guid]);
+});
+
+add_task(async function test_saveCreditCardWithBillingAddress() {
+ await setStorage(TEST_ADDRESS_4, TEST_ADDRESS_1);
+ let addresses = await getAddresses();
+ let billingAddress = addresses[0];
+
+ const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_2, {
+ billingAddressGUID: undefined,
+ });
+
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-number"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ TEST_CREDIT_CARD["cc-exp-month"].toString(),
+ {},
+ win
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(
+ TEST_CREDIT_CARD["cc-exp-year"].toString(),
+ {},
+ win
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-name"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey(billingAddress["given-name"], {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ info("saving credit card");
+ EventUtils.synthesizeKey("VK_RETURN", {}, win);
+ });
+ let creditCards = await getCreditCards();
+
+ is(creditCards.length, 2, "Two credit cards are in storage");
+ for (let [fieldName, fieldValue] of Object.entries(TEST_CREDIT_CARD)) {
+ if (fieldName === "cc-number") {
+ fieldValue = "*".repeat(fieldValue.length - 4) + fieldValue.substr(-4);
+ }
+ is(creditCards[1][fieldName], fieldValue, "check " + fieldName);
+ }
+ ok(creditCards[1]["cc-number-encrypted"], "cc-number-encrypted exists");
+ await removeCreditCards([creditCards[1].guid]);
+ await removeAddresses([addresses[0].guid, addresses[1].guid]);
+});
+
+add_task(async function test_editCreditCard() {
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "only one credit card is in storage");
+ await testDialog(
+ EDIT_CREDIT_CARD_DIALOG_URL,
+ win => {
+ ok(
+ win.document.documentElement
+ .querySelector("title")
+ .textContent.includes("Edit"),
+ "Edit card dialog title is correct"
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_RIGHT", {}, win);
+ EventUtils.synthesizeKey("test", {}, win);
+ win.document.querySelector("#save").click();
+ },
+ {
+ record: creditCards[0],
+ }
+ );
+ ok(true, "Edit credit card dialog is closed");
+ creditCards = await getCreditCards();
+
+ is(creditCards.length, 1, "only one credit card is in storage");
+ is(
+ creditCards[0]["cc-name"],
+ TEST_CREDIT_CARD_1["cc-name"] + "test",
+ "cc name changed"
+ );
+ await removeCreditCards([creditCards[0].guid]);
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 0, "Credit card storage is empty");
+});
+
+add_task(async function test_editCreditCardWithMissingBillingAddress() {
+ const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_2, {
+ billingAddressGUID: "unknown-guid",
+ });
+ await setStorage(TEST_CREDIT_CARD);
+
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "one credit card in storage");
+ is(
+ creditCards[0].billingAddressGUID,
+ TEST_CREDIT_CARD.billingAddressGUID,
+ "Check saved billingAddressGUID"
+ );
+ await testDialog(
+ EDIT_CREDIT_CARD_DIALOG_URL,
+ win => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_RIGHT", {}, win);
+ EventUtils.synthesizeKey("test", {}, win);
+ win.document.querySelector("#save").click();
+ },
+ {
+ record: creditCards[0],
+ }
+ );
+ ok(true, "Edit credit card dialog is closed");
+ creditCards = await getCreditCards();
+
+ is(creditCards.length, 1, "only one credit card is in storage");
+ is(
+ creditCards[0]["cc-name"],
+ TEST_CREDIT_CARD["cc-name"] + "test",
+ "cc name changed"
+ );
+ is(
+ creditCards[0].billingAddressGUID,
+ undefined,
+ "unknown GUID removed upon manual save"
+ );
+ await removeCreditCards([creditCards[0].guid]);
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 0, "Credit card storage is empty");
+});
+
+add_task(async function test_addInvalidCreditCard() {
+ await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, async win => {
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("test", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("test name", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeMouseAtCenter(
+ win.document.querySelector("#save"),
+ {},
+ win
+ );
+
+ is(
+ win.document.querySelector("form").checkValidity(),
+ false,
+ "cc-number is invalid"
+ );
+ await ensureCreditCardDialogNotClosed(win);
+ info("closing");
+ win.close();
+ });
+ info("closed");
+ let creditCards = await getCreditCards();
+
+ is(creditCards.length, 0, "Credit card storage is empty");
+});
+
+add_task(async function test_editInvalidCreditCardNumber() {
+ await setStorage(TEST_ADDRESS_4);
+ let addresses = await getAddresses();
+ let billingAddress = addresses[0];
+
+ const INVALID_CREDIT_CARD_NUMBER = "123456789";
+ const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_2, {
+ billingAddressGUID: billingAddress.guid,
+ guid: "invalid-number",
+ version: 2,
+ "cc-number": INVALID_CREDIT_CARD_NUMBER,
+ });
+
+ // Directly use FormAutofillStorage so we can set
+ // sourceSync: true, since saveCreditCard uses FormAutofillParent
+ // which doesn't expose this option.
+ let { formAutofillStorage } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillStorage.sys.mjs"
+ );
+ await formAutofillStorage.initialize();
+ // Use `sourceSync: true` to bypass field normalization which will
+ // fail due to the invalid credit card number.
+ await formAutofillStorage.creditCards.add(TEST_CREDIT_CARD, {
+ sourceSync: true,
+ });
+
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "only one credit card is in storage");
+ is(
+ creditCards[0]["cc-number"],
+ "*********",
+ "invalid credit card number stored"
+ );
+ await testDialog(
+ EDIT_CREDIT_CARD_DIALOG_URL,
+ win => {
+ is(
+ win.document.querySelector("#cc-number").value,
+ INVALID_CREDIT_CARD_NUMBER,
+ "cc-number field should be showing invalid credit card number"
+ );
+ is(
+ win.document.querySelector("#cc-number").checkValidity(),
+ false,
+ "cc-number is invalid"
+ );
+ win.document.querySelector("#cancel").click();
+ },
+ {
+ record: creditCards[0],
+ skipDecryption: true,
+ }
+ );
+ ok(true, "Edit credit card dialog is closed");
+ creditCards = await getCreditCards();
+
+ is(creditCards.length, 1, "only one credit card is in storage");
+ is(
+ creditCards[0]["cc-number"],
+ "*********",
+ "invalid cc number still in record"
+ );
+ await removeCreditCards([creditCards[0].guid]);
+ await removeAddresses([addresses[0].guid]);
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 0, "Credit card storage is empty");
+ addresses = await getAddresses();
+ is(addresses.length, 0, "Address storage is empty");
+});
+
+add_task(async function test_editCreditCardWithInvalidNumber() {
+ const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_1);
+ await setStorage(TEST_CREDIT_CARD);
+
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "only one credit card is in storage");
+ await testDialog(
+ EDIT_CREDIT_CARD_DIALOG_URL,
+ win => {
+ ok(
+ win.document.documentElement
+ .querySelector("title")
+ .textContent.includes("Edit"),
+ "Edit card dialog title is correct"
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ is(
+ win.document.querySelector("#cc-number").validity.customError,
+ false,
+ "cc-number field should not have a custom error"
+ );
+ EventUtils.synthesizeKey("4111111111111112", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ is(
+ win.document.querySelector("#cc-number").validity.customError,
+ true,
+ "cc-number field should have a custom error"
+ );
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ win.document.querySelector("#cancel").click();
+ },
+ {
+ record: creditCards[0],
+ }
+ );
+ ok(true, "Edit credit card dialog is closed");
+ creditCards = await getCreditCards();
+
+ is(creditCards.length, 1, "only one credit card is in storage");
+ await removeCreditCards([creditCards[0].guid]);
+
+ creditCards = await getCreditCards();
+ is(creditCards.length, 0, "Credit card storage is empty");
+});
+
+add_task(async function test_noAutocompletePopupOnSystemTab() {
+ await setStorage(TEST_CREDIT_CARD_1);
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PRIVACY_PREF_URL },
+ async browser => {
+ // Open credit card manage dialog
+ await SpecialPowers.spawn(browser, [], async () => {
+ let button = content.document.querySelector(
+ "#creditCardAutofill button"
+ );
+ button.click();
+ });
+ let dialog = await waitForSubDialogLoad(
+ content,
+ MANAGE_CREDIT_CARDS_DIALOG_URL
+ );
+
+ // Open edit credit card dialog
+ await SpecialPowers.spawn(dialog, [], async () => {
+ let button = content.document.querySelector("#add");
+ button.click();
+ });
+ dialog = await waitForSubDialogLoad(content, EDIT_CREDIT_CARD_DIALOG_URL);
+
+ // Focus on credit card number field
+ await SpecialPowers.spawn(dialog, [], async () => {
+ let number = content.document.querySelector("#cc-number");
+ number.focus();
+ });
+
+ // autocomplete popup should not appear
+ await ensureNoAutocompletePopup(browser);
+ }
+ );
+
+ await removeAllRecords();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js b/browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js
new file mode 100644
index 0000000000..5de499b942
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js
@@ -0,0 +1,145 @@
+"use strict";
+
+// Remove the scheme from the URLs so we can switch between http: and https: later.
+const TEST_URL_PATH_CC =
+ "://example.org" +
+ HTTP_TEST_PATH +
+ "creditCard/autocomplete_creditcard_basic.html";
+const TEST_URL_PATH =
+ "://example.org" + HTTP_TEST_PATH + "autocomplete_basic.html";
+
+add_task(async function setup_storage() {
+ await setStorage(
+ TEST_ADDRESS_1,
+ TEST_ADDRESS_2,
+ TEST_ADDRESS_3,
+ TEST_CREDIT_CARD_1,
+ TEST_CREDIT_CARD_2,
+ TEST_CREDIT_CARD_3
+ );
+});
+
+add_task(async function test_insecure_form() {
+ async function runTest({
+ urlPath,
+ protocol,
+ focusInput,
+ expectedType,
+ expectedResultLength,
+ }) {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: protocol + urlPath },
+ async function (browser) {
+ await openPopupOn(browser, focusInput);
+
+ const items = getDisplayedPopupItems(browser);
+ is(
+ items.length,
+ expectedResultLength,
+ `Should show correct amount of results in "${protocol}"`
+ );
+ const firstItem = items[0];
+ is(
+ firstItem.getAttribute("originaltype"),
+ expectedType,
+ `Item should attach with correct binding in "${protocol}"`
+ );
+
+ await closePopup(browser);
+ }
+ );
+ }
+
+ const testSets = [
+ {
+ urlPath: TEST_URL_PATH,
+ protocol: "https",
+ focusInput: "#organization",
+ expectedType: "autofill-profile",
+ expectedResultLength: 2,
+ },
+ {
+ urlPath: TEST_URL_PATH,
+ protocol: "http",
+ focusInput: "#organization",
+ expectedType: "autofill-profile",
+ expectedResultLength: 2,
+ },
+ {
+ urlPath: TEST_URL_PATH_CC,
+ protocol: "https",
+ focusInput: "#cc-name",
+ expectedType: "autofill-profile",
+ expectedResultLength: 3,
+ },
+ {
+ urlPath: TEST_URL_PATH_CC,
+ protocol: "http",
+ focusInput: "#cc-name",
+ expectedType: "autofill-insecureWarning", // insecure warning field
+ expectedResultLength: 1,
+ },
+ ];
+
+ for (const test of testSets) {
+ await runTest(test);
+ }
+});
+
+add_task(async function test_click_on_insecure_warning() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "http" + TEST_URL_PATH_CC },
+ async function (browser) {
+ await openPopupOn(browser, "#cc-name");
+ const insecureItem = getDisplayedPopupItems(browser)[0];
+ let popupClosePromise = BrowserTestUtils.waitForPopupEvent(
+ browser.autoCompletePopup,
+ "hidden"
+ );
+ await EventUtils.synthesizeMouseAtCenter(insecureItem, {});
+ // Check input's value after popup closed to ensure the completion of autofilling.
+ await popupClosePromise;
+
+ const inputValue = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ return content.document.querySelector("#cc-name").value;
+ }
+ );
+ is(inputValue, "");
+
+ await closePopup(browser);
+ }
+ );
+});
+
+add_task(async function test_press_enter_on_insecure_warning() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "http" + TEST_URL_PATH_CC },
+ async function (browser) {
+ await openPopupOn(browser, "#cc-name");
+
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+
+ let popupClosePromise = BrowserTestUtils.waitForPopupEvent(
+ browser.autoCompletePopup,
+ "hidden"
+ );
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ // Check input's value after popup closed to ensure the completion of autofilling.
+ await popupClosePromise;
+
+ const inputValue = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ return content.document.querySelector("#cc-name").value;
+ }
+ );
+ is(inputValue, "");
+
+ await closePopup(browser);
+ }
+ );
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_manageCreditCardsDialog.js b/browser/extensions/formautofill/test/browser/creditCard/browser_manageCreditCardsDialog.js
new file mode 100644
index 0000000000..7f54140c78
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_manageCreditCardsDialog.js
@@ -0,0 +1,290 @@
+"use strict";
+
+const TEST_SELECTORS = {
+ selRecords: "#credit-cards",
+ btnRemove: "#remove",
+ btnAdd: "#add",
+ btnEdit: "#edit",
+};
+
+const DIALOG_SIZE = "width=600,height=400";
+
+add_task(async function test_manageCreditCardsInitialState() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: MANAGE_CREDIT_CARDS_DIALOG_URL },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [TEST_SELECTORS], args => {
+ let selRecords = content.document.querySelector(args.selRecords);
+ let btnRemove = content.document.querySelector(args.btnRemove);
+ let btnAdd = content.document.querySelector(args.btnAdd);
+ let btnEdit = content.document.querySelector(args.btnEdit);
+
+ is(selRecords.length, 0, "No credit card");
+ is(btnRemove.disabled, true, "Remove button disabled");
+ is(btnAdd.disabled, false, "Add button enabled");
+ is(btnEdit.disabled, true, "Edit button disabled");
+ });
+ }
+ );
+});
+
+add_task(async function test_cancelManageCreditCardsDialogWithESC() {
+ let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL);
+ await waitForFocusAndFormReady(win);
+ let unloadPromise = BrowserTestUtils.waitForEvent(win, "unload");
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ await unloadPromise;
+ ok(true, "Manage credit cards dialog is closed with ESC key");
+});
+
+add_task(async function test_removingSingleAndMultipleCreditCards() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.reduceTimerPrecision", false]],
+ });
+ await setStorage(
+ TEST_CREDIT_CARD_1,
+ TEST_CREDIT_CARD_2,
+ TEST_CREDIT_CARD_3,
+ TEST_CREDIT_CARD_4,
+ TEST_CREDIT_CARD_5
+ );
+
+ let win = window.openDialog(
+ MANAGE_CREDIT_CARDS_DIALOG_URL,
+ null,
+ DIALOG_SIZE
+ );
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+ let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove);
+ let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
+
+ const expectedLabels = [
+ {
+ id: "credit-card-label-number-name-2",
+ args: { number: "**** 1881", name: "Chris P. Bacon", type: "Visa" },
+ },
+ {
+ id: "credit-card-label-number-2",
+ args: { number: "**** 5100", type: "MasterCard" },
+ },
+ {
+ id: "credit-card-label-number-expiration-2",
+ args: {
+ number: "**** 7870",
+ month: "1",
+ year: "2000",
+ type: "MasterCard",
+ },
+ },
+ {
+ id: "credit-card-label-number-name-expiration-2",
+ args: {
+ number: "**** 1045",
+ name: "Timothy Berners-Lee",
+ month: "12",
+ year: (new Date().getFullYear() + 10).toString(),
+ type: "Visa",
+ },
+ },
+ {
+ id: "credit-card-label-number-name-expiration-2",
+ args: {
+ number: "**** 1111",
+ name: "John Doe",
+ month: "4",
+ year: new Date().getFullYear().toString(),
+ type: "Visa",
+ },
+ },
+ ];
+
+ is(
+ selRecords.length,
+ expectedLabels.length,
+ "Correct number of credit cards"
+ );
+ expectedLabels.forEach((expected, i) => {
+ const l10nAttrs = document.l10n.getAttributes(selRecords[i]);
+ is(
+ l10nAttrs.id,
+ expected.id,
+ `l10n id set for credit card ${expectedLabels.length - i}`
+ );
+ Object.keys(expected.args).forEach(arg => {
+ is(
+ l10nAttrs.args[arg],
+ expected.args[arg],
+ `Set display ${arg} for credit card ${expectedLabels.length - i}`
+ );
+ });
+ });
+
+ EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+ is(btnRemove.disabled, false, "Remove button enabled");
+ is(btnEdit.disabled, false, "Edit button enabled");
+ EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
+ is(selRecords.length, 4, "Four credit cards left");
+
+ EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+ EventUtils.synthesizeMouseAtCenter(
+ selRecords.children[3],
+ { shiftKey: true },
+ win
+ );
+ is(btnEdit.disabled, true, "Edit button disabled when multi-select");
+
+ EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
+ is(selRecords.length, 0, "All credit cards are removed");
+
+ win.close();
+});
+
+add_task(async function test_removingCreditCardsViaKeyboardDelete() {
+ await setStorage(TEST_CREDIT_CARD_1);
+ let win = window.openDialog(
+ MANAGE_CREDIT_CARDS_DIALOG_URL,
+ null,
+ DIALOG_SIZE
+ );
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+
+ is(selRecords.length, 1, "One credit card");
+
+ EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+ EventUtils.synthesizeKey("VK_DELETE", {}, win);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
+ is(selRecords.length, 0, "No credit cards left");
+
+ win.close();
+});
+
+add_task(async function test_creditCardsDialogWatchesStorageChanges() {
+ let win = window.openDialog(
+ MANAGE_CREDIT_CARDS_DIALOG_URL,
+ null,
+ DIALOG_SIZE
+ );
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+
+ await setStorage(TEST_CREDIT_CARD_1);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
+ is(selRecords.length, 1, "One credit card is shown");
+
+ await removeCreditCards([selRecords.options[0].value]);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
+ is(selRecords.length, 0, "Credit card is removed");
+ win.close();
+});
+
+add_task(async function test_showCreditCardIcons() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.reduceTimerPrecision", false]],
+ });
+ await setStorage(TEST_CREDIT_CARD_1);
+ await setStorage(TEST_CREDIT_CARD_3);
+
+ let win = window.openDialog(
+ MANAGE_CREDIT_CARDS_DIALOG_URL,
+ null,
+ DIALOG_SIZE
+ );
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+
+ is(
+ selRecords.classList.contains("branded"),
+ AppConstants.MOZILLA_OFFICIAL,
+ "record picker has 'branded' class in an MOZILLA_OFFICIAL build"
+ );
+
+ let option0 = selRecords.options[0];
+ let icon0Url = win.getComputedStyle(option0, "::before").backgroundImage;
+ let option1 = selRecords.options[1];
+ let icon1Url = win.getComputedStyle(option1, "::before").backgroundImage;
+
+ is(
+ option0.getAttribute("cc-type"),
+ "mastercard",
+ "Option has the expected cc-type"
+ );
+ is(
+ option1.getAttribute("cc-type"),
+ "visa",
+ "Option has the expected cc-type"
+ );
+
+ if (AppConstants.MOZILLA_OFFICIAL) {
+ ok(
+ icon0Url.includes("icon-credit-card-generic.svg"),
+ "unknown network option ::before element has the generic icon as backgroundImage: " +
+ icon0Url
+ );
+ ok(
+ icon1Url.includes("cc-logo-visa.svg"),
+ "visa option ::before element has the visa icon as backgroundImage " +
+ icon1Url
+ );
+ }
+
+ await removeCreditCards([option0.value, option1.value]);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
+ is(selRecords.length, 0, "Credit card is removed");
+ win.close();
+});
+
+add_task(async function test_hasEditLoginPrompt() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login on official builds."
+ );
+ return;
+ }
+
+ await setStorage(TEST_CREDIT_CARD_1);
+
+ let win = window.openDialog(
+ MANAGE_CREDIT_CARDS_DIALOG_URL,
+ null,
+ DIALOG_SIZE
+ );
+ await waitForFocusAndFormReady(win);
+
+ let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
+ let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove);
+ let btnAdd = win.document.querySelector(TEST_SELECTORS.btnAdd);
+ let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
+
+ EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(); // cancel
+ EventUtils.synthesizeMouseAtCenter(btnEdit, {}, win);
+ await osKeyStoreLoginShown;
+ await new Promise(resolve => waitForFocus(resolve, win));
+ await new Promise(resolve => executeSoon(resolve));
+
+ // Login is not required for removing credit cards.
+ EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
+ await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
+ is(selRecords.length, 0, "Credit card is removed");
+
+ // gSubDialog.open should be called when trying to add a credit card,
+ // no OS login dialog is required.
+ window.gSubDialog = {
+ open: url =>
+ is(url, EDIT_CREDIT_CARD_DIALOG_URL, "Edit credit card dialog is called"),
+ };
+ EventUtils.synthesizeMouseAtCenter(btnAdd, {}, win);
+ delete window.gSubDialog;
+
+ win.close();
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/head_cc.js b/browser/extensions/formautofill/test/browser/creditCard/head_cc.js
new file mode 100644
index 0000000000..42196e8422
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/head_cc.js
@@ -0,0 +1 @@
+/* import-globals-from ../head.js */
diff --git a/browser/extensions/formautofill/test/browser/empty.html b/browser/extensions/formautofill/test/browser/empty.html
new file mode 100644
index 0000000000..1ad28bb1f7
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/empty.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Empty file</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/browser/fathom/test-setup.sh b/browser/extensions/formautofill/test/browser/fathom/test-setup.sh
new file mode 100755
index 0000000000..1547c4a395
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/test-setup.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+# This script download samples for testing
+
+clean=1
+cleanall=0
+sample_dir=autofill-repo-samples
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ -c) clean=1 ;;
+ -cc) cleanall=1 ;;
+ esac
+ shift
+done
+
+# Check out source code
+if ! [ -d "fathom-form-autofill" ]; then
+ echo "Get samples from repo..."
+ git clone https://github.com/mozilla-services/fathom-form-autofill
+fi
+
+if ! [ -d "samples" ]; then
+ echo "Copy samples..."
+ mkdir $sample_dir
+ cp -r fathom-form-autofill/samples/testing $sample_dir
+ cp -r fathom-form-autofill/samples/training $sample_dir
+ cp -r fathom-form-autofill/samples/validation $sample_dir
+else
+ echo "\`samples\` directory already exists"
+fi
+
+if [ "$clean" = 1 ] || [ "$cleanall" = 1 ]; then
+ echo "Cleanup..."
+ rm -rf fathom-form-autofill
+fi
+
+if [ "$cleanall" = 1 ]; then
+ rm -rf $sample_dir
+fi
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/1.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/1.svg
new file mode 100644
index 0000000000..179f3b5cce
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/1.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="30" height="20" viewBox="0 0 30 20">
+ <path fill="#CCC" fill-rule="evenodd" d="M30 5V1a1 1 0 0 0-1-1H1a1 1 0 0 0-1 1v4h30zm0 4v10a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V9h30z"/>
+</svg>
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/10.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/10.svg
new file mode 100644
index 0000000000..619b82106d
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/10.svg
@@ -0,0 +1 @@
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="348.333px" height="348.333px" viewBox="0 0 348.333 348.334" style="enable-background:new 0 0 348.333 348.334;" xml:space="preserve"><g><path fill="#565656" d="M336.559,68.611L231.016,174.165l105.543,105.549c15.699,15.705,15.699,41.145,0,56.85c-7.844,7.844-18.128,11.769-28.407,11.769c-10.296,0-20.581-3.919-28.419-11.769L174.167,231.003L68.609,336.563c-7.843,7.844-18.128,11.769-28.416,11.769c-10.285,0-20.563-3.919-28.413-11.769c-15.699-15.698-15.699-41.139,0-56.85l105.54-105.549L11.774,68.611c-15.699-15.699-15.699-41.145,0-56.844c15.696-15.687,41.127-15.687,56.829,0l105.563,105.554L279.721,11.767c15.705-15.687,41.139-15.687,56.832,0C352.258,27.466,352.258,52.912,336.559,68.611z"/></g></svg> \ No newline at end of file
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/11.png b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/11.png
new file mode 100644
index 0000000000..446ba13cec
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/11.png
Binary files differ
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/12.gif b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/12.gif
new file mode 100644
index 0000000000..b3aa80d843
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/12.gif
Binary files differ
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/13.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/13.svg
new file mode 100644
index 0000000000..ed16723fa0
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/13.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="164.105px" height="48.177px" viewBox="0 21.835 164.105 48.177" enable-background="new 0 21.835 164.105 48.177"
+ xml:space="preserve">
+<path fill="#78BE20" d="M134.879,52.052l4.932-16.246l4.919,16.246H134.879z M98.851,57.406h-3.028V34.901h2.989
+ c4.53,0,9.571,1.875,9.571,11.2C108.383,54.361,103.918,57.406,98.851,57.406 M18.927,52.052l5.033-16.436l4.94,16.436H18.927z
+ M148.76,23.221h-16.905l-9.64,27.871c0.427-2.314,0.504-4.164,0.504-4.968c0-11.677-6.697-22.902-22.437-22.902H82.209
+ l0.056,27.724c-1.565-7.876-8.77-9.987-17.731-12.708c-3.348-1.03-5.186-2.666-4.516-4.193c0.574-1.332,2.626-1.75,5.118-1.405
+ c3.802,0.543,6.838,1.813,9.763,3.401l3.995-9.885c-0.902-0.442-7.374-4.323-15.485-4.323c-11.311,0-18.535,5.511-18.535,13.644
+ c0,7.249,4.469,11.462,12.686,13.823c8.839,2.562,11.079,3.589,10.814,6.13c-0.231,2.187-5.708,4.853-19.11-3.384l-3.982,8.23
+ L32.989,23.221h-16.9L0.115,69.285h13.321l2.078-6.524h16.797l1.987,6.524h13.985l-1.585-4.507
+ c4.729,2.73,10.591,5.228,17.604,5.228c10.747,0,16.555-5.86,17.939-11.477v10.756h18.025c10.74,0,16.361-5.118,19.298-10.591
+ l-3.687,10.591h13.312l2.162-6.524h16.781l1.931,6.524h14.001L148.76,23.221z"/>
+</svg>
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/14.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/14.svg
new file mode 100644
index 0000000000..21f7d71bf6
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/14.svg
@@ -0,0 +1,14 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
+ <defs>
+ <path id="a" d="M11.5 17a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1zm1.003-3h-1a.5.5 0 0 1-.5-.5v-7a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5zm-9.218 5h17.43a.5.5 0 0 0 .436-.746l-8.716-15.42a.5.5 0 0 0-.87 0l-8.716 15.42a.5.5 0 0 0 .436.746zM13.74 1.08l9.572 16.936A2 2 0 0 1 21.573 21H2.427a2 2 0 0 1-1.741-2.984L10.259 1.08a2 2 0 0 1 3.482 0z"/>
+ </defs>
+ <g fill="none" fill-rule="evenodd" transform="translate(4 6)">
+ <mask id="b" fill="#fff">
+ <use xlink:href="#a"/>
+ </mask>
+ <use fill="#d43030" xlink:href="#a"/>
+ <g fill="#d43030" mask="url(#b)">
+ <path d="M-4-6h32v32H-4z"/>
+ </g>
+ </g>
+</svg>
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/15.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/15.svg
new file mode 100644
index 0000000000..44c5c294d8
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/15.svg
@@ -0,0 +1 @@
+<svg height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="M17 19.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V17h-2.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H15v-2.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V15h2.5a.5.5 0 01.5.5v1a.5.5 0 01-.5.5H17zm-7.611 4.504L12.394 22H25.5a.5.5 0 00.5-.5v-11a.5.5 0 00-.5-.5h-19a.5.5 0 00-.5.5v11a.5.5 0 00.5.5H9v1.796a.25.25 0 00.389.208zM7 27.066V24H6a2 2 0 01-2-2V10a2 2 0 012-2h20a2 2 0 012 2v12a2 2 0 01-2 2H13l-5.223 3.482A.5.5 0 017 27.066z" fill="#3d3d3d"/></svg>
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/16.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/16.svg
new file mode 100644
index 0000000000..05105ed133
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/16.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="111px" height="33px" viewBox="0 0 111 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>ASDA Logo - White</title>
+ <g id="Footer-Amends" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Group" transform="translate(-1.000000, -1.000000)" fill="#FFFFFF">
+ <g id="Logo" transform="translate(1.000000, 1.000000)">
+ <path d="M100.697948,1.012292 L89.3366436,1.012292 L82.8584522,19.7642556 C83.148104,18.2068119 83.1992581,16.9618991 83.1992581,16.4207628 C83.1992581,8.56536178 78.6975861,1.012292 68.1226373,1.012292 L55.9778554,1.012292 L56.013619,19.6652524 C54.9634097,14.3658639 50.1220389,12.9452287 44.1009128,11.1157202 C41.8504643,10.4218107 40.6150151,9.3208018 41.0659906,8.29407301 C41.4513073,7.398942 42.8306965,7.11767529 44.5051631,7.34860889 C47.0601003,7.71502058 49.0999533,8.56990728 51.0644038,9.63765645 L53.7484443,2.98659136 C53.1426778,2.68836218 48.7941359,0.0782470703 43.3442306,0.0782470703 C35.744192,0.0782470703 30.8900881,3.78626686 30.8900881,9.25771912 C30.8900881,14.1342651 33.8930113,16.9692162 39.4152188,18.5582567 C45.353524,20.2807797 46.8595806,20.9735805 46.6813163,22.6821344 C46.5266361,24.1539896 42.8459763,25.947245 33.8399749,20.4062798 L31.1644602,25.9433647 L22.9041793,1.012292 L11.5474142,1.012292 L0.81413124,32.0042908 L9.76565687,32.0042908 L11.5,27 L22,27 L23.7839856,32.0042908 L33.1815042,32.0042908 L32.1156829,28.9725528 C35.293438,30.8094893 39.2329685,32.4896615 43.945236,32.4896615 C51.1667121,32.4896615 55.0690396,28.5473822 56,24.7678539 L56,32.0042908 L68.1107899,32.0042908 C75.3288336,32.0042908 79.1053795,28.561573 81.0783558,24.878498 L78.600814,32.0042908 L87.5461392,32.0042908 L89.3366436,27 L100,27 L101.571997,32.0042908 L110.981142,32.0042908 L100.697948,1.012292 Z M13.5,20 L16.8375459,9.35239857 L20,20 L13.5,20 Z M65,24.0118596 L65,9 L67.1348759,9 C70.178213,9 73.5,10.2318086 73.5,16.5059298 C73.5,22.063414 70.5657441,24.0118596 67.1610065,24.0118596 L65,24.0118596 Z M91.5,20 L94.6839085,9.47878566 L98,20 L91.5,20 Z" id="Fill-3"></path>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/17.bin b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/17.bin
new file mode 100644
index 0000000000..5fa299eb94
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/17.bin
Binary files differ
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/18.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/18.svg
new file mode 100644
index 0000000000..6f66ac60a2
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/18.svg
@@ -0,0 +1 @@
+<svg width="136" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none"><path d="M79.039 7.346c0 1.784-.449 3.186-1.346 4.206-.897 1.021-2.152 1.532-3.767 1.532-1.641 0-2.905-.505-3.791-1.513-.887-1.008-1.335-2.422-1.346-4.24 0-1.815.449-3.221 1.346-4.22.897-1 2.165-1.498 3.805-1.496 1.6 0 2.85.507 3.748 1.523.899 1.015 1.35 2.418 1.351 4.208zm-8.88 0c0 1.51.32 2.654.963 3.434.642.78 1.577 1.17 2.804 1.168 1.234 0 2.166-.388 2.796-1.165.63-.777.945-1.923.947-3.437 0-1.498-.314-2.634-.942-3.41-.627-.774-1.557-1.163-2.787-1.164-1.235 0-2.173.39-2.815 1.17-.642.78-.964 1.915-.964 3.404h-.002zm16.891 5.587V7.535c0-.68-.155-1.188-.466-1.523-.31-.336-.795-.504-1.455-.504-.874 0-1.514.236-1.922.708-.407.472-.61 1.251-.61 2.339v4.378h-1.265V4.575h1.028l.204 1.143h.062a2.583 2.583 0 011.076-.955 3.541 3.541 0 011.564-.339c1.006 0 1.763.242 2.271.727.508.484.762 1.26.762 2.327v5.455H87.05zm7.392.151c-1.234 0-2.208-.376-2.922-1.128-.714-.752-1.073-1.796-1.077-3.132 0-1.346.332-2.415.996-3.208a3.302 3.302 0 012.672-1.19 3.151 3.151 0 012.484 1.034c.61.689.915 1.597.915 2.723v.808h-5.75c.024.98.272 1.725.742 2.233a2.57 2.57 0 001.986.762 6.727 6.727 0 002.667-.566v1.128c-.409.18-.834.319-1.27.414-.476.09-.96.13-1.443.122zm-.344-7.597c-.609-.03-1.2.21-1.615.656a3.022 3.022 0 00-.705 1.814h4.378c0-.798-.18-1.409-.538-1.832a1.884 1.884 0 00-1.52-.638zm9.192 7.446h-1.294V2.94h-3.53V1.79h8.341v1.152h-3.517zm8.824-8.506c.335-.004.67.027.998.091l-.175 1.173c-.3-.07-.607-.108-.915-.113a2.228 2.228 0 00-1.733.824c-.488.57-.745 1.3-.72 2.05v4.48h-1.266V4.576h1.044l.146 1.547h.062c.27-.5.654-.93 1.119-1.257a2.521 2.521 0 011.44-.438zm3.748.148v5.422c0 .682.155 1.19.466 1.523.31.334.795.501 1.456.503.873 0 1.512-.238 1.916-.716.403-.477.605-1.256.605-2.338V4.575h1.265v8.342h-1.044l-.183-1.12h-.068c-.26.412-.633.74-1.076.945a3.627 3.627 0 01-1.574.328c-1.016 0-1.776-.241-2.282-.724-.506-.482-.759-1.255-.759-2.317V4.575h1.278zm13.781 6.079a2.09 2.09 0 01-.87 1.797c-.579.422-1.392.633-2.437.633-1.107 0-1.971-.18-2.592-.539v-1.16c.412.207.845.368 1.292.48.434.113.88.172 1.33.174.526.03 1.051-.08 1.522-.317a1.076 1.076 0 00.11-1.798 6.663 6.663 0 00-1.649-.807 8.931 8.931 0 01-1.658-.775 2.258 2.258 0 01-.737-.73 1.923 1.923 0 01-.24-.981 1.884 1.884 0 01.832-1.615c.554-.393 1.314-.59 2.28-.59a6.668 6.668 0 012.636.539l-.449 1.028a6.052 6.052 0 00-2.28-.52 2.624 2.624 0 00-1.345.27.872.872 0 00-.457.777.947.947 0 00.172.57c.15.188.338.341.552.45.474.237.963.443 1.464.616.99.36 1.659.718 2.007 1.077.36.383.546.896.517 1.42zm4.755 1.386c.217 0 .434-.016.648-.049.167-.024.332-.058.495-.102v.968a2.312 2.312 0 01-.605.165c-.238.04-.48.062-.721.064-1.615 0-2.422-.851-2.422-2.554v-4.97h-1.198v-.61l1.198-.539.538-1.784h.732v1.946h2.422v.982h-2.422v4.922c-.028.416.1.829.358 1.157.25.273.607.42.977.403z" fill="#6FBE4A"/><text transform="translate(.79 .615)" fill="#696969" font-family="ArialMT, Arial" font-size="12.109"><tspan x=".034" y="11">Powered by</tspan></text></g></svg> \ No newline at end of file
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/2.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/2.svg
new file mode 100644
index 0000000000..01977c77b0
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/2.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="30" height="20" viewBox="0 0 30 20">
+ <g fill="none" fill-rule="evenodd">
+ <rect width="30" height="20" fill="#353A48" rx="1"/>
+ <circle cx="11" cy="10" r="6" fill="#ED0006"/>
+ <circle cx="20" cy="10" r="6" fill="#F9A000"/>
+ <path fill="#FF8150" d="M15.5 6a5.731 5.731 0 0 1 1.597 4 5.731 5.731 0 0 1-1.597 4 5.731 5.731 0 0 1-1.597-4c0-1.567.612-2.983 1.597-4z"/>
+ </g>
+</svg>
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/3.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/3.svg
new file mode 100644
index 0000000000..bb024231ef
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/3.svg
@@ -0,0 +1 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 22"><defs><style>.cls-1{fill:#191f70;}.cls-2{fill:#fff;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1-2"><path class="cls-1" d="M.91,0H31.09A1,1,0,0,1,32,1V21a1,1,0,0,1-.91,1H.91A1,1,0,0,1,0,21V1A1,1,0,0,1,.91,0Z"/><path class="cls-2" d="M16.14,9.52c0,1.23,1.09,1.91,1.92,2.32s1.15.68,1.15,1.05c0,.58-.69.83-1.32.84a4.55,4.55,0,0,1-2.26-.54l-.4,1.87a6.82,6.82,0,0,0,2.45.45c2.31,0,3.82-1.15,3.83-2.91,0-2.24-3.1-2.38-3.08-3.38,0-.31.29-.63.93-.71a4.07,4.07,0,0,1,2.17.37l.39-1.8a6,6,0,0,0-2.06-.37c-2.17,0-3.7,1.16-3.73,2.81m9.51-2.66a1,1,0,0,0-.94.63l-3.3,7.89h2.31l.46-1.27H27l.26,1.27h2L27.52,6.86ZM26,9.17l.67,3.19H24.81ZM13.33,6.86l-1.81,8.51h2.2l1.82-8.51Zm-3.25,0-2.29,5.8L6.86,7.73a1,1,0,0,0-1-.87H2.09l0,.25a9.22,9.22,0,0,1,2.17.72.93.93,0,0,1,.53.75l1.75,6.79H8.82l3.57-8.51Z"/></g></g></svg> \ No newline at end of file
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/4.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/4.svg
new file mode 100644
index 0000000000..634943eee9
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/4.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="109" viewBox="0 0 24 109">
+ <g fill="none" fill-rule="nonzero">
+ <rect width="24" height="109" fill="#3D3D3D" rx="12"/>
+ <path stroke="#EEE" stroke-linecap="square" stroke-width="2" d="M5.5 50.5h13M5.5 55.25h13M5.5 60.25h13"/>
+ </g>
+</svg>
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/5.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/5.svg
new file mode 100644
index 0000000000..857064edc9
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/5.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="30" height="20" viewBox="0 0 30 20">
+ <g fill="none" fill-rule="evenodd">
+ <rect width="30" height="20" fill="#007ECD" rx="1"/>
+ <path fill="#FFF" d="M9.72 13H8.47l-.498-1.363H5.69L5.222 13H4l2.22-6h1.218l2.283 6zm-2.119-2.374L6.816 8.4l-.77 2.226H7.6zM10.316 13V7h1.723l1.034 4.093L14.096 7h1.727v6h-1.07V8.277L13.622 13h-1.109l-1.128-4.723V13h-1.07zm6.65 0V7h4.228v1.015h-3.077v1.33h2.863v1.011h-2.863v1.633h3.186V13h-4.337zm4.733 0l1.949-3.131L21.882 7h1.346l1.143 1.928L25.491 7h1.334l-1.773 2.914L27 13h-1.388l-1.264-2.075L23.08 13H21.7z"/>
+ </g>
+</svg>
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/6.svg b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/6.svg
new file mode 100644
index 0000000000..0678ad42f7
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/6.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="30" height="20" viewBox="0 0 30 20">
+ <g fill="none" fill-rule="evenodd">
+ <rect width="30" height="20" fill="#000052" rx="1"/>
+ <circle cx="11" cy="10" r="6" fill="#06C"/>
+ <circle cx="20" cy="10" r="6" fill="#C00"/>
+ <path fill="#C70B8C" d="M15.5 6a5.731 5.731 0 0 1 1.597 4 5.731 5.731 0 0 1-1.597 4 5.731 5.731 0 0 1-1.597-4c0-1.567.612-2.983 1.597-4z"/>
+ </g>
+</svg>
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/7.woff2 b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/7.woff2
new file mode 100644
index 0000000000..52b6d6916a
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/7.woff2
Binary files differ
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/8.woff2 b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/8.woff2
new file mode 100644
index 0000000000..e379beda77
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/8.woff2
Binary files differ
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/9.woff2 b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/9.woff2
new file mode 100644
index 0000000000..efa300c564
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/resources/sample/9.woff2
Binary files differ
diff --git a/browser/extensions/formautofill/test/browser/fathom/testing/sample.html b/browser/extensions/formautofill/test/browser/fathom/testing/sample.html
new file mode 100644
index 0000000000..ddb0f5f2da
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/fathom/testing/sample.html
@@ -0,0 +1,20 @@
+<html lang="en-GB"><!--
+ Page saved with SingleFile
+ url: https://www.asda.com/account?request_origin=asda&redirect_uri=https%3A%2F%2Fwww.asda.com%2Fgood-living%2Ftag%2Frecipes
+ saved date: Wed Mar 16 2022 11:21:53 GMT+0200 (Eastern European Standard Time)
+--><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0,shrink-to-fit=no"><meta name="referrer" content="no-referrer-when-downgrade"><title>My Account | Account Settings – Asda Groceries</title><style>:root{--sf-img-16: url("resources/sample/1.svg");--sf-img-19: url("resources/sample/2.svg");--sf-img-20: url("resources/sample/3.svg");--sf-img-15: url("resources/sample/4.svg");--sf-img-17: url("resources/sample/5.svg");--sf-img-18: url("resources/sample/6.svg")}</style><style>html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}article *,article *:before,article *:after{box-sizing:border-box}@font-face{font-family:"SourceSansProBold";src:url(resources/sample/7.woff2) format("woff2")}@font-face{font-family:"SourceSansProSemiBold";src:url(resources/sample/8.woff2) format("woff2")}@font-face{font-family:"SourceSansProRegular";src:url(resources/sample/9.woff2) format("woff2")}h1,h2,h3,h4,p,a{color:#191919;padding-bottom:8px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}h1{font-size:1.25em;font-weight:700;color:#191919;padding-bottom:16px;line-height:1.25}.theme-ghs h1{font-family:SourceSansProBold,sans-serif}.theme-george h1{font-family:LatoBold,sans-serif}h2{font-size:1.125em;font-weight:800}.theme-ghs h2{font-family:SourceSansProBold,sans-serif}.theme-george h2{font-family:LatoBold,sans-serif}h3{font-size:1em;font-weight:600}.theme-ghs h3{font-family:SourceSansProSemiBold,sans-serif}.theme-george h3{font-family:LatoRegular,sans-serif}h4{font-size:.875em;font-weight:400}.theme-ghs h4{font-family:SourceSansProRegular,sans-serif}.theme-george h4{font-family:LatoRegular,sans-serif}p{color:off-black;padding-bottom:16px;font-size:.875em;letter-spacing:.2px;line-height:1.3}.theme-ghs p{font-family:SourceSansProRegular,sans-serif}.theme-george p{font-family:LatoRegular,sans-serif}a{margin-bottom:16px;padding-bottom:0;font-weight:500;font-size:1em}.theme-ghs a{color:#0073b1;text-decoration:none;font-family:SourceSansProBold,sans-serif}.theme-george a{color:#191919;text-decoration:underline;font-family:LatoRegular,sans-serif}.button-as-link{display:inline;background-color:transparent;border:0;padding:0;width:auto!important;height:auto!important;color:#0073b1;margin-left:0;padding-bottom:16px;font-weight:500;text-decoration:none}.theme-ghs .button-as-link{font-size:.875em}.theme-george .button-as-link{font-size:12px}.theme-ghs .button-as-link{color:#0073b1}.theme-george .button-as-link{color:#191919}.short-password-error{bottom:62px}.blank-password-error{bottom:50px}a:hover{cursor:pointer}.theme-ghs strong{font-family:SourceSansProBold,sans-serif}.theme-george strong{font-family:LatoBold,sans-serif}.input-error{font-family:SourceSansProRegular,sans-serif;font-size:.75em;color:#d43030;line-height:1.125em}.theme-ghs .input-error{font-family:SourceSansProRegular,sans-serif;font-size:.75em;color:#d43030;clear:both}.theme-george .input-error{font-family:LatoBold,sans-serif;font-size:.875em;color:#da291c;clear:both}.theme-ghs .input-error a{font-family:SourceSansProRegular,sans-serif;color:#d43030;text-decoration:underline}.theme-george .input-error a{font-family:LatoBold,sans-serif;color:#da291c;text-decoration:underline}.left{float:left!important}.right{float:right!important;text-align:right!important}.theme-ghs .right{padding-top:3px;line-height:1.5}.theme-george .right{line-height:1.5}.center{text-align:center!important}.red{color:#da0500!important}.orange{color:#fdb45b!important}.pink{color:#ec938e!important}.yellow{color:#fae100!important}.green{color:#68a51c!important}.blue{color:#0073b1!important}.grey{color:grey1!important}.white{color:#fff!important}.black{color:#191919!important}.regular{font-weight:400!important}.bold{font-weight:700!important}@media (min-width:768px){.main-container.login{margin-top:32px}}form{border-bottom:1px solid #ccc;padding:8px 0 16px;margin-bottom:16px}form label.for-username{padding-bottom:4px;display:block}.theme-ghs form label.for-username{font-family:SourceSansProRegular,sans-serif;color:#3d3d3d;font-size:.875em;padding:0 0 4px}.theme-george form label.for-username{font-family:LatoRegular,sans-serif;color:#191919;font-size:14px;padding:0 0 8px}form .username-box.error{height:74px}form .username-box .alert{top:4px}form .login-links{width:100%;height:30px;padding:10px 0;margin-bottom:10px}form .login-links a{font-size:14px}form .form-error{margin-bottom:20px;margin-top:5px;font-family:SourceSansProRegular,sans-serif;font-size:.75em;color:#da291c;line-height:16px;clear:both}form .form-error a{color:#da291c;font-family:SourceSansProRegular,sans-serif;text-decoration:underline}form .dropdown-wrapper.phone-login-dropdown{float:none}form .dropdown-wrapper.phone-login-dropdown .dropdown-list::-webkit-scrollbar-thumb{height:150px}form .dropdown-wrapper{width:100%}form .dropdown-wrapper .list-wrapper .dropdown-list{width:498px;border:solid 1px #ccc;height:192px;padding:20px 20px}form .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{display:flex;font-size:14px;font-weight:normal;padding-bottom:12px;justify-content:start}form .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{width:auto;text-align:left;padding-right:15px}form .dropdown-header.phone-login-dropdown-header{height:44px;width:unset;border-radius:4px;border:solid 1px #ccc;margin-bottom:10px}form .dropdown-header.phone-login-dropdown-header .country-code{flex-grow:1;font-weight:normal;font-size:14px;margin-left:10px}form .remember-me{bottom:38px;width:130px}.theme-ghs form .remember-me{text-decoration:none}.theme-george form .remember-me{text-decoration:underline}a{user-select:none}a.login-link{color:#da291c;text-decoration:underline}.login-container .softLogin-form{border-bottom:0;margin:0;padding:8px 0 0}.login-container button.reset-pwd{border:0;font-size:14px;padding:0px;height:auto;width:auto;margin:0 0 16px 0;letter-spacing:inherit;text-transform:inherit;-webkit-font-smoothing:antialiased}.theme-ghs .login-container button.reset-pwd{color:#0073b1;text-decoration:none;font-family:SourceSansProBold,sans-serif}.theme-george .login-container button.reset-pwd{color:#191919;text-decoration:underline;font-family:LatoRegular,sans-serif}.reset-password-container .code-link{display:inline-block;padding-bottom:0;margin-top:16px}.reset-password-container button.basic{margin-top:20px}.reset-password-container form{border-bottom:0;margin-bottom:0}.reset-password-container .input-box input::placeholder{font-size:14px;color:#767676}.reset-code form{padding-bottom:0}.reset-code .input-box input.half,.reset-code button{width:48%;margin-right:0}.reset-code .reset-code-container{position:relative;padding-bottom:27px}.reset-code .reset-code-container .input-error{position:absolute;top:57px}.reset-code .reset-code-container .alert{top:20px;left:38%}@media (min-width:768px){.reset-code .reset-code-container .alert{left:44%}}.reset-code .code-label{display:inline;padding-bottom:0}.req-new-code{margin-top:8px;margin-bottom:8px;display:inline-block}.theme-ghs .req-new-code{font-family:SourceSansProBold,sans-serif;text-decoration:none;padding:0;text-transform:none}.theme-george .req-new-code{font-family:LatoBold,sans-serif;text-decoration:underline;font-size:14px;padding:0;text-transform:none}.try-other-email{font-size:1em}@media (min-width:768px){.reset-code .input-box input.half{width:54%}.reset-code button{width:44%}}.recaptcha-container{border-bottom:1px #ccc solid;margin-bottom:20px}.recaptcha-container .g-recaptcha{margin:10px 0 20px 0}.reset-confirmation{clear:fix();height:240px}.reset-confirmation button.full{margin-bottom:16px}.reset-confirmation button.full{width:100%;margin:0 0 16px;float:left}.theme-ghs .reset-confirmation button.next-dest a{color:#68a51c}.theme-george .reset-confirmation button.next-dest a{color:#191919}.password-strength{clear:both;font-family:SourceSansProRegular,sans-serif}.password-strength .strength-label{font-family:SourceSansProSemiBold,sans-serif;font-size:.875em}.password-strength .password-strength-measure{color:#767676;font-size:.875em;margin-top:5px;display:flex;justify-content:space-between}.theme-ghs .password-strength .password-strength-measure{font-family:SourceSansProRegular,sans-serif}.theme-george .password-strength .password-strength-measure{font-family:LatoRegular,sans-serif}.password-strength .strength-text{font-size:.875em}.password-strength .strength-indicator{display:inline-block;height:5px;border-radius:5px;position:relative;bottom:5px}.password-strength .strength-indicator.invalid{background-color:#c2c2c2;width:10%}.password-strength .strength-indicator.weak{background:#d43030;width:20%}.password-strength .strength-indicator.fair{background:#fbc42c;width:50%}.password-strength .strength-indicator.strong{background:#68a51c;width:100%}.password-strength .strength-indicator-bar{background:#e9e9e9;width:100%;height:5px;border-radius:5px}.password-strength .strength-desc{margin-top:16px}.password-strength .strength-desc button{font-size:.875em;display:inline;background-color:transparent;border:0;padding:0;color:#0073b1;margin-left:0;text-transform:none;letter-spacing:normal;margin-bottom:16px;font-weight:500}.theme-ghs .password-strength .strength-desc button{color:#0073b1;letter-spacing:.5px}.theme-george .password-strength .strength-desc button{color:#191919;letter-spacing:.5px}.password-strength .strength-desc button .chevron{padding-left:6px}.password-strength .strength-desc button .chevron.flipped{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg);padding-top:0;padding-bottom:0;padding-left:0;padding-right:6px}.password-strength .strength-desc .strength-para{margin-bottom:16px}.password-strength .strength-desc .strength-para p{padding:0}.password-strength .strength-desc .strength-para ul{list-style-type:unset;list-style-position:inside;margin-top:8px}.password-strength .strength-desc .strength-para ul li{font-family:SourceSansProRegular,sans-serif;font-size:.875em;margin-left:8px;margin-bottom:4px}.postcode-wrap{width:100%;float:left;margin-bottom:8px}.postcode-wrap .custom-input.half{width:50%;text-transform:uppercase}.postcode-wrap .custom-input.half+.input-error{clear:both;line-height:17px;padding-top:5px}.regCheckTnC .register-link{font-size:100%}.theme-ghs .regCheckTnC .register-link{font-family:SourceSansProBold,sans-serif}.theme-george .regCheckTnC .register-link{font-family:LatoBold,sans-serif}.regCheckTnC .input-error{font-size:.875em}.register-container form{border-bottom:0;padding-bottom:0;margin-bottom:0}.register-container form button+p{padding-top:20px;padding-bottom:0}.register-container .register-link{font-size:100%}.theme-ghs .register-container .register-link{font-family:SourceSansProBold,sans-serif}.theme-george .register-container .register-link{font-family:LatoBold,sans-serif}.register-container .george-reward-link{font-size:14px;font-family:LatoBold,sans-serif}.password-container{position:relative}.password-container .button-show-password{position:absolute;padding-bottom:0;text-decoration:none;right:12px;text-align:center;display:inline-block;margin:0;width:40px!important;transition:all 200ms ease;padding:0}.theme-ghs .password-container .button-show-password{font-family:SourceSansProSemiBold,sans-serif;top:30px;text-transform:none;font-size:1em}.theme-george .password-container .button-show-password{font-family:LatoRegular,sans-serif;top:37px;text-transform:uppercase;font-size:.75em}.password-container .input-box input.show-password{padding-right:60px}.password-container .show-password{padding-right:65px;margin-bottom:7px}.co-third-party-login__container .primary{margin-bottom:16px}.co-third-party-login__container h3{margin:7.2px 0 24px 0}.co-login-disclaimer p{padding:16px 0 0}.co-login-disclaimer a{font-family:SourceSansProRegular;font-weight:bold;font-size:.95em}form#cd-reg-form{border-bottom:0;padding-bottom:0;margin-bottom:0}form#cd-reg-form .form-error{margin-bottom:20px;margin-top:5px;font-family:SourceSansProRegular,sans-serif;font-size:.75em;color:#da291c;line-height:16px;clear:both}form#cd-reg-form input:last-child{margin-bottom:16px}a{user-select:none}a.login-link{color:#da291c;text-decoration:underline}.login-container .softLogin-form{border-bottom:0;margin:0;padding:8px 0 0}.page-mask{position:fixed;width:100vw;height:100vh;left:0;top:0;background-color:#000;opacity:.5}.cd-modal{position:absolute;z-index:1000;top:10vh;left:calc(50% - 200px);width:400px;height:auto;border-radius:2px;background-color:#fff;box-shadow:0 5px 10px 0 rgba(0,0,0,.2)}.cd-modal h1{padding:20px;border-bottom:1px solid #ccc}.cd-modal p{padding:20px 40px 20px 20px;border-bottom:1px solid #ccc}.theme-george .register-container a.tc-link-register{font-family:LatoBold,sans-serif}.theme-george .register-container .password-strength .strength-desc button{text-decoration:underline}.app{display:flex;flex-wrap:wrap;align-items:flex-start;flex-direction:row}.container-wrapper{height:auto;flex-basis:calc((100% * 1) - 16px);min-width:0;margin:8px;margin-left:calc(100% * 0 + (16px / 2))!important;min-height:calc(100vh - 200px)}.theme-ghs .container-wrapper{min-height:calc(100vh - 300px)}.theme-george .container-wrapper{min-height:calc(100vh - 261px)}@media (min-width:481px){.container-wrapper{flex-basis:calc((100% * 0.6666666667) - 16px);min-width:0;margin:8px;margin-left:calc(100% * 0.1666666667 + (16px / 2))!important;min-height:calc(100vh - 200px)}}@media (min-width:768px){.container-wrapper{flex-basis:calc((100% * 0.5) - 16px);min-width:0;margin:8px;margin-left:calc(100% * 0.25 + (16px / 2))!important}}@media (min-width:961px){.container-wrapper{flex-basis:calc((100% * 0.3333333333) - 16px);min-width:0;margin:8px;margin-left:calc(100% * 0.3333333333 + (16px / 2))!important}}.main-container{height:auto;padding:4px;max-width:432px;margin:0 auto}@media (min-width:481px){.main-container{border:1px solid #ccc}.theme-ghs .main-container{padding:20px;box-shadow:0 0}.theme-george .main-container{padding:32px;box-shadow:4px 4px 4px 0 rgba(0,0,0,.06)}}footer{width:100%;margin-top:60px}.theme-ghs footer{background-color:#191919;min-height:100px}.theme-george footer{background-color:#fff;min-height:60px}.theme-ghs footer{border-top:0}.theme-george footer{border-top:1px solid #cbcbcb}footer .up-arrow{margin-left:5px;-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);transform:rotate(-90deg);height:10px;width:10px}.footer-wrapper{position:relative;max-width:1024px;margin:0 auto;padding:16px}.footer-top{position:absolute;display:flex;justify-content:space-between;flex-direction:row;top:-50px}.footer-top .back-top button{font-weight:600;letter-spacing:.2px;color:#191919;font-family:SourceSansProSemiBold,sans-serif;font-size:13px;display:inline;background-color:transparent;border:0;padding:0}.footer-top a.website-feedback-link{align-items:center;color:#191919;display:none}.footer-top .feedback-icon{width:30px;height:30px;margin-right:5px}.footer-main{text-align:center}.footer-logo{margin:40px 0 40px}.footer-george-logo{font-family:LatoRegular,sans-serif}@media (min-width:320px){.footer-george-logo{position:absolute;bottom:16px;width:95%;text-align:center}}.footer-links{font-family:SourceSansProSemiBold,sans-serif;margin:8px 0;display:-webkit-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:center;-ms-justify-content:center;justify-content:center}.footer-links ul{display:-webkit-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;-moz-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;max-width:320px;-webkit-justify-content:center;-ms-justify-content:center;justify-content:center;width:100%}.footer-links li{font-size:.8125em;letter-spacing:.2px}.theme-ghs .footer-links li{color:#fff}.theme-george .footer-links li{color:#191919}.theme-george .footer-links li{border-right:1px solid #cbcbcb}.theme-ghs .footer-links li{padding:4px}.theme-george .footer-links li{padding:0}.theme-ghs .footer-links li{padding-right:0}.theme-george .footer-links li{padding-right:4px}.theme-ghs .footer-links li{margin:0}.theme-george .footer-links li{margin:4px}.footer-links li a{font-size:100%}.theme-ghs .footer-links li a{color:#fff}.theme-george .footer-links li a{color:#191919}.theme-ghs .footer-links li a{font-family:SourceSansProSemiBold,sans-serif}.theme-george .footer-links li a{font-family:LatoRegular,sans-serif}.theme-george .footer-links li a{text-decoration:none}.footer-links li a:hover{text-decoration:underline}.footer-links li:last-child{border-right:0}@media (min-width:768px){footer{min-height:102px}.footer-main{display:-webkit-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:space-between;-ms-justify-content:space-between;justify-content:space-between;-webkit-align-items:center;-moz-align-items:center;-ms-align-items:center;align-items:center;width:100%}.footer-logo{margin:0}.footer-links{margin:0;order:-1;-webkit-align-self:flex-end;-moz-align-self:flex-end;-ms-align-self:flex-end;align-self:flex-end}.footer-links ul{max-width:100%}.footer-top .back-top{top:-55px}}@media (min-width:320px){.theme-ghs footer{height:205px}.theme-george footer{height:92px}.theme-ghs .footer-wrapper{height:205px}.theme-george .footer-wrapper{height:92px}.footer-top{top:-50px}}@media (min-width:768px){.theme-ghs footer{height:100px}.theme-george footer{height:60px}.theme-ghs .footer-wrapper{height:100px}.theme-george .footer-wrapper{height:60px}.footer-george-logo{font-family:LatoRegular,sans-serif;position:relative;bottom:0;width:auto;text-align:left}}header{height:92px;width:100%;background-color:#fff;transition-property:margin-top;transition-duration:.5s;transition-delay:.5s}.theme-ghs header{box-shadow:0 2px 4px 0 #7f7f7f;margin-bottom:32px}.theme-george header{box-shadow:0 2px 4px 0 #cbcbcb;margin-bottom:32px}header[data-apply-onetrust=true]{margin-top:150px}header .header-container{display:flex;height:92px;justify-content:space-between;align-items:center;background-color:#fff;width:100%;max-width:1024px;margin:0 auto;padding:8px}header .header-container #logo{padding:0}header .header-container #logo img{width:93px}.theme-ghs header .header-container nav{margin-bottom:0}.theme-george header .header-container nav{margin-bottom:8px}header .header-container nav button{width:142px;letter-spacing:normal;text-transform:none}.theme-ghs header .header-container nav button{width:200px;background-image:none}.theme-george header .header-container nav button{width:120px;border:0;background-image:none}header .header-container nav button:hover{box-shadow:none}header .header-container nav .need-help{display:none}@media (min-width:320px){header{margin-bottom:24px;height:48px;transition-property:margin-top;transition-duration:.5s;transition-delay:.5s}header[data-apply-onetrust=true]{margin-top:150px}.theme-ghs header{margin-bottom:24px}.theme-george header{margin-bottom:24px}header .header-container{height:48px;padding:13px 16px 10px}header .header-container #logo{margin-bottom:0}header .header-container nav{margin-bottom:0}.theme-ghs header .header-container nav{margin-bottom:2px}.theme-george header .header-container nav{margin-bottom:2px}.theme-ghs header .header-container nav button{font-family:SourceSansProSemiBold,sans-serif;width:144px;margin-right:0;margin-left:8px}.theme-george header .header-container nav button{font-family:LatoRegular,sans-serif;width:88px;padding:16px 0 16px 12px;text-align:right;font-size:12px;margin-right:0;margin-left:8px}header .header-container nav button.basic:hover{background-image:none}}@media (min-width:481px){header{height:92px;margin-bottom:24px;padding-top:0;transition-property:margin-top;transition-duration:.5s;transition-delay:.5s}header[data-apply-onetrust=true]{margin-top:125px}header .header-container{height:92px;width:100%;padding:16px}.theme-ghs header .header-container{padding-top:20px}.theme-george header .header-container{padding-top:31px}header .header-container #logo img{width:113px}.theme-george header .header-container nav button{border:0}}@media (min-width:768px){header{transition-property:margin-top;transition-duration:.5s;transition-delay:.5s}header[data-apply-onetrust=true]{margin-top:140px}.theme-ghs header .header-container{padding-top:20px}.theme-george header .header-container{padding-top:31px}.theme-ghs header .header-container nav button{text-align:center}.theme-george header .header-container nav button{text-align:center;padding:0}header .header-container nav .need-help{display:inline-block}.theme-ghs header .header-container nav .need-help{background-image:none;text-transform:none;font-size:18px;width:200px}.theme-george header .header-container nav .need-help{border-right:1px solid #cbcbcb;border-radius:0;background-image:none;text-transform:none;font-size:14px;width:120px}.theme-ghs header .header-container nav .back-shop{background-image:none;width:200px;text-transform:none;font-size:18px}.theme-george header .header-container nav .back-shop{background-image:none;width:120px;text-transform:none;font-size:14px}}@media (min-width:961px){header{padding-top:0;transition-property:margin-top;transition-duration:.5s;transition-delay:.5s}.theme-ghs header{margin-bottom:32px}.theme-george header{margin-bottom:32px}header[data-apply-onetrust=true]{margin-top:75px}header .header-container{height:92px}.theme-ghs header .header-container{padding-top:20px}.theme-george header .header-container{padding-top:31px}header .header-container #logo img{width:140px}.theme-ghs header .header-container nav button{font-family:SourceSansProSemiBold,sans-serif}.theme-george header .header-container nav button{width:150px;border:0;border-radius:0;padding:0;height:18px;font-family:LatoRegular,sans-serif;margin:0}}@media (min-width:768px){.layout__section{min-height:640px}}@media (min-width:1024px){.layout__section{display:flex}.layout__main{flex:1 1 auto}}@media (min-width:1280px){.site-width.layout__section{margin:0 auto;max-width:1392px;padding:0 16px}.site-width.layout__main{flex:1 1 auto}}.onetrust-consent-sdk-box[data-apply-ot-banner-popup=true] .onetrust-pc-dark-filter.ot-hide{display:block!important}.onetrust-consent-sdk-box #onetrust-banner-sdk:focus{outline-color:none!important;outline-width:0px!important}.onetrust-consent-sdk-box #onetrust-banner-sdk{background-color:#fff!important;max-width:500px;min-width:500px;width:100%;top:23%;left:50%!important;transform:translate(-50%,-25%);padding:16px 22px 16px 24px;border-radius:2px;z-index:2147483646!important}.onetrust-consent-sdk-box #onetrust-banner-sdk #onetrust-accept-btn-handler{background-color:#0073b1;border-color:#0073b1;color:#fff;border-radius:4px}.onetrust-consent-sdk-box #onetrust-banner-sdk #onetrust-pc-btn-handler{background-color:#fff;border:1px solid #0073b1;color:#0073b1;border-radius:4px}.onetrust-consent-sdk-box #onetrust-banner-sdk #onetrust-group-container button{color:#0073b1;display:inline}.onetrust-consent-sdk-box #onetrust-banner-sdk #onetrust-policy-title,.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-dpd-title{width:100%!important}.onetrust-consent-sdk-box #onetrust-banner-sdk #onetrust-policy-text{width:100%!important;line-height:1.5}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-b-addl-desc{width:100%!important;border-right:0;color:#000}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-dpd-content .ot-dpd-desc{line-height:1.5}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-dpd-container{width:100%!important;padding:0!important}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row{display:flex;flex-direction:column}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row #onetrust-group-container{width:100%}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row #onetrust-group-container p,.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row #onetrust-group-container h3{color:#000}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row #onetrust-button-group-parent{position:relative!important;width:100%;left:auto!important;top:auto;transform:translateY(0%);right:0%}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row #onetrust-button-group-parent #onetrust-button-group{display:flex;justify-content:space-evenly}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row #onetrust-button-group-parent #onetrust-button-group #onetrust-pc-btn-handler,.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container .ot-sdk-row #onetrust-button-group-parent #onetrust-button-group #onetrust-accept-btn-handler{margin-right:0px;max-width:112px;width:100%;margin-top:1em}.onetrust-consent-sdk-box #onetrust-banner-sdk #onetrust-policy{margin:0px}.onetrust-consent-sdk-box #onetrust-pc-sdk{background-color:#fff!important;color:#000;max-width:500px!important;min-width:100%;padding:7px 16px 25px}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-pc-content{overflow-x:hidden}.onetrust-consent-sdk-box #onetrust-pc-sdk *:focus{outline:none!important}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-fltr-cnt,.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-anchor{background-color:#fff!important}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-fltr-cnt{width:auto}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-sel-blk{background-color:#fff}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-pc-header{padding:0}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-pc-title,.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-pc-content,.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-category-title{color:#000!important;width:auto;margin:0;padding:0;padding-bottom:5px}.onetrust-consent-sdk-box #onetrust-pc-sdk #onetrust-group-container button{color:#0073b1}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-pc-desc,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-accordion-layout .ot-cat-header{color:#000!important;line-height:1.5!important;min-height:20px}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-lst-title span{color:#000!important;line-height:1.5}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-ven-name{color:#000!important;font-weight:400!important;line-height:1.5}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-pc-desc{margin-bottom:10px}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-pc-desc .privacy-notice-link{color:#0073b1;text-decoration:none}.onetrust-consent-sdk-box #onetrust-pc-sdk #accept-recommended-btn-handler,.onetrust-consent-sdk-box #onetrust-pc-sdk .save-preference-btn-handler,.onetrust-consent-sdk-box #onetrust-pc-sdk #filter-apply-handler,.onetrust-consent-sdk-box #onetrust-pc-sdk #filter-cancel-handler{background-color:#0073b1!important;border-color:#0073b1!important;color:#fff!important;margin-bottom:10px;border-radius:4px;margin-right:25px}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-cat-grp{margin-top:0px}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-acc-grpdesc,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-subgrp-desc,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-subgrp h5,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-ven-disc h4,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-ven-dets p,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-ven-dets span,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-label-txt{color:#000!important;font-size:12px!important}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-ven-dets h4,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-ven-pur h4,.onetrust-consent-sdk-box #onetrust-pc-sdk #clear-filters-handler{color:#000!important}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-vlst-cntr a,.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-ven-link{color:#0073b1!important;padding:0px}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-vlst-cntr button{color:#0073b1!important}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-tgl input:checked+.ot-switch .ot-switch-nob{background-color:#0073b1;border:1px solid #fff}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-tgl input:checked+.ot-switch .ot-switch-nob:before{background-color:#fff;border-color:#fff;width:9px;height:9px}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-switch{width:28px;height:11px;padding:1px 1px 1px 4px}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-switch-nob:before{background-color:#fff;width:9px;height:9px;border-radius:5.5px}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-switch-nob{background-color:#767676;border-radius:5.5px;padding-left:1px}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-acc-hdr{min-height:auto}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-always-active{color:#000}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-pc-footer-logo a{margin-right:25px}.onetrust-consent-sdk-box #onetrust-pc-sdk #filter-btn-handler{width:30px;height:30px;border-radius:4px;background-color:#538316}.onetrust-consent-sdk-box #onetrust-pc-sdk #ot-sel-blk{background-color:#fff!important}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-chkbox label::before{border:1px solid #538316}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-chkbox input:checked~label::before{background-color:#538316}.onetrust-consent-sdk-box #onetrust-pc-sdk .ot-plus-minus{width:15px;height:15px}.onetrust-consent-sdk-box #onetrust-pc-sdk #vendor-search-handler{border-radius:4px}@media (max-width:767px){.onetrust-consent-sdk-box #onetrust-banner-sdk{top:28%;max-width:480px;min-width:325px;height:fit-content}.onetrust-consent-sdk-box #onetrust-banner-sdk .ot-sdk-container{padding:0px}}@media (min-width:768px)and (max-width:1023px){.onetrust-consent-sdk-box #onetrust-banner-sdk{top:23%;max-width:452px;min-width:452px;height:fit-content!important}}@media (min-width:768px){.onetrust-consent-sdk-box #onetrust-pc-sdk{min-width:500px!important}}@media (min-width:1024px){.onetrust-consent-sdk-box #onetrust-banner-sdk{height:fit-content}}button,a.reg-link,.register-nav-link{width:200px;height:40px;border:1px solid #191919;border-radius:4px;text-align:center;user-select:none;margin:0 5px;line-height:14px;font-weight:500;letter-spacing:.5px;cursor:pointer;background:transparent;transition:all 200ms ease}.theme-ghs button,.theme-ghs a.reg-link,.theme-ghs .register-nav-link{font-family:SourceSansProSemiBold,sans-serif;height:40px;font-size:18px;padding:11px}.theme-george button,.theme-george a.reg-link,.theme-george .register-nav-link{font-family:LatoBold,sans-serif;text-transform:uppercase;height:42px;font-size:14px;padding:14px}button a,a.reg-link a,.register-nav-link a{color:inherit;font-size:inherit;font-weight:inherit;font-family:inherit}.theme-george button a,.theme-george a.reg-link a,.theme-george .register-nav-link a{text-decoration:none}button.primary,a.reg-link.primary,.register-nav-link.primary{color:#fff;text-decoration:none}.theme-ghs button.primary,.theme-ghs a.reg-link.primary,.theme-ghs .register-nav-link.primary{background-color:#0073b1;border-color:#0073b1}.theme-george button.primary,.theme-george a.reg-link.primary,.theme-george .register-nav-link.primary{background-image:linear-gradient(to bottom,#424242 35%,black);border-color:#191919}button.primary:hover,a.reg-link.primary:hover,.register-nav-link.primary:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.theme-ghs button.primary:hover,.theme-ghs a.reg-link.primary:hover,.theme-ghs .register-nav-link.primary:hover{background-color:#005a8b}.theme-george button.primary:hover,.theme-george a.reg-link.primary:hover,.theme-george .register-nav-link.primary:hover{background-image:linear-gradient(to bottom,black 35%,#424242)}.theme-ghs button.secondary,.theme-ghs a.reg-link.secondary,.theme-ghs .register-nav-link.secondary{color:#fff;background-color:#68a51c;border-color:#68a51c}.theme-george button.secondary,.theme-george a.reg-link.secondary,.theme-george .register-nav-link.secondary{color:#fff;background-color:#191919;background-image:linear-gradient(to bottom,#424242 35%,black);border-color:#191919;text-transform:uppercase}button.secondary:hover,a.reg-link.secondary:hover,.register-nav-link.secondary:hover{background-color:#538316;box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.theme-george button.secondary:hover,.theme-george a.reg-link.secondary:hover,.theme-george .register-nav-link.secondary:hover{background-image:linear-gradient(to bottom,black 35%,#424242)}.theme-ghs button.secondary:disabled,.theme-ghs a.reg-link.secondary:disabled,.theme-ghs .register-nav-link.secondary:disabled{background-color:#68a51c;opacity:.5;cursor:not-allowed}.theme-george button.secondary:disabled,.theme-george a.reg-link.secondary:disabled,.theme-george .register-nav-link.secondary:disabled{background-color:#191919;background-image:linear-gradient(to bottom,#424242 35%,black);opacity:.5;cursor:not-allowed}.theme-ghs button.secondary.empty,.theme-ghs a.reg-link.secondary.empty,.theme-ghs .register-nav-link.secondary.empty{border-color:#68a51c;color:#68a51c;background-color:#fff}.theme-george button.secondary.empty,.theme-george a.reg-link.secondary.empty,.theme-george .register-nav-link.secondary.empty{border-color:#191919;color:#fff}.theme-ghs button.destructive,.theme-ghs a.reg-link.destructive,.theme-ghs .register-nav-link.destructive{color:#d43030;background-color:#fff;border-color:#d43030}button.destructive:hover,a.reg-link.destructive:hover,.register-nav-link.destructive:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.theme-ghs button.destructive:hover,.theme-ghs a.reg-link.destructive:hover,.theme-ghs .register-nav-link.destructive:hover{border-width:2px}.theme-ghs button.basic,.theme-ghs a.reg-link.basic,.theme-ghs .register-nav-link.basic{background-color:#fff;border-color:#7ebd2f;color:#68a51c}.theme-george button.basic,.theme-george a.reg-link.basic,.theme-george .register-nav-link.basic{background-image:linear-gradient(to bottom,#fdfdfd,#d8d8d8);border-color:#b9b9b9;color:#191919}button.basic:hover,a.reg-link.basic:hover,.register-nav-link.basic:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.theme-ghs button.basic:hover,.theme-ghs a.reg-link.basic:hover,.theme-ghs .register-nav-link.basic:hover{border-width:2px}.theme-george button.basic:hover,.theme-george a.reg-link.basic:hover,.theme-george .register-nav-link.basic:hover{background-image:linear-gradient(to bottom,#d8d8d8,white);border-width:1px}button.full,a.reg-link.full,.register-nav-link.full{width:100%;border-radius:4px;margin:0}@media (min-width:320px){button.half,a.reg-link.half,.register-nav-link.half{width:100%}}@media (min-width:768px){button.half,a.reg-link.half,.register-nav-link.half{width:40%;float:left;margin-right:8px}}button.center,a.reg-link.center,.register-nav-link.center{margin:20px auto;display:block;float:none}@keyframes circle1s{0%{left:0;top:0;width:15px;height:15px;z-index:100}25%{left:26.25px;top:0;width:15px;height:15px;z-index:100}50%{left:52.5px;top:0;width:15px;height:15px;z-index:100}75%{left:52.5px;top:7.5px;width:0;height:0;z-index:0}76%{left:7.5px;top:7.5px;width:0;height:0;z-index:0}100%{left:0;top:0;width:15px;height:15px;z-index:100}}@keyframes circle2s{0%{left:26.25px;top:0;width:15px;height:15px;z-index:100}25%{left:52.5px;top:0;width:15px;height:15px;z-index:100}50%{left:52.5px;top:7.5px;width:0;height:0;z-index:0}51%{left:7.5px;top:7.5px;width:0;height:0;z-index:0}75%{left:0;top:0;width:15px;height:15px;z-index:100}100%{left:26.25px;top:0;width:15px;height:15px;z-index:100}}@keyframes circle3s{0%{left:52.5px;top:0;width:15px;height:15px;z-index:100}25%{left:52.5px;top:7.5px;width:0;height:0;z-index:0}26%{left:7.5px;top:7.5px;width:0;height:0;z-index:0}50%{left:0;top:0;width:15px;height:15px;z-index:100}75%{left:26.25px;top:0;width:15px;height:15px;z-index:100}100%{left:52.5px;top:0;width:15px;height:15px;z-index:100}}@keyframes circle4s{0%{left:7.5px;top:7.5px;width:0;height:0;z-index:0}25%{left:0;top:0;width:15px;height:15px;z-index:100}50%{left:26.25px;top:0;width:15px;height:15px;z-index:100}75%{left:52.5px;top:0;width:15px;height:15px;z-index:100}100%{left:52.5px;top:7.5px;width:0;height:0;z-index:0}}button #loading,a.reg-link #loading,.register-nav-link #loading{display:block;margin:0 auto;width:67.5px;height:15px;position:relative}.theme-ghs button #loading,.theme-ghs a.reg-link #loading,.theme-ghs .register-nav-link #loading{margin-top:0}.theme-george button #loading,.theme-george a.reg-link #loading,.theme-george .register-nav-link #loading{margin-top:-2px}button #loading .animation,a.reg-link #loading .animation,.register-nav-link #loading .animation{position:relative}button #loading .animation .circle,a.reg-link #loading .animation .circle,.register-nav-link #loading .animation .circle{position:absolute;border-radius:100%;border:1px solid #fff;animation-name:circle1s;animation-duration:1.1s;animation-iteration-count:infinite}.theme-ghs button #loading .animation .circle,.theme-ghs a.reg-link #loading .animation .circle,.theme-ghs .register-nav-link #loading .animation .circle{background-color:#fff;border-color:#fff}.theme-george button #loading .animation .circle,.theme-george a.reg-link #loading .animation .circle,.theme-george .register-nav-link #loading .animation .circle{background-color:#d9dddf;border-color:#d9dddf}button #loading .animation .circle:nth-child(2),a.reg-link #loading .animation .circle:nth-child(2),.register-nav-link #loading .animation .circle:nth-child(2){animation-name:circle2s}.theme-ghs button #loading .animation .circle:nth-child(2),.theme-ghs a.reg-link #loading .animation .circle:nth-child(2),.theme-ghs .register-nav-link #loading .animation .circle:nth-child(2){background-color:#fff;border-color:#fff}.theme-george button #loading .animation .circle:nth-child(2),.theme-george a.reg-link #loading .animation .circle:nth-child(2),.theme-george .register-nav-link #loading .animation .circle:nth-child(2){background-color:#b8babd;border-color:#b8babd}button #loading .animation .circle:nth-child(3),a.reg-link #loading .animation .circle:nth-child(3),.register-nav-link #loading .animation .circle:nth-child(3){animation-name:circle3s}.theme-ghs button #loading .animation .circle:nth-child(3),.theme-ghs a.reg-link #loading .animation .circle:nth-child(3),.theme-ghs .register-nav-link #loading .animation .circle:nth-child(3){background-color:#fff;border-color:#fff}.theme-george button #loading .animation .circle:nth-child(3),.theme-george a.reg-link #loading .animation .circle:nth-child(3),.theme-george .register-nav-link #loading .animation .circle:nth-child(3){background-color:#a3a3ab;border-color:#a3a3ab}button #loading .animation .circle:nth-child(4),a.reg-link #loading .animation .circle:nth-child(4),.register-nav-link #loading .animation .circle:nth-child(4){animation-name:circle4s}.theme-ghs button #loading .animation .circle:nth-child(4),.theme-ghs a.reg-link #loading .animation .circle:nth-child(4),.theme-ghs .register-nav-link #loading .animation .circle:nth-child(4){background-color:#fff;border-color:#fff}.theme-george button #loading .animation .circle:nth-child(4),.theme-george a.reg-link #loading .animation .circle:nth-child(4),.theme-george .register-nav-link #loading .animation .circle:nth-child(4){background-color:#efefef;border-color:#efefef}a.reg-link,.register-nav-link{display:block;-webkit-font-smoothing:auto}.saved{width:88px;margin:0 auto;display:block}.saved .tick{float:left;margin-right:8px;margin-top:-2px}.saved .saved-text{width:auto;float:left}label.check-box{clear:both;margin-bottom:16px;display:block;font-size:.875em;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;position:relative;padding-left:34px;line-height:1.5;color:#3d3d3d}.theme-ghs label.check-box{font-family:SourceSansProRegular,sans-serif}.theme-george label.check-box{font-family:LatoRegular,sans-serif}label.check-box .checkmark{position:absolute;top:0;left:0;border:1px solid;background-color:#fff}.theme-ghs label.check-box .checkmark{width:20px;height:20px;border-radius:2px;border-color:#68a51c}.theme-george label.check-box .checkmark{width:24px;height:24px;border-radius:5px;border-color:#ccc}label.check-box .checkmark.error{border-color:#d43030}label.check-box .checkmark:after{content:"";position:absolute;display:none;border:solid #fff;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.theme-ghs label.check-box .checkmark:after{left:6px;top:0;width:6px;height:14px;border-width:0 2px 2px 0}.theme-george label.check-box .checkmark:after{left:7px;top:3px;width:8px;height:13px;border-width:0 3px 3px 0}.theme-ghs label.check-box .checkmark.disabled{background-color:#fff;border:1px solid #7ebd2f;opacity:.5;cursor:not-allowed!important}.theme-george label.check-box .checkmark.disabled{background-color:#d8d8d8;border:1px solid #d8d8d8;opacity:.5;cursor:not-allowed!important}label.check-box input[type=checkbox]{position:absolute;opacity:0;height:20px;width:20px;margin:0;left:0}label.check-box input[type=checkbox]:focus~.checkmark,label.check-box input[type=checkbox]:active~.checkmark{outline:#75aee8 auto 5px}.theme-ghs label.check-box input[type=checkbox]:checked~.checkmark{background-color:#68a51c}.theme-george label.check-box input[type=checkbox]:checked~.checkmark{background-color:#191919}label.check-box input[type=checkbox]:checked~.checkmark:after{display:block}label.check-box-tc{padding-left:54px}label.check-box-tc .checkmark{width:40px;height:40px;border-radius:8px;border:solid 2px #767676;background-color:#fff}label.check-box-tc .checkmark:after{left:14px;top:6px;width:12.3px;height:17.8px;border:solid #68a51c;border-width:0 3px 3px 0}label.check-box-tc input[type=checkbox]:checked~.checkmark{background-color:#fff}.input-error{margin-bottom:8px;margin-top:4px}.input-box{position:relative}.input-box__wrapper{position:relative;overflow-wrap:break-word}.theme-ghs .input-box__wrapper.read-only{font-size:1em;font-family:SourceSansProRegular,sans-serif;color:#3d3d3d}.theme-george .input-box__wrapper.read-only{font-size:.875em;font-family:LatoRegular,sans-serif;color:#191919}.input-box.half{width:calc(50% - 4px);margin-right:4px;float:left}.input-box.half.last{margin-left:4px;margin-right:0}.input-box.custom-icon{position:relative}.input-box.custom-icon:before{content:"";position:absolute}.input-box.custom-icon input{padding-left:38px}.input-box label{padding-bottom:4px;display:block}.theme-ghs .input-box label{font-family:SourceSansProRegular,sans-serif;color:#3d3d3d;font-size:.875em;padding:0 0 4px}.theme-george .input-box label{font-family:LatoRegular,sans-serif;color:#191919;font-size:14px;padding:0 0 8px}.input-box .alert{position:absolute;top:3px;right:12px}.theme-ghs .input-box .alert{display:block}.theme-george .input-box .alert{display:none}.input-box input{width:100%;border-radius:4px;border:1px solid #ccc;clear:both;transition:border 200ms ease,box-shadow 200ms ease}.theme-ghs .input-box input{height:40px;font-size:1em;padding:12px;margin-bottom:12px;font-family:SourceSansProRegular,sans-serif;color:#3d3d3d}.theme-george .input-box input{height:42px;font-size:.875em;padding:14px 12px;margin-bottom:16px;font-family:LatoRegular,sans-serif;color:#191919}.input-box input:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1);cursor:default}.input-box input:focus{border-color:#767676}.input-box input::placeholder{color:#767676}.input-box input.half{width:40%;float:left;margin-right:10}.input-box input.error{margin-bottom:0}.theme-ghs .input-box input.error{border:2px solid #d43030}.theme-george .input-box input.error{border:1px solid #da291c}.input-box input.read-only{border:0;outline:0;padding:0;margin:0;height:20px;pointer-events:none;background:0;box-shadow:none}.input-box.check-box-grouped label.check-box{width:20px;padding-left:unset;float:left}.input-box.check-box-grouped input[type=checkbox]{width:20px;top:0;left:0;height:20px;padding:0;margin:0}.input-box.check-box-grouped label{line-height:20px;padding-left:34px}.input-box.check-box-grouped .input-error{margin-top:-10px}.theme-ghs .input-error{margin:4px 0 16px}.theme-george .input-error{margin:8px 0 16px}.input-warning{padding:8px;margin:-1px 0 10px 16px;background-color:#ebf1f5;color:#000;font-size:.75em;font-family:SourceSansProRegular,sans-serif;z-index:100;display:inline-block;position:relative}.input-warning .triangle{position:absolute;top:-20px}.input-label{padding-bottom:4px;display:block}.theme-ghs .input-label{font-family:SourceSansProRegular,sans-serif;color:#3d3d3d;font-size:.875em}.theme-george .input-label{font-family:LatoRegular,sans-serif;color:#191919;font-size:14px}label.radio{margin-bottom:16px;display:block;font-size:.875em;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;position:relative;padding-left:28px;padding-right:16px;line-height:1.5}.theme-ghs label.radio{font-family:SourceSansProRegular,sans-serif}.theme-george label.radio{font-family:LatoRegular,sans-serif}label.radio .checkmark{position:absolute;top:0;left:0;height:20px;width:20px;background-color:#fff;border-radius:90px;transition:all 250ms ease}.theme-ghs label.radio .checkmark{border:1px solid #68a51c}.theme-george label.radio .checkmark{border:1px solid #191919}label.radio .checkmark:hover{border-width:2px}label.radio .checkmark.error{border-color:#d43030}label.radio.property-type-radio{float:left}label.radio input[type=radio]{position:absolute;opacity:0}label.radio input[type=radio]:focus~.checkmark,label.radio input[type=radio]:active~.checkmark{outline:#75aee8 auto 5px}.theme-ghs label.radio input[type=radio]:checked~.checkmark{border:1px solid #68a51c}.theme-george label.radio input[type=radio]:checked~.checkmark{border:1px solid #191919}label.radio input[type=radio]:checked~.checkmark:before{content:"";display:inline-block;width:18px;height:18px;position:relative;border-radius:100%;vertical-align:top;text-align:center;cursor:pointer;transition:all 250ms ease}.theme-ghs label.radio input[type=radio]:checked~.checkmark:before{background-color:#68a51c}.theme-george label.radio input[type=radio]:checked~.checkmark:before{background-color:#fff}.theme-ghs label.radio input[type=radio]:checked~.checkmark:before{box-shadow:inset 0 0 0 4px #fff}.theme-george label.radio input[type=radio]:checked~.checkmark:before{box-shadow:inset 0 0 0 5px #191919}@keyframes circle1{0%{left:0;top:0;width:40px;height:40px;z-index:100}25%{left:70px;top:0;width:40px;height:40px;z-index:100}50%{left:140px;top:0;width:40px;height:40px;z-index:100}75%{left:140px;top:20px;width:0;height:0;z-index:0}76%{left:20px;top:20px;width:0;height:0;z-index:0}100%{left:0;top:0;width:40px;height:40px;z-index:100}}@keyframes circle2{0%{left:70px;top:0;width:40px;height:40px;z-index:100}25%{left:140px;top:0;width:40px;height:40px;z-index:100}50%{left:140px;top:20px;width:0;height:0;z-index:0}51%{left:20px;top:20px;width:0;height:0;z-index:0}75%{left:0;top:0;width:40px;height:40px;z-index:100}100%{left:70px;top:0;width:40px;height:40px;z-index:100}}@keyframes circle3{0%{left:140px;top:0;width:40px;height:40px;z-index:100}25%{left:140px;top:20px;width:0;height:0;z-index:0}26%{left:20px;top:20px;width:0;height:0;z-index:0}50%{left:0;top:0;width:40px;height:40px;z-index:100}75%{left:70px;top:0;width:40px;height:40px;z-index:100}100%{left:140px;top:0;width:40px;height:40px;z-index:100}}@keyframes circle4{0%{left:20px;top:20px;width:0;height:0;z-index:0}25%{left:0;top:0;width:40px;height:40px;z-index:100}50%{left:70px;top:0;width:40px;height:40px;z-index:100}75%{left:140px;top:0;width:40px;height:40px;z-index:100}100%{left:140px;top:20px;width:0;height:0;z-index:0}}#loading{display:block;margin:calc(50vh - 171px) auto;width:180px;height:40px}#loading .animation{position:relative}#loading .animation .circle{position:absolute;background-color:#d9dddf;border-radius:100%;border:1px solid #d9dddf;animation-name:circle1;animation-duration:1.1s;animation-iteration-count:infinite}#loading .animation .circle:nth-child(2){background-color:#b8babd;border-color:#b8babd;animation-name:circle2}#loading .animation .circle:nth-child(3){background-color:#a3a3ab;border-color:#a3a3ab;animation-name:circle3}#loading .animation .circle:nth-child(4){background-color:#efefef;border-color:#efefef;animation-name:circle4}.asda-backdrop{bottom:0;left:0;position:fixed;right:0;top:0;z-index:10}.asda-backdrop[data-color=transparent]{background-color:transparent}.asda-backdrop[data-color=black]{background-color:rgba(68,70,82,.88)}.dropdown-list-item.selected,.dropdown-list-item:hover{color:#fff;background-color:#767676}.dropdown-wrapper-single{user-select:none;position:relative;width:265px}.dropdown-wrapper-single .dropdown-header{border:1px solid #ccc}.dropdown-wrapper-single .dropdown-header .dropdown-header-name{font-weight:400}.dropdown-wrapper-single .dropdown-list{border:1px solid #ccc;border-top:0}.dropdown-wrapper{font-family:SourceSansProRegular,sans-serif;font-size:24px;font-weight:600;font-style:normal;font-stretch:normal;line-height:normal;letter-spacing:.3px;float:left;margin-right:16px}.dropdown-wrapper .dropdown-header{display:flex;align-items:center;justify-content:space-between;width:176px;height:78px;border:1px solid #767676;cursor:default;position:relative;background-color:#fff;padding:20px}.dropdown-wrapper .dropdown-header.active{border-bottom:0px;top:1px;z-index:11}.dropdown-wrapper .list-wrapper .dropdown-list{z-index:10;position:absolute;width:560px;height:636px;border:1px solid #767676;background-color:#fff;padding:20px 75px 20px 20px;overflow-y:scroll;color:#767676}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{width:100%;padding:8px 0px;line-height:1.54px;cursor:default;display:flex;align-items:center;justify-content:space-between;text-overflow:ellipsis}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item:nth-child(5){padding-bottom:18px;border-bottom:2px solid #979797}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{width:420px;text-align:left;padding-left:15px}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{width:80px;background-color:none}.dropdown-wrapper .dropdown-list::-webkit-scrollbar-thumb{background-image:var(--sf-img-15);height:109px;background-repeat:no-repeat}.dropdown-wrapper .dropdown-list-item.selected,.dropdown-wrapper .dropdown-list-item:hover{color:#fff;background-color:#767676}div[class^=country-flag-]{width:34px;height:20px;background-size:34px 20px}div[class^=country-flag-] img{width:34px;height:20px;background-repeat:no-repeat;background-size:34px 20px}.modal-wrapper{position:absolute;z-index:1000;top:206px;width:802px;height:300px;border-radius:14px;box-shadow:2px 3px 8px 3px rgba(0,0,0,.1);border:solid 1px #ccc;background-color:#fff;padding:30px;font-family:SourceSansProRegular,sans-serif;font-weight:600;font-style:normal;font-stretch:normal;line-height:normal;color:#3d3d3d}.phone-input{width:100%}@media (min-width:320px){.phone-input{display:flex;flex-direction:column}}@media (min-width:481px){.phone-input{flex-direction:row;height:52px}}.phone-input .dropdown-wrapper{float:left;height:42px;margin-right:8px;color:#3d3d3d;font-family:SourceSansProRegular,sans-serif;transition:all 200ms ease;font-size:1em;user-select:none}@media (min-width:320px){.phone-input .dropdown-wrapper{width:100%}}@media (min-width:481px){.phone-input .dropdown-wrapper{width:32%}}.phone-input .dropdown-wrapper .dropdown-header{cursor:pointer;width:100%;height:40px;padding:0 8px;border-radius:4px;border:1px solid #ccc;transition:box-shadow 200ms ease}.phone-input .dropdown-wrapper .dropdown-header.active{height:41px;border-top-color:#767676;border-right-color:#767676;border-left-color:#767676;border-bottom-left-radius:0;border-bottom-right-radius:0;top:0}.phone-input .dropdown-wrapper .dropdown-header.active .country-code{transition:none;margin-top:-1px}.phone-input .dropdown-wrapper .dropdown-header.active div[class^=country-flag-] img{transition:none;margin-top:-1px}.phone-input .dropdown-wrapper .dropdown-header.active .down-arrow{transform:rotate(0.5turn);padding-bottom:4px}.phone-input .dropdown-wrapper .dropdown-header:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.phone-input .dropdown-wrapper .dropdown-header .country-code{font-size:16px;padding-left:4px}@media (min-width:320px){.phone-input .dropdown-wrapper .dropdown-header .country-code{flex:1;padding-left:15px;display:flex;align-items:center}}.phone-input .dropdown-wrapper .dropdown-header .country-code .country-name{width:100px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:215px}@media (min-width:481px){.phone-input .dropdown-wrapper .dropdown-header .country-code .country-name{display:none}}.phone-input .dropdown-wrapper .dropdown-header .down-arrow{width:20px;padding-top:4px}.phone-input .dropdown-wrapper .dropdown-header div[class^=country-flag-] img{width:32px;background-repeat:no-repeat;background-size:34px 20px}.phone-input .dropdown-wrapper .list-wrapper{position:relative}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list{margin-top:-1px;z-index:10;position:absolute;height:200px;border:1px solid #767676;border-radius:0 0 4px 4px;background-color:#fff;padding:0;overflow-y:scroll;color:#767676}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list{width:100%}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list{width:372px}}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list::-webkit-scrollbar *{background:transparent}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{color:#191919;height:40px;font-size:1em}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item:hover{background-color:#f6f6f6}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{padding:12px 8px 12px 8px;height:48px}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{padding:12px 80px 12px 8px;height:40px}}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{line-height:initial}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{width:calc(100% - 32px)}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{width:420px}}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{text-align:right}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{width:60px}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{width:80px}}@media (min-width:320px){.phone-input .input-box{width:100%;margin-top:12px}}@media (min-width:481px){.phone-input .input-box{width:calc(68% - 8px);margin-top:0px}}.divider{width:100%;border-top:1px solid #cbcbcb;margin-bottom:24px}.react-datepicker__year-read-view--down-arrow,.react-datepicker__month-read-view--down-arrow,.react-datepicker__month-year-read-view--down-arrow,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle,.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle{margin-left:-8px;position:absolute}.react-datepicker__year-read-view--down-arrow,.react-datepicker__month-read-view--down-arrow,.react-datepicker__month-year-read-view--down-arrow,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle,.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle,.react-datepicker__year-read-view--down-arrow::before,.react-datepicker__month-read-view--down-arrow::before,.react-datepicker__month-year-read-view--down-arrow::before,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before{box-sizing:content-box;position:absolute;border:8px solid transparent;height:0;width:1px}.react-datepicker__year-read-view--down-arrow::before,.react-datepicker__month-read-view--down-arrow::before,.react-datepicker__month-year-read-view--down-arrow::before,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before{content:"";z-index:-1;border-width:8px;left:-8px}.theme-ghs .react-datepicker__year-read-view--down-arrow::before,.theme-ghs .react-datepicker__month-read-view--down-arrow::before,.theme-ghs .react-datepicker__month-year-read-view--down-arrow::before,.theme-ghs .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=top] .theme-ghs .react-datepicker__triangle::before,.theme-ghs .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .theme-ghs .react-datepicker__triangle::before{border-bottom-color:#538316}.theme-george .react-datepicker__year-read-view--down-arrow::before,.theme-george .react-datepicker__month-read-view--down-arrow::before,.theme-george .react-datepicker__month-year-read-view--down-arrow::before,.theme-george .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=top] .theme-george .react-datepicker__triangle::before,.theme-george .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .theme-george .react-datepicker__triangle::before{border-bottom-color:#020202}.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle{top:0;margin-top:-8px}.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle,.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before{border-top:0}.theme-ghs .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle,.react-datepicker-popper[data-placement^=bottom] .theme-ghs .react-datepicker__triangle,.theme-ghs .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .theme-ghs .react-datepicker__triangle::before{border-bottom-color:#538316}.theme-george .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle,.react-datepicker-popper[data-placement^=bottom] .theme-george .react-datepicker__triangle,.theme-george .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .theme-george .react-datepicker__triangle::before{border-bottom-color:#020202}.react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before{top:-1px}.theme-ghs .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .theme-ghs .react-datepicker__triangle::before{border-bottom-color:#538316}.theme-george .react-datepicker-popper[data-placement^=bottom] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=bottom] .theme-george .react-datepicker__triangle::before{border-bottom-color:#020202}.react-datepicker__year-read-view--down-arrow,.react-datepicker__month-read-view--down-arrow,.react-datepicker__month-year-read-view--down-arrow,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle{bottom:0;margin-bottom:-8px}.react-datepicker__year-read-view--down-arrow,.react-datepicker__month-read-view--down-arrow,.react-datepicker__month-year-read-view--down-arrow,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle,.react-datepicker__year-read-view--down-arrow::before,.react-datepicker__month-read-view--down-arrow::before,.react-datepicker__month-year-read-view--down-arrow::before,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before{border-bottom:0}.theme-ghs .react-datepicker__year-read-view--down-arrow,.theme-ghs .react-datepicker__month-read-view--down-arrow,.theme-ghs .react-datepicker__month-year-read-view--down-arrow,.theme-ghs .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle,.react-datepicker-popper[data-placement^=top] .theme-ghs .react-datepicker__triangle,.theme-ghs .react-datepicker__year-read-view--down-arrow::before,.theme-ghs .react-datepicker__month-read-view--down-arrow::before,.theme-ghs .react-datepicker__month-year-read-view--down-arrow::before,.theme-ghs .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=top] .theme-ghs .react-datepicker__triangle::before{border-top-color:#538316}.theme-george .react-datepicker__year-read-view--down-arrow,.theme-george .react-datepicker__month-read-view--down-arrow,.theme-george .react-datepicker__month-year-read-view--down-arrow,.theme-george .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle,.react-datepicker-popper[data-placement^=top] .theme-george .react-datepicker__triangle,.theme-george .react-datepicker__year-read-view--down-arrow::before,.theme-george .react-datepicker__month-read-view--down-arrow::before,.theme-george .react-datepicker__month-year-read-view--down-arrow::before,.theme-george .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=top] .theme-george .react-datepicker__triangle::before{border-top-color:#020202}.react-datepicker__year-read-view--down-arrow::before,.react-datepicker__month-read-view--down-arrow::before,.react-datepicker__month-year-read-view--down-arrow::before,.react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before{bottom:-1px}.theme-ghs .react-datepicker__year-read-view--down-arrow::before,.theme-ghs .react-datepicker__month-read-view--down-arrow::before,.theme-ghs .react-datepicker__month-year-read-view--down-arrow::before,.theme-ghs .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=top] .theme-ghs .react-datepicker__triangle::before{border-top-color:#538316}.theme-george .react-datepicker__year-read-view--down-arrow::before,.theme-george .react-datepicker__month-read-view--down-arrow::before,.theme-george .react-datepicker__month-year-read-view--down-arrow::before,.theme-george .react-datepicker-popper[data-placement^=top] .react-datepicker__triangle::before,.react-datepicker-popper[data-placement^=top] .theme-george .react-datepicker__triangle::before{border-top-color:#020202}.react-datepicker-wrapper{display:inline-block;padding:0;border:0;width:100%}.react-datepicker_input-default{font-size:13px;border-radius:4px;line-height:16px;padding:6px 10px 5px;width:100%;height:40px}.theme-ghs .react-datepicker_input-default{border:1px solid #538316}.theme-george .react-datepicker_input-default{border:1px solid #020202}.react-datepicker_input-default:hover{box-shadow:0px 2px 10px 0px rgba(0,0,0,.1)}.theme-ghs .react-datepicker_input-default:focus{border-color:#37570f}.theme-george .react-datepicker_input-default:focus{border-color:#000}.react-datepicker{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:.8rem;background-color:#fff;border-radius:.3rem;display:inline-block;position:relative}.theme-ghs .react-datepicker{color:#000;border:1px solid #538316}.theme-george .react-datepicker{color:#000;border:1px solid #020202}.react-datepicker--time-only .react-datepicker__triangle{left:35px}.react-datepicker--time-only .react-datepicker__time-container{border-left:0}.react-datepicker--time-only .react-datepicker__time{border-radius:.3rem}.react-datepicker--time-only .react-datepicker__time-box{border-radius:.3rem}.react-datepicker__triangle{position:absolute;left:50px}.react-datepicker-popper{z-index:1}.react-datepicker-popper[data-placement=bottom-end] .react-datepicker__triangle,.react-datepicker-popper[data-placement=top-end] .react-datepicker__triangle{left:auto;right:50px}.react-datepicker-popper[data-placement^=top]{margin-bottom:10px}.react-datepicker-popper[data-placement^=right]{margin-left:8px}.react-datepicker-popper[data-placement^=right] .react-datepicker__triangle{left:auto;right:42px}.react-datepicker-popper[data-placement^=left]{margin-right:8px}.react-datepicker-popper[data-placement^=left] .react-datepicker__triangle{left:42px;right:auto}.react-datepicker__header{text-align:center;border-top-left-radius:.3rem;border-top-right-radius:.3rem;padding-top:8px;position:relative}.theme-ghs .react-datepicker__header{background-color:#538316;border-bottom:1px solid #538316}.theme-george .react-datepicker__header{background-color:#020202;border-bottom:1px solid #020202}.react-datepicker__header--time{padding-bottom:8px;padding-left:5px;padding-right:5px}.react-datepicker__year-dropdown-container--select,.react-datepicker__month-dropdown-container--select,.react-datepicker__month-year-dropdown-container--select,.react-datepicker__year-dropdown-container--scroll,.react-datepicker__month-dropdown-container--scroll,.react-datepicker__month-year-dropdown-container--scroll{display:inline-block;margin:0 2px}.react-datepicker__current-month,.react-datepicker-time__header,.react-datepicker-year-header{margin-top:0;font-weight:bold;font-size:.944rem;margin-bottom:8px}.theme-ghs .react-datepicker__current-month,.theme-ghs .react-datepicker-time__header,.theme-ghs .react-datepicker-year-header{color:#fff}.theme-george .react-datepicker__current-month,.theme-george .react-datepicker-time__header,.theme-george .react-datepicker-year-header{color:#fff}.react-datepicker-time__header{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.react-datepicker__navigation{background:0;line-height:1.7rem;text-align:center;cursor:pointer;position:absolute;top:10px;width:0;padding:0;border:.45rem solid transparent;z-index:1;height:10px;width:10px;text-indent:-999em;overflow:hidden}.react-datepicker__navigation--previous{left:10px;padding:0!important;height:0!important}.theme-ghs .react-datepicker__navigation--previous{border-right-color:#ccc}.theme-george .react-datepicker__navigation--previous{border-right-color:#ccc}.theme-ghs .react-datepicker__navigation--previous:hover{border-right-color:#b3b3b3}.theme-george .react-datepicker__navigation--previous:hover{border-right-color:#b3b3b3}.react-datepicker__navigation--previous--disabled,.react-datepicker__navigation--previous--disabled:hover{cursor:default}.theme-ghs .react-datepicker__navigation--previous--disabled,.theme-ghs .react-datepicker__navigation--previous--disabled:hover{border-right-color:#e6e6e6}.theme-george .react-datepicker__navigation--previous--disabled,.theme-george .react-datepicker__navigation--previous--disabled:hover{border-right-color:#e6e6e6}.react-datepicker__navigation--next{right:10px;padding:0!important;height:0!important}.theme-ghs .react-datepicker__navigation--next{border-left-color:#ccc}.theme-george .react-datepicker__navigation--next{border-left-color:#ccc}.react-datepicker__navigation--next--with-time:not(.react-datepicker__navigation--next--with-today-button){right:80px}.theme-ghs .react-datepicker__navigation--next:hover{border-left-color:#b3b3b3}.theme-george .react-datepicker__navigation--next:hover{border-left-color:#b3b3b3}.react-datepicker__navigation--next--disabled,.react-datepicker__navigation--next--disabled:hover{cursor:default}.theme-ghs .react-datepicker__navigation--next--disabled,.theme-ghs .react-datepicker__navigation--next--disabled:hover{border-left-color:#e6e6e6}.theme-george .react-datepicker__navigation--next--disabled,.theme-george .react-datepicker__navigation--next--disabled:hover{border-left-color:#e6e6e6}.react-datepicker__navigation--years{position:relative;top:0;display:block;margin-left:auto;margin-right:auto}.react-datepicker__navigation--years-previous{top:4px}.theme-ghs .react-datepicker__navigation--years-previous{border-top-color:#ccc}.theme-george .react-datepicker__navigation--years-previous{border-top-color:#ccc}.theme-ghs .react-datepicker__navigation--years-previous:hover{border-top-color:#b3b3b3}.theme-george .react-datepicker__navigation--years-previous:hover{border-top-color:#b3b3b3}.react-datepicker__navigation--years-upcoming{top:-4px}.theme-ghs .react-datepicker__navigation--years-upcoming{border-bottom-color:#ccc}.theme-george .react-datepicker__navigation--years-upcoming{border-bottom-color:#ccc}.theme-ghs .react-datepicker__navigation--years-upcoming:hover{border-bottom-color:#b3b3b3}.theme-george .react-datepicker__navigation--years-upcoming:hover{border-bottom-color:#b3b3b3}.react-datepicker__month-container{float:left}.react-datepicker__month{margin:.4rem;text-align:center}.react-datepicker__month .react-datepicker__month-text,.react-datepicker__month .react-datepicker__quarter-text{display:inline-block;width:4rem;margin:2px}.react-datepicker__input-time-container{clear:both;width:100%;float:left;margin:5px 0 10px 15px;text-align:left}.react-datepicker__input-time-container .react-datepicker-time__caption{display:inline-block}.react-datepicker__input-time-container .react-datepicker-time__input-container{display:inline-block}.react-datepicker__input-time-container .react-datepicker-time__input-container .react-datepicker-time__input{display:inline-block;margin-left:10px}.react-datepicker__input-time-container .react-datepicker-time__input-container .react-datepicker-time__input input{width:85px}.react-datepicker__input-time-container .react-datepicker-time__input-container .react-datepicker-time__input input[type=time]::-webkit-inner-spin-button,.react-datepicker__input-time-container .react-datepicker-time__input-container .react-datepicker-time__input input[type=time]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.react-datepicker__input-time-container .react-datepicker-time__input-container .react-datepicker-time__input input[type=time]{-moz-appearance:textfield}.react-datepicker__input-time-container .react-datepicker-time__input-container .react-datepicker-time__delimiter{margin-left:5px;display:inline-block}.react-datepicker__time-container{float:right;width:85px}.theme-ghs .react-datepicker__time-container{border-left:1px solid #538316}.theme-george .react-datepicker__time-container{border-left:1px solid #020202}.react-datepicker__time-container--with-today-button{display:inline;border:1px solid #aeaeae;border-radius:.3rem;position:absolute;right:-72px;top:0}.react-datepicker__time-container .react-datepicker__time{position:relative;background:#fff}.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box{width:85px;overflow-x:hidden;margin:0 auto;text-align:center}.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list{list-style:none;margin:0;height:calc(195px + (1.7rem / 2));overflow-y:scroll;padding-right:0px;padding-left:0px;width:100%;box-sizing:content-box}.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item{height:30px;padding:5px 10px;white-space:nowrap}.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item:hover{cursor:pointer}.theme-ghs .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item:hover{background-color:#538316}.theme-george .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item:hover{background-color:#020202}.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected{color:#fff;font-weight:bold}.theme-ghs .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected{background-color:#538316}.theme-ghs .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected:hover{background-color:#538316}.theme-george .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected{background-color:#020202}.theme-george .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected:hover{background-color:#020202}.theme-ghs .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--disabled{color:#ccc}.theme-george .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--disabled{color:#ccc}.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--disabled:hover{cursor:default;background-color:transparent}.react-datepicker__week-number{display:inline-block;width:1.7rem;line-height:1.7rem;text-align:center;margin:.166rem}.theme-ghs .react-datepicker__week-number{color:#ccc}.theme-george .react-datepicker__week-number{color:#ccc}.react-datepicker__week-number.react-datepicker__week-number--clickable{cursor:pointer}.react-datepicker__week-number.react-datepicker__week-number--clickable:hover{border-radius:.3rem}.theme-ghs .react-datepicker__week-number.react-datepicker__week-number--clickable:hover{background-color:#538316}.theme-george .react-datepicker__week-number.react-datepicker__week-number--clickable:hover{background-color:#020202}.react-datepicker__day-names,.react-datepicker__week{white-space:nowrap}.react-datepicker__day,.react-datepicker__day-name,.react-datepicker__time-name{display:inline-block;width:1.7rem;line-height:1.7rem;text-align:center;margin:.166rem}.theme-ghs .react-datepicker__day,.theme-ghs .react-datepicker__day-name,.theme-ghs .react-datepicker__time-name{color:#000}.theme-george .react-datepicker__day,.theme-george .react-datepicker__day-name,.theme-george .react-datepicker__time-name{color:#000}.theme-ghs .react-datepicker__day-name{color:#fff}.theme-george .react-datepicker__day-name{color:#fff}.react-datepicker__month--selected,.react-datepicker__month--in-selecting-range,.react-datepicker__month--in-range,.react-datepicker__quarter--selected,.react-datepicker__quarter--in-selecting-range,.react-datepicker__quarter--in-range{border-radius:.3rem;color:#fff}.theme-ghs .react-datepicker__month--selected,.theme-ghs .react-datepicker__month--in-selecting-range,.theme-ghs .react-datepicker__month--in-range,.theme-ghs .react-datepicker__quarter--selected,.theme-ghs .react-datepicker__quarter--in-selecting-range,.theme-ghs .react-datepicker__quarter--in-range{background-color:#538316}.theme-ghs .react-datepicker__month--selected:hover,.theme-ghs .react-datepicker__month--in-selecting-range:hover,.theme-ghs .react-datepicker__month--in-range:hover,.theme-ghs .react-datepicker__quarter--selected:hover,.theme-ghs .react-datepicker__quarter--in-selecting-range:hover,.theme-ghs .react-datepicker__quarter--in-range:hover{background-color:#456d12}.theme-george .react-datepicker__month--selected,.theme-george .react-datepicker__month--in-selecting-range,.theme-george .react-datepicker__month--in-range,.theme-george .react-datepicker__quarter--selected,.theme-george .react-datepicker__quarter--in-selecting-range,.theme-george .react-datepicker__quarter--in-range{background-color:#020202}.theme-george .react-datepicker__month--selected:hover,.theme-george .react-datepicker__month--in-selecting-range:hover,.theme-george .react-datepicker__month--in-range:hover,.theme-george .react-datepicker__quarter--selected:hover,.theme-george .react-datepicker__quarter--in-selecting-range:hover,.theme-george .react-datepicker__quarter--in-range:hover{background-color:#000}.react-datepicker__month--disabled,.react-datepicker__quarter--disabled{pointer-events:none}.theme-ghs .react-datepicker__month--disabled,.theme-ghs .react-datepicker__quarter--disabled{color:#ccc}.theme-george .react-datepicker__month--disabled,.theme-george .react-datepicker__quarter--disabled{color:#ccc}.react-datepicker__month--disabled:hover,.react-datepicker__quarter--disabled:hover{cursor:default;background-color:transparent}.react-datepicker__day,.react-datepicker__day-name,.react-datepicker__month-text,.react-datepicker__quarter-text{cursor:pointer}.react-datepicker__day:hover,.react-datepicker__day-name:hover,.react-datepicker__month-text:hover,.react-datepicker__quarter-text:hover{border-radius:.3rem}.theme-ghs .react-datepicker__day:hover,.theme-ghs .react-datepicker__day-name:hover,.theme-ghs .react-datepicker__month-text:hover,.theme-ghs .react-datepicker__quarter-text:hover{background-color:#68a51c;color:#fff}.theme-george .react-datepicker__day:hover,.theme-george .react-datepicker__day-name:hover,.theme-george .react-datepicker__month-text:hover,.theme-george .react-datepicker__quarter-text:hover{background-color:#3d3d3d;color:#fff}.react-datepicker__day--today,.react-datepicker__month-text--today,.react-datepicker__quarter-text--today{font-weight:bold}.react-datepicker__day--highlighted,.react-datepicker__month-text--highlighted,.react-datepicker__quarter-text--highlighted{border-radius:.3rem;color:#fff}.theme-ghs .react-datepicker__day--highlighted,.theme-ghs .react-datepicker__month-text--highlighted,.theme-ghs .react-datepicker__quarter-text--highlighted{background-color:#68a51c}.theme-ghs .react-datepicker__day--highlighted:hover,.theme-ghs .react-datepicker__month-text--highlighted:hover,.theme-ghs .react-datepicker__quarter-text--highlighted:hover{background-color:#5a8f18}.theme-george .react-datepicker__day--highlighted,.theme-george .react-datepicker__month-text--highlighted,.theme-george .react-datepicker__quarter-text--highlighted{background-color:#3d3d3d}.theme-george .react-datepicker__day--highlighted:hover,.theme-george .react-datepicker__month-text--highlighted:hover,.theme-george .react-datepicker__quarter-text--highlighted:hover{background-color:#303030}.react-datepicker__day--highlighted-custom-1,.react-datepicker__month-text--highlighted-custom-1,.react-datepicker__quarter-text--highlighted-custom-1{color:#f0f}.react-datepicker__day--highlighted-custom-2,.react-datepicker__month-text--highlighted-custom-2,.react-datepicker__quarter-text--highlighted-custom-2{color:green}.theme-ghs .react-datepicker__day--in-selecting-range:not(.react-datepicker__day--in-range,.react-datepicker__month-text--in-range,.react-datepicker__quarter-text--in-range),.theme-ghs .react-datepicker__month-text--in-selecting-range:not(.react-datepicker__day--in-range,.react-datepicker__month-text--in-range,.react-datepicker__quarter-text--in-range),.theme-ghs .react-datepicker__quarter-text--in-selecting-range:not(.react-datepicker__day--in-range,.react-datepicker__month-text--in-range,.react-datepicker__quarter-text--in-range){background-color:rgba(83,131,22,.5)}.theme-george .react-datepicker__day--in-selecting-range:not(.react-datepicker__day--in-range,.react-datepicker__month-text--in-range,.react-datepicker__quarter-text--in-range),.theme-george .react-datepicker__month-text--in-selecting-range:not(.react-datepicker__day--in-range,.react-datepicker__month-text--in-range,.react-datepicker__quarter-text--in-range),.theme-george .react-datepicker__quarter-text--in-selecting-range:not(.react-datepicker__day--in-range,.react-datepicker__month-text--in-range,.react-datepicker__quarter-text--in-range){background-color:rgba(2,2,2,.5)}.theme-ghs .react-datepicker__month--selecting-range .react-datepicker__day--in-range:not(.react-datepicker__day--in-selecting-range,.react-datepicker__month-text--in-selecting-range,.react-datepicker__quarter-text--in-selecting-range),.theme-ghs .react-datepicker__month--selecting-range .react-datepicker__month-text--in-range:not(.react-datepicker__day--in-selecting-range,.react-datepicker__month-text--in-selecting-range,.react-datepicker__quarter-text--in-selecting-range),.theme-ghs .react-datepicker__month--selecting-range .react-datepicker__quarter-text--in-range:not(.react-datepicker__day--in-selecting-range,.react-datepicker__month-text--in-selecting-range,.react-datepicker__quarter-text--in-selecting-range){background-color:#538316;color:#000}.theme-george .react-datepicker__month--selecting-range .react-datepicker__day--in-range:not(.react-datepicker__day--in-selecting-range,.react-datepicker__month-text--in-selecting-range,.react-datepicker__quarter-text--in-selecting-range),.theme-george .react-datepicker__month--selecting-range .react-datepicker__month-text--in-range:not(.react-datepicker__day--in-selecting-range,.react-datepicker__month-text--in-selecting-range,.react-datepicker__quarter-text--in-selecting-range),.theme-george .react-datepicker__month--selecting-range .react-datepicker__quarter-text--in-range:not(.react-datepicker__day--in-selecting-range,.react-datepicker__month-text--in-selecting-range,.react-datepicker__quarter-text--in-selecting-range){background-color:#020202;color:#000}.react-datepicker__day--selected,.react-datepicker__day--in-selecting-range,.react-datepicker__day--in-range,.react-datepicker__month-text--selected,.react-datepicker__month-text--in-selecting-range,.react-datepicker__month-text--in-range,.react-datepicker__quarter-text--selected,.react-datepicker__quarter-text--in-selecting-range,.react-datepicker__quarter-text--in-range{border-radius:.3rem}.theme-ghs .react-datepicker__day--selected,.theme-ghs .react-datepicker__day--in-selecting-range,.theme-ghs .react-datepicker__day--in-range,.theme-ghs .react-datepicker__month-text--selected,.theme-ghs .react-datepicker__month-text--in-selecting-range,.theme-ghs .react-datepicker__month-text--in-range,.theme-ghs .react-datepicker__quarter-text--selected,.theme-ghs .react-datepicker__quarter-text--in-selecting-range,.theme-ghs .react-datepicker__quarter-text--in-range{color:#fff;background-color:#538316}.theme-ghs .react-datepicker__day--selected:hover,.theme-ghs .react-datepicker__day--in-selecting-range:hover,.theme-ghs .react-datepicker__day--in-range:hover,.theme-ghs .react-datepicker__month-text--selected:hover,.theme-ghs .react-datepicker__month-text--in-selecting-range:hover,.theme-ghs .react-datepicker__month-text--in-range:hover,.theme-ghs .react-datepicker__quarter-text--selected:hover,.theme-ghs .react-datepicker__quarter-text--in-selecting-range:hover,.theme-ghs .react-datepicker__quarter-text--in-range:hover{background-color:#456d12}.theme-george .react-datepicker__day--selected,.theme-george .react-datepicker__day--in-selecting-range,.theme-george .react-datepicker__day--in-range,.theme-george .react-datepicker__month-text--selected,.theme-george .react-datepicker__month-text--in-selecting-range,.theme-george .react-datepicker__month-text--in-range,.theme-george .react-datepicker__quarter-text--selected,.theme-george .react-datepicker__quarter-text--in-selecting-range,.theme-george .react-datepicker__quarter-text--in-range{color:#fff;background-color:#020202}.theme-george .react-datepicker__day--selected:hover,.theme-george .react-datepicker__day--in-selecting-range:hover,.theme-george .react-datepicker__day--in-range:hover,.theme-george .react-datepicker__month-text--selected:hover,.theme-george .react-datepicker__month-text--in-selecting-range:hover,.theme-george .react-datepicker__month-text--in-range:hover,.theme-george .react-datepicker__quarter-text--selected:hover,.theme-george .react-datepicker__quarter-text--in-selecting-range:hover,.theme-george .react-datepicker__quarter-text--in-range:hover{background-color:#000}.react-datepicker__day--keyboard-selected,.react-datepicker__month-text--keyboard-selected,.react-datepicker__quarter-text--keyboard-selected{border-radius:.3rem}.theme-ghs .react-datepicker__day--keyboard-selected,.theme-ghs .react-datepicker__month-text--keyboard-selected,.theme-ghs .react-datepicker__quarter-text--keyboard-selected{color:#fff;background-color:#6faf1d}.theme-ghs .react-datepicker__day--keyboard-selected:hover,.theme-ghs .react-datepicker__month-text--keyboard-selected:hover,.theme-ghs .react-datepicker__quarter-text--keyboard-selected:hover{background-color:#456d12}.theme-george .react-datepicker__day--keyboard-selected,.theme-george .react-datepicker__month-text--keyboard-selected,.theme-george .react-datepicker__quarter-text--keyboard-selected{color:#fff;background-color:#1c1c1c}.theme-george .react-datepicker__day--keyboard-selected:hover,.theme-george .react-datepicker__month-text--keyboard-selected:hover,.theme-george .react-datepicker__quarter-text--keyboard-selected:hover{background-color:#000}.react-datepicker__day--disabled,.react-datepicker__month-text--disabled,.react-datepicker__quarter-text--disabled{cursor:default}.theme-ghs .react-datepicker__day--disabled,.theme-ghs .react-datepicker__month-text--disabled,.theme-ghs .react-datepicker__quarter-text--disabled{color:#ccc}.theme-george .react-datepicker__day--disabled,.theme-george .react-datepicker__month-text--disabled,.theme-george .react-datepicker__quarter-text--disabled{color:#ccc}.react-datepicker__day--disabled:hover,.react-datepicker__month-text--disabled:hover,.react-datepicker__quarter-text--disabled:hover{background-color:transparent}.theme-ghs .react-datepicker__month-text.react-datepicker__month--selected:hover,.theme-ghs .react-datepicker__month-text.react-datepicker__month--in-range:hover,.theme-ghs .react-datepicker__month-text.react-datepicker__quarter--selected:hover,.theme-ghs .react-datepicker__month-text.react-datepicker__quarter--in-range:hover,.theme-ghs .react-datepicker__quarter-text.react-datepicker__month--selected:hover,.theme-ghs .react-datepicker__quarter-text.react-datepicker__month--in-range:hover,.theme-ghs .react-datepicker__quarter-text.react-datepicker__quarter--selected:hover,.theme-ghs .react-datepicker__quarter-text.react-datepicker__quarter--in-range:hover{background-color:#538316}.theme-george .react-datepicker__month-text.react-datepicker__month--selected:hover,.theme-george .react-datepicker__month-text.react-datepicker__month--in-range:hover,.theme-george .react-datepicker__month-text.react-datepicker__quarter--selected:hover,.theme-george .react-datepicker__month-text.react-datepicker__quarter--in-range:hover,.theme-george .react-datepicker__quarter-text.react-datepicker__month--selected:hover,.theme-george .react-datepicker__quarter-text.react-datepicker__month--in-range:hover,.theme-george .react-datepicker__quarter-text.react-datepicker__quarter--selected:hover,.theme-george .react-datepicker__quarter-text.react-datepicker__quarter--in-range:hover{background-color:#020202}.theme-ghs .react-datepicker__month-text:hover,.theme-ghs .react-datepicker__quarter-text:hover{background-color:#538316}.theme-george .react-datepicker__month-text:hover,.theme-george .react-datepicker__quarter-text:hover{background-color:#020202}.react-datepicker__input-container{position:relative;display:inline-block;width:100%}.react-datepicker__year-read-view,.react-datepicker__month-read-view,.react-datepicker__month-year-read-view{border:1px solid transparent;border-radius:.3rem}.react-datepicker__year-read-view:hover,.react-datepicker__month-read-view:hover,.react-datepicker__month-year-read-view:hover{cursor:pointer}.theme-ghs .react-datepicker__year-read-view:hover .react-datepicker__year-read-view--down-arrow,.theme-ghs .react-datepicker__year-read-view:hover .react-datepicker__month-read-view--down-arrow,.theme-ghs .react-datepicker__month-read-view:hover .react-datepicker__year-read-view--down-arrow,.theme-ghs .react-datepicker__month-read-view:hover .react-datepicker__month-read-view--down-arrow,.theme-ghs .react-datepicker__month-year-read-view:hover .react-datepicker__year-read-view--down-arrow,.theme-ghs .react-datepicker__month-year-read-view:hover .react-datepicker__month-read-view--down-arrow{border-top-color:#b3b3b3}.theme-george .react-datepicker__year-read-view:hover .react-datepicker__year-read-view--down-arrow,.theme-george .react-datepicker__year-read-view:hover .react-datepicker__month-read-view--down-arrow,.theme-george .react-datepicker__month-read-view:hover .react-datepicker__year-read-view--down-arrow,.theme-george .react-datepicker__month-read-view:hover .react-datepicker__month-read-view--down-arrow,.theme-george .react-datepicker__month-year-read-view:hover .react-datepicker__year-read-view--down-arrow,.theme-george .react-datepicker__month-year-read-view:hover .react-datepicker__month-read-view--down-arrow{border-top-color:#b3b3b3}.react-datepicker__year-read-view--down-arrow,.react-datepicker__month-read-view--down-arrow,.react-datepicker__month-year-read-view--down-arrow{float:right;margin-left:20px;top:8px;position:relative;border-width:.45rem}.theme-ghs .react-datepicker__year-read-view--down-arrow,.theme-ghs .react-datepicker__month-read-view--down-arrow,.theme-ghs .react-datepicker__month-year-read-view--down-arrow{border-top-color:#ccc}.theme-george .react-datepicker__year-read-view--down-arrow,.theme-george .react-datepicker__month-read-view--down-arrow,.theme-george .react-datepicker__month-year-read-view--down-arrow{border-top-color:#ccc}.react-datepicker__year-dropdown,.react-datepicker__month-dropdown,.react-datepicker__month-year-dropdown{position:absolute;width:50%;left:25%;top:30px;z-index:1;text-align:center;border-radius:.3rem}.theme-ghs .react-datepicker__year-dropdown,.theme-ghs .react-datepicker__month-dropdown,.theme-ghs .react-datepicker__month-year-dropdown{background-color:#538316;border:1px solid #538316}.theme-george .react-datepicker__year-dropdown,.theme-george .react-datepicker__month-dropdown,.theme-george .react-datepicker__month-year-dropdown{background-color:#020202;border:1px solid #020202}.react-datepicker__year-dropdown:hover,.react-datepicker__month-dropdown:hover,.react-datepicker__month-year-dropdown:hover{cursor:pointer}.react-datepicker__year-dropdown--scrollable,.react-datepicker__month-dropdown--scrollable,.react-datepicker__month-year-dropdown--scrollable{height:150px;overflow-y:scroll}.react-datepicker__year-option,.react-datepicker__month-option,.react-datepicker__month-year-option{line-height:20px;width:100%;display:block;margin-left:auto;margin-right:auto}.react-datepicker__year-option:first-of-type,.react-datepicker__month-option:first-of-type,.react-datepicker__month-year-option:first-of-type{border-top-left-radius:.3rem;border-top-right-radius:.3rem}.react-datepicker__year-option:last-of-type,.react-datepicker__month-option:last-of-type,.react-datepicker__month-year-option:last-of-type{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border-bottom-left-radius:.3rem;border-bottom-right-radius:.3rem}.theme-ghs .react-datepicker__year-option:hover,.theme-ghs .react-datepicker__month-option:hover,.theme-ghs .react-datepicker__month-year-option:hover{background-color:#ccc}.theme-ghs .react-datepicker__year-option:hover .react-datepicker__navigation--years-upcoming,.theme-ghs .react-datepicker__month-option:hover .react-datepicker__navigation--years-upcoming,.theme-ghs .react-datepicker__month-year-option:hover .react-datepicker__navigation--years-upcoming{border-bottom-color:#b3b3b3}.theme-ghs .react-datepicker__year-option:hover .react-datepicker__navigation--years-previous,.theme-ghs .react-datepicker__month-option:hover .react-datepicker__navigation--years-previous,.theme-ghs .react-datepicker__month-year-option:hover .react-datepicker__navigation--years-previous{border-top-color:#b3b3b3}.theme-george .react-datepicker__year-option:hover,.theme-george .react-datepicker__month-option:hover,.theme-george .react-datepicker__month-year-option:hover{background-color:#ccc}.theme-george .react-datepicker__year-option:hover .react-datepicker__navigation--years-upcoming,.theme-george .react-datepicker__month-option:hover .react-datepicker__navigation--years-upcoming,.theme-george .react-datepicker__month-year-option:hover .react-datepicker__navigation--years-upcoming{border-bottom-color:#b3b3b3}.theme-george .react-datepicker__year-option:hover .react-datepicker__navigation--years-previous,.theme-george .react-datepicker__month-option:hover .react-datepicker__navigation--years-previous,.theme-george .react-datepicker__month-year-option:hover .react-datepicker__navigation--years-previous{border-top-color:#b3b3b3}.react-datepicker__year-option--selected,.react-datepicker__month-option--selected,.react-datepicker__month-year-option--selected{position:absolute;left:15px}.react-datepicker__close-icon{cursor:pointer;background-color:transparent;border:0;outline:0;padding:0px 6px 0px 0px;position:absolute;top:0;right:0;height:100%;display:table-cell;vertical-align:middle}.react-datepicker__close-icon::after{cursor:pointer;color:#fff;border-radius:50%;height:16px;width:16px;padding:2px;font-size:12px;line-height:1;text-align:center;display:table-cell;vertical-align:middle;content:"\D7"}.theme-ghs .react-datepicker__close-icon::after{background-color:#538316}.theme-george .react-datepicker__close-icon::after{background-color:#020202}.react-datepicker__today-button{cursor:pointer;text-align:center;font-weight:bold;padding:5px 0;clear:left}.theme-ghs .react-datepicker__today-button{background:#538316;border-top:1px solid #538316}.theme-george .react-datepicker__today-button{background:#020202;border-top:1px solid #020202}.react-datepicker__portal{position:fixed;width:100vw;height:100vh;background-color:rgba(0,0,0,.8);left:0;top:0;justify-content:center;align-items:center;display:flex;z-index:2147483647}.react-datepicker__portal .react-datepicker__day-name,.react-datepicker__portal .react-datepicker__day,.react-datepicker__portal .react-datepicker__time-name{width:3rem;line-height:3rem}@media (max-width:400px),(max-height:550px){.react-datepicker__portal .react-datepicker__day-name,.react-datepicker__portal .react-datepicker__day,.react-datepicker__portal .react-datepicker__time-name{width:2rem;line-height:2rem}}.react-datepicker__portal .react-datepicker__current-month,.react-datepicker__portal .react-datepicker-time__header{font-size:1.44rem}.react-datepicker__portal .react-datepicker__navigation{border:.81rem solid transparent}.theme-ghs .react-datepicker__portal .react-datepicker__navigation--previous{border-right-color:#ccc}.theme-ghs .react-datepicker__portal .react-datepicker__navigation--previous:hover{border-right-color:#b3b3b3}.theme-george .react-datepicker__portal .react-datepicker__navigation--previous{border-right-color:#ccc}.theme-george .react-datepicker__portal .react-datepicker__navigation--previous:hover{border-right-color:#b3b3b3}.react-datepicker__portal .react-datepicker__navigation--previous--disabled,.react-datepicker__portal .react-datepicker__navigation--previous--disabled:hover{cursor:default}.theme-ghs .react-datepicker__portal .react-datepicker__navigation--previous--disabled,.theme-ghs .react-datepicker__portal .react-datepicker__navigation--previous--disabled:hover{border-right-color:#e6e6e6}.theme-george .react-datepicker__portal .react-datepicker__navigation--previous--disabled,.theme-george .react-datepicker__portal .react-datepicker__navigation--previous--disabled:hover{border-right-color:#e6e6e6}.theme-ghs .react-datepicker__portal .react-datepicker__navigation--next{border-left-color:#ccc}.theme-ghs .react-datepicker__portal .react-datepicker__navigation--next:hover{border-left-color:#b3b3b3}.theme-george .react-datepicker__portal .react-datepicker__navigation--next{border-left-color:#ccc}.theme-george .react-datepicker__portal .react-datepicker__navigation--next:hover{border-left-color:#b3b3b3}.react-datepicker__portal .react-datepicker__navigation--next--disabled,.react-datepicker__portal .react-datepicker__navigation--next--disabled:hover{cursor:default}.theme-ghs .react-datepicker__portal .react-datepicker__navigation--next--disabled,.theme-ghs .react-datepicker__portal .react-datepicker__navigation--next--disabled:hover{border-left-color:#e6e6e6}.theme-george .react-datepicker__portal .react-datepicker__navigation--next--disabled,.theme-george .react-datepicker__portal .react-datepicker__navigation--next--disabled:hover{border-left-color:#e6e6e6}.datepicker-wrapper{position:relative}.datepicker-wrapper--error input{border:2px solid #d43030}.theme-ghs .what-can-i-expect,.theme-george .what-can-i-expect{font-family:SourceSansProRegular,sans-serif;padding:2px 0 28px 0}.theme-ghs .what-can-i-expect__button,.theme-george .what-can-i-expect__button{font-size:14px;padding:0px;text-transform:inherit}.theme-ghs .what-can-i-expect__chevron,.theme-george .what-can-i-expect__chevron{padding-left:6px}.theme-ghs .what-can-i-expect__chevron--flipped,.theme-george .what-can-i-expect__chevron--flipped{transform:rotate(180deg)}.theme-ghs .what-can-i-expect__description,.theme-george .what-can-i-expect__description{color:#3d3d3d;font-size:14px;line-height:1.5;letter-spacing:.2px;list-style:disc;padding-left:20px;padding-top:14px}.theme-ghs .what-can-i-expect__description li,.theme-george .what-can-i-expect__description li{padding-bottom:16px}.theme-ghs .what-can-i-expect__description li:last-child,.theme-george .what-can-i-expect__description li:last-child{padding-bottom:0px}.theme-george .what-can-i-expect__button{text-decoration:underline}</style><style>html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}article *,article *:before,article *:after{box-sizing:border-box}@font-face{font-family:"SourceSansProBold";src:url(resources/sample/7.woff2) format("woff2")}@font-face{font-family:"SourceSansProSemiBold";src:url(resources/sample/8.woff2) format("woff2")}@font-face{font-family:"SourceSansProRegular";src:url(resources/sample/9.woff2) format("woff2")}h1,h2,h3,h4,p,a{color:#191919;padding-bottom:8px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}h1{font-size:1.25em;font-weight:700;color:#191919;padding-bottom:16px;line-height:1.25}.theme-ghs h1{font-family:SourceSansProBold,sans-serif}.theme-george h1{font-family:LatoBold,sans-serif}h2{font-size:1.125em;font-weight:800}.theme-ghs h2{font-family:SourceSansProBold,sans-serif}.theme-george h2{font-family:LatoBold,sans-serif}h3{font-size:1em;font-weight:600}.theme-ghs h3{font-family:SourceSansProSemiBold,sans-serif}.theme-george h3{font-family:LatoRegular,sans-serif}h4{font-size:.875em;font-weight:400}.theme-ghs h4{font-family:SourceSansProRegular,sans-serif}.theme-george h4{font-family:LatoRegular,sans-serif}p{color:off-black;padding-bottom:16px;font-size:.875em;letter-spacing:.2px;line-height:1.3}.theme-ghs p{font-family:SourceSansProRegular,sans-serif}.theme-george p{font-family:LatoRegular,sans-serif}a{margin-bottom:16px;padding-bottom:0;font-weight:500;font-size:1em}.theme-ghs a{color:#0073b1;text-decoration:none;font-family:SourceSansProBold,sans-serif}.theme-george a{color:#191919;text-decoration:underline;font-family:LatoRegular,sans-serif}.button-as-link{display:inline;background-color:transparent;border:0;padding:0;width:auto!important;height:auto!important;color:#0073b1;margin-left:0;padding-bottom:16px;font-weight:500;text-decoration:none}.theme-ghs .button-as-link{font-size:.875em}.theme-george .button-as-link{font-size:12px}.theme-ghs .button-as-link{color:#0073b1}.theme-george .button-as-link{color:#191919}.short-password-error{bottom:62px}.blank-password-error{bottom:50px}a:hover{cursor:pointer}.theme-ghs strong{font-family:SourceSansProBold,sans-serif}.theme-george strong{font-family:LatoBold,sans-serif}.input-error{font-family:SourceSansProRegular,sans-serif;font-size:.75em;color:#d43030;line-height:1.125em}.theme-ghs .input-error{font-family:SourceSansProRegular,sans-serif;font-size:.75em;color:#d43030;clear:both}.theme-george .input-error{font-family:LatoBold,sans-serif;font-size:.875em;color:#da291c;clear:both}.theme-ghs .input-error a{font-family:SourceSansProRegular,sans-serif;color:#d43030;text-decoration:underline}.theme-george .input-error a{font-family:LatoBold,sans-serif;color:#da291c;text-decoration:underline}.left{float:left!important}.right{float:right!important;text-align:right!important}.theme-ghs .right{padding-top:3px;line-height:1.5}.theme-george .right{line-height:1.5}.center{text-align:center!important}.red{color:#da0500!important}.orange{color:#fdb45b!important}.pink{color:#ec938e!important}.yellow{color:#fae100!important}.green{color:#68a51c!important}.blue{color:#0073b1!important}.grey{color:grey1!important}.white{color:#fff!important}.black{color:#191919!important}.regular{font-weight:400!important}.bold{font-weight:700!important}html{overflow-x:hidden;width:100vw}.co-account{min-height:calc(100vh - 294px);height:auto;width:calc(100% - 32px);min-height:calc(100vh - 356px);margin:0 16px;position:relative}.theme-ghs .co-account{min-height:calc(100vh - 300px)}.theme-george .co-account{min-height:calc(100vh - 261px)}@media (min-width:481px){.co-account{width:416px;margin:0 auto}.theme-ghs .co-account{min-height:calc(100vh - 284px)}.theme-george .co-account{min-height:calc(100vh - 245px)}}.co-account{height:auto;margin:0 auto 60px}@media (min-width:481px){.co-account{width:406px}}.co-sign-in-details__password-container,.co-sign-in-details__email-container,.co-sign-in-details__phone-container{position:relative;margin-bottom:20px;height:auto;display:block}.co-sign-in-details__password-container{margin-bottom:0}.co-sign-in-details__email-container--read-only .input-box{max-width:86%}.with-whats-this{height:32px}.with-whats-this .whats-this-label{float:left}.address-phone-book .address-section,.address-phone-book .contact-section{margin-bottom:16px}@media (min-width:320px){.address-phone-book .address-section label[for^=account-postcode],.address-phone-book .contact-section label[for^=account-postcode]{height:32px;display:flex;flex-direction:column-reverse}}@media (min-width:481px){.address-phone-book .address-section label[for^=account-postcode],.address-phone-book .contact-section label[for^=account-postcode]{height:auto}}.address-phone-book .address-section .list .list-item,.address-phone-book .contact-section .list .list-item{display:flex}.address-phone-book .address-section .list .list-item .list-item-delete-button img,.address-phone-book .contact-section .list .list-item .list-item-delete-button img{margin-top:0}.permission-centre button.add_phone{margin-bottom:16px}.wallet-card .co-list-card{display:flex;flex-direction:column}.wallet-card .co-list-card .list-item .list-item-radio{padding-right:0}.wallet-card .co-edit-card{margin-top:16px;padding-bottom:16px;border-bottom:1px solid #ccc}.wallet-card .modal{font-weight:400}.wallet-card .co-add-card{padding-top:10px}.wallet-card .co-add-card .custom-icon input{padding-left:50px}.wallet-card .co-add-card .custom-icon:before{left:10px;top:30px;width:30px;height:20px;background-image:var(--sf-img-16);z-index:1}.wallet-card .co-add-card .custom-icon.amex:before{background-image:var(--sf-img-17)}.wallet-card .co-add-card .custom-icon.maestro:before{background-image:var(--sf-img-18)}.wallet-card .co-add-card .custom-icon.mastercard:before{background-image:var(--sf-img-19)}.wallet-card .co-add-card .custom-icon.visa:before{background-image:var(--sf-img-20)}.wallet-card .co-add-card .date-input:before{top:30px;left:10px}.theme-ghs .wallet-card .co-add-card .date-input:before{top:30px}.theme-george .wallet-card .co-add-card .date-input:before{top:35px}@media (min-width:320px){.wallet-card .co-add-card label[for^=account-postcode]{height:32px;display:flex;flex-direction:column-reverse}}@media (min-width:481px){.wallet-card .co-add-card label[for^=account-postcode]{height:auto}}.wallet-card .date-input{position:relative}.wallet-card .date-input input{letter-spacing:.8px}.wallet-card .date-input:before{content:" / ";pointer-events:none;white-space:pre;position:absolute;top:28px;left:13px;display:block;width:40px;height:19px;color:#191919;font-size:1.25em;z-index:1}.co-personal-details__colleague-details{position:relative;margin-bottom:20px;height:auto;display:block}.underlined-link-toggle{border:0;padding:0!important;text-align:left;font-size:14px;width:auto;height:auto;margin:0;text-decoration:underline}.theme-ghs .underlined-link-toggle{color:#0073b1;letter-spacing:normal}.theme-george .underlined-link-toggle{color:#191919;letter-spacing:normal}.co-account .leave-btc .destructive{font-size:19px;margin-top:36px}.co-account .leave-btc .close-icon{position:absolute;top:8px;right:8px;width:auto;height:auto;border:0;padding:0;margin:0;background:transparent}.co-account .leave-btc .modal{width:400px;top:25%}.co-account .leave-btc .modal .modal-title,.co-account .leave-btc .modal .modal-content{border-bottom:0;padding-bottom:0px}.co-account .leave-btc .modal .modal-footer{flex-direction:column-reverse;gap:16px}.co-account .leave-btc .modal .modal-footer button{font-size:19px;font-weight:bold}.co-account .leave-btc .modal .modal-footer button.destructive{margin-top:0px}@media (min-width:481px){.leave-btc .modal{left:calc(50% - 200px)}}.panel{position:relative;border:1px solid #ccc}.theme-ghs .panel{border-radius:4px;margin:0 0 8px}.theme-george .panel{border-radius:0;margin:0 0 16px}.theme-ghs .panel.open{border-color:#ccc;box-shadow:none}.theme-george .panel.open{border-color:#9b9b9b;box-shadow:4px 4px 4px 0 rgba(0,0,0,.08)}.panel__header{width:100%;border:1px solid transparent;text-align:right;line-height:40px;margin:0;overflow:hidden;text-transform:none;letter-spacing:normal}.theme-ghs .panel__header{box-shadow:none;background:#f6f6f6;height:52px;padding:6px 16px;border-radius:4px}.theme-george .panel__header{box-shadow:4px 4px 4px 0 rgba(0,0,0,.08);background:#fff;height:52px;padding:6px 16px;border-radius:0}.panel__header:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.panel__header.add-bottom-border{border:1px solid transparent;border-bottom:1px solid transparent}.theme-ghs .panel__header.add-bottom-border{border-bottom:1px solid #ccc;box-shadow:none}.theme-george .panel__header.add-bottom-border{border-bottom:1px solid #9b9b9b;box-shadow:none}.panel__chevron{padding:16px 0}.panel__toggle{color:#0979b2}.theme-ghs .panel__toggle{font-family:SourceSansProRegular,sans-serif;font-size:18px;font-weight:600}.theme-george .panel__toggle{font-family:LatoRegular,sans-serif;font-size:14px;font-weight:500}.panel__title{float:left;text-transform:none}.theme-ghs .panel__title{font-family:SourceSansProRegular,sans-serif;font-size:18px;font-weight:600}.theme-george .panel__title{font-family:LatoRegular,sans-serif;font-size:14px;font-weight:normal}.panel__section-title{font-size:16px;text-align:left;color:#000;padding-bottom:16px}.theme-ghs .panel__section-title{font-family:SourceSansProRegular,sans-serif}.theme-george .panel__section-title{font-family:LatoRegular,sans-serif}.panel__sub-title{font-size:12px;letter-spacing:.2px;text-align:left;color:#000}.panel__anchor{display:block;padding:0;float:right}.panel__main{transition:height .2s cubic-bezier(0.4,0,0.2,1);padding:16px}.panel__main .expanded_container{padding:16px;float:left;width:100%}.panel__footer{height:40px;line-height:40px;padding:0 10px}@media (min-width:320px){.panel__anchor{padding:0 10px 0 25px}}@media (min-width:768px){.panel--no-border{border:0}}.button-container{width:100%;height:44px;display:block;margin-top:4px}@media (min-width:320px){.button-container{height:auto}}@media (min-width:481px){.button-container{height:44px}}.button-container .half{margin:0 2% 0 0}@media (min-width:320px){.button-container .half.half{width:100%;margin:0 0 8px}}@media (min-width:481px){.button-container .half.half{width:calc(50% - 8px);float:right;margin-right:8px}.button-container .half.half:nth-child(1){margin:0 0 0 2%}}h4.address-form-title{padding-bottom:12px}.theme-ghs h4.address-form-title{font-family:SourceSansProSemiBold,sans-serif}.theme-george h4.address-form-title{font-family:LatoRegular,sans-serif}h4.address-form-title button{height:20px;padding:0;font-size:14px;text-transform:none}.link-toggle{border:0;padding:0;text-align:left;font-size:14px;width:auto;height:auto;margin:0}.theme-ghs .link-toggle{font-family:SourceSansProRegular,sans-serif;color:#0073b1;text-decoration:none;letter-spacing:normal}.theme-george .link-toggle{font-family:LatoBold,sans-serif;color:#191919;text-decoration:underline;letter-spacing:normal}.link-toggle:hover{text-decoration:underline}label.radio-group-label{font-size:16px;width:100%;display:block;padding-bottom:12px}.theme-ghs label.radio-group-label{font-family:SourceSansProRegular,sans-serif}.theme-george label.radio-group-label{font-family:LatoRegular,sans-serif}.theme-ghs .find-button{margin-bottom:16px}.theme-george .find-button{margin-bottom:16px}.select-box{position:relative}.select-box label{padding-bottom:4px;display:block}.theme-ghs .select-box label{font-family:SourceSansProRegular,sans-serif;color:#3d3d3d;font-size:.875em;padding:0 0 4px}.theme-george .select-box label{font-family:LatoRegular,sans-serif;color:#191919;font-size:14px;padding:0 0 8px}.select-box .select-arrow{position:absolute;top:36px;right:12px;z-index:-100}.theme-ghs .select-box .select-arrow{top:36px}.theme-george .select-box .select-arrow{top:41px}.theme-ghs .select-box .select-arrow.no-label{top:18px}.theme-george .select-box .select-arrow.no-label{top:18px}.select-box select{width:100%;height:40px;font-size:1em;color:#767676;border-radius:4px;border:1px solid #ccc;clear:both;appearance:none;-webkit-appearance:none;cursor:pointer;background-color:transparent;padding:0 30px 0 12px;margin-bottom:8px;transition:all 200ms ease}.theme-ghs .select-box select{height:40px;font-size:1em;margin-bottom:12px;font-family:SourceSansProRegular,sans-serif;color:#3d3d3d}.theme-george .select-box select{height:42px;font-size:.875em;margin-bottom:16px;font-family:LatoRegular,sans-serif;color:#191919}.select-box select:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.select-box select:focus{border-color:#767676}.select-box select.half{width:calc(50% - 10px);float:left;margin-right:8px}.select-box select.error{border:2px solid #d43030;margin-bottom:0}.select-box.half{width:calc(50% - 5px);float:left;margin-right:10px}.select-box.no-gutter{margin:0}.select-error{margin-bottom:8px;margin-top:4px}.text-area label{padding-bottom:4px;display:block}.theme-ghs .text-area label{font-family:SourceSansProRegular,sans-serif;color:#3d3d3d;font-size:.875em}.theme-george .text-area label{font-family:LatoRegular,sans-serif;color:#191919;font-size:14px}.text-area textarea{width:100%;height:110px;font-size:1em;border-radius:4px;border:1px solid #ccc;clear:both;padding:8px;margin-bottom:8px;color:#191919;font-family:SourceSansProRegular,sans-serif;resize:none}.text-area textarea::-webkit-input-placeholder{color:#767676}.text-area textarea:-moz-placeholder{color:#767676}.text-area textarea::-moz-placeholder{color:#767676}.text-area textarea:-ms-input-placeholder{color:#767676}.text-area textarea.half{width:calc(50% - 10px);float:left;margin-right:8px}.text-area textarea.error{border:solid 1px #d43030;margin-bottom:0}.text-area.half{width:calc(50% - 5px);float:left;margin-right:10px}.text-area.no-gutter{margin:0}.input-error{margin-bottom:8px;margin-top:4px}.co-account__title{font-size:22px;padding-bottom:8px}.theme-ghs .co-account__title{font-family:SourceSansProRegular,sans-serif;padding-bottom:8px}.theme-george .co-account__title{font-family:LatoRegular,sans-serif;padding-bottom:32px}.co-account__description{font-size:14px;padding-bottom:16px}.theme-ghs .co-account__description{font-family:SourceSansProRegular,sans-serif}.theme-george .co-account__description{font-family:LatoRegular,sans-serif}a.george-account-link{font-family:LatoRegular,sans-serif;font-size:14px;color:#191919;display:block}a.george-account-link img{margin-right:8px}.edit-link{position:absolute;right:0;top:22px;padding:0;border:0;width:auto;height:auto;margin:0;font-size:18px;background:transparent;transition:all 200ms ease;letter-spacing:normal}.theme-ghs .edit-link{padding:0;height:auto;text-transform:none}.theme-george .edit-link{padding:0;height:auto;text-transform:none}.edit-link:hover{text-decoration:underline}.edit-link.change{top:16px}.theme-ghs .edit-link{font-family:SourceSansProRegular,sans-serif;color:#0073b1;text-decoration:none}.theme-george .edit-link{font-family:LatoBold,sans-serif;color:#191919;text-decoration:underline}.dropdown-list-item.selected,.dropdown-list-item:hover{color:#fff;background-color:#767676}.dropdown-wrapper-single{user-select:none;position:relative;width:265px}.dropdown-wrapper-single .dropdown-header{border:1px solid #ccc}.dropdown-wrapper-single .dropdown-header .dropdown-header-name{font-weight:400}.dropdown-wrapper-single .dropdown-list{border:1px solid #ccc;border-top:0}.dropdown-wrapper{font-family:SourceSansProRegular,sans-serif;font-size:24px;font-weight:600;font-style:normal;font-stretch:normal;line-height:normal;letter-spacing:.3px;float:left;margin-right:16px}.dropdown-wrapper .dropdown-header{display:flex;align-items:center;justify-content:space-between;width:176px;height:78px;border:1px solid #767676;cursor:default;position:relative;background-color:#fff;padding:20px}.dropdown-wrapper .dropdown-header.active{border-bottom:0px;top:1px;z-index:11}.dropdown-wrapper .list-wrapper .dropdown-list{z-index:10;position:absolute;width:560px;height:636px;border:1px solid #767676;background-color:#fff;padding:20px 75px 20px 20px;overflow-y:scroll;color:#767676}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{width:100%;padding:8px 0px;line-height:1.54px;cursor:default;display:flex;align-items:center;justify-content:space-between;text-overflow:ellipsis}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item:nth-child(5){padding-bottom:18px;border-bottom:2px solid #979797}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{width:420px;text-align:left;padding-left:15px}.dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{width:80px;background-color:none}.dropdown-wrapper .dropdown-list::-webkit-scrollbar-thumb{background-image:var(--sf-img-15);height:109px;background-repeat:no-repeat}.dropdown-wrapper .dropdown-list-item.selected,.dropdown-wrapper .dropdown-list-item:hover{color:#fff;background-color:#767676}div[class^=country-flag-]{width:34px;height:20px;background-size:34px 20px}div[class^=country-flag-] img{width:34px;height:20px;background-repeat:no-repeat;background-size:34px 20px}.list{font-family:SourceSansProRegular,sans-serif;font-size:14px;padding:8px 0 16px}.list-header{width:100%;padding-bottom:16px}.list-header-name{float:left;width:68%;margin-right:5%;font-family:SourceSansProSemiBold,sans-serif}.list-header-default{float:left;color:gray}.list-item{width:100%;height:auto;min-height:48px;border-bottom:1px solid #ccc;padding:0}.list-item.editing{border-bottom:0}.list-item-name{width:68%;text-overflow:ellipsis;float:left;white-space:nowrap;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;margin-right:6%;padding:16px 0 0}.list-item-radio{clear:none;float:left;margin-top:13px}.list-item-edit-button{display:block;width:auto;height:auto;border:0;padding:0;color:#000;font-family:SourceSansProRegular,sans-serif;font-size:14px;clear:both}.theme-ghs .list-item-edit-button{height:auto;padding:0;margin:0 0 16px 42px;font-size:14px;text-transform:none;letter-spacing:normal;text-align:left;cursor:pointer;font-family:SourceSansProRegular,sans-serif;color:#0073b1;text-decoration:none}.theme-george .list-item-edit-button{height:auto;padding:0;margin:0 0 16px 42px;font-size:14px;text-transform:none;letter-spacing:normal;text-align:left;cursor:pointer;font-family:LatoRegular,sans-serif;color:#191919;text-decoration:underline}.list-item-edit-button:hover{text-decoration:underline}.list-item-delete-button{display:block;width:auto;border:0;float:right;color:#000;font-family:SourceSansProRegular,sans-serif;font-size:14px}.theme-ghs .list-item-delete-button{height:auto;padding:0;margin:0;font-size:14px;text-transform:none;letter-spacing:normal;font-family:SourceSansProRegular,sans-serif;text-decoration:underline;cursor:pointer}.theme-george .list-item-delete-button{height:auto;padding:0;margin:0;font-size:14px;text-transform:none;letter-spacing:normal;font-family:LatoRegular,sans-serif;text-decoration:underline;cursor:pointer}.list-item-delete-button:hover{text-decoration:underline}.list-item-delete-button img{margin-top:6px;float:right}.phone-input{width:100%}@media (min-width:320px){.phone-input{display:flex;flex-direction:column}}@media (min-width:481px){.phone-input{flex-direction:row;height:52px}}.phone-input .dropdown-wrapper{float:left;height:42px;margin-right:8px;color:#3d3d3d;font-family:SourceSansProRegular,sans-serif;transition:all 200ms ease;font-size:1em;user-select:none}@media (min-width:320px){.phone-input .dropdown-wrapper{width:100%}}@media (min-width:481px){.phone-input .dropdown-wrapper{width:32%}}.phone-input .dropdown-wrapper .dropdown-header{cursor:pointer;width:100%;height:40px;padding:0 8px;border-radius:4px;border:1px solid #ccc;transition:box-shadow 200ms ease}.phone-input .dropdown-wrapper .dropdown-header.active{height:41px;border-top-color:#767676;border-right-color:#767676;border-left-color:#767676;border-bottom-left-radius:0;border-bottom-right-radius:0;top:0}.phone-input .dropdown-wrapper .dropdown-header.active .country-code{transition:none;margin-top:-1px}.phone-input .dropdown-wrapper .dropdown-header.active div[class^=country-flag-] img{transition:none;margin-top:-1px}.phone-input .dropdown-wrapper .dropdown-header.active .down-arrow{transform:rotate(0.5turn);padding-bottom:4px}.phone-input .dropdown-wrapper .dropdown-header:hover{box-shadow:0 2px 10px 0 rgba(61,61,61,.1)}.phone-input .dropdown-wrapper .dropdown-header .country-code{font-size:16px;padding-left:4px}@media (min-width:320px){.phone-input .dropdown-wrapper .dropdown-header .country-code{flex:1;padding-left:15px;display:flex;align-items:center}}.phone-input .dropdown-wrapper .dropdown-header .country-code .country-name{width:100px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:215px}@media (min-width:481px){.phone-input .dropdown-wrapper .dropdown-header .country-code .country-name{display:none}}.phone-input .dropdown-wrapper .dropdown-header .down-arrow{width:20px;padding-top:4px}.phone-input .dropdown-wrapper .dropdown-header div[class^=country-flag-] img{width:32px;background-repeat:no-repeat;background-size:34px 20px}.phone-input .dropdown-wrapper .list-wrapper{position:relative}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list{margin-top:-1px;z-index:10;position:absolute;height:200px;border:1px solid #767676;border-radius:0 0 4px 4px;background-color:#fff;padding:0;overflow-y:scroll;color:#767676}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list{width:100%}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list{width:372px}}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list::-webkit-scrollbar *{background:transparent}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{color:#191919;height:40px;font-size:1em}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item:hover{background-color:#f6f6f6}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{padding:12px 8px 12px 8px;height:48px}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item{padding:12px 80px 12px 8px;height:40px}}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{line-height:initial}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{width:calc(100% - 32px)}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-name{width:420px}}.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{text-align:right}@media (min-width:320px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{width:60px}}@media (min-width:481px){.phone-input .dropdown-wrapper .list-wrapper .dropdown-list .dropdown-list-item .country-code{width:80px}}@media (min-width:320px){.phone-input .input-box{width:100%;margin-top:12px}}@media (min-width:481px){.phone-input .input-box{width:calc(68% - 8px);margin-top:0px}}.card-name{line-height:initial}.card-name .custom-icon:before{content:"";width:30px;margin-right:12px;margin-top:0px;height:20px;float:left;background-image:var(--sf-img-16)}.card-name .custom-icon.amex:before{background-image:var(--sf-img-17)}.card-name .custom-icon.maestro:before{background-image:var(--sf-img-18)}.card-name .custom-icon.mastercard:before{background-image:var(--sf-img-19)}.card-name .custom-icon.visa:before{background-image:var(--sf-img-20)}.card-name .card-info{font-size:12px;clear:both;padding-left:42px;padding-bottom:12px;color:#767676}.card-name .card-info .highlight-red{color:#d43030}.modal{position:absolute;z-index:1000;top:8px;left:8px;display:block;width:536px;max-width:calc(100% - 16px);height:auto;border-radius:8px;box-shadow:2px 3px 8px 3px rgba(0,0,0,.1);border:solid 1px #ccc;background-color:#fff;font-style:normal;font-stretch:normal;line-height:normal;color:#3d3d3d}.theme-ghs .modal{font-family:SourceSansProRegular,sans-serif}.theme-george .modal{font-family:LatoRegular,sans-serif}.modal-title{font-size:22px;padding:16px;border-bottom:solid 1px #ccc}.theme-ghs .modal-title{font-family:SourceSansProSemiBold,sans-serif}.theme-george .modal-title{font-family:LatoRegular,sans-serif}.modal-content{font-size:16px;padding:16px;border-bottom:solid 1px #ccc}.modal-footer{display:flex;justify-content:center;padding:16px}.modal-footer .half{width:45%}.modal-footer .full{width:100%}.input-error.server-error{margin:0 0 12px}.theme-ghs .help-icon{width:auto;height:auto;border:0;padding:0;margin:-7px 0 0 4px;float:left}.theme-george .help-icon{width:auto;height:auto;border:0;padding:0;margin:-7px 0 0 4px;float:left}.bubble{position:absolute;top:100px;left:16px;width:288px;height:auto;background-color:#191919;color:#fff;padding:12px 30px 12px 12px;border-radius:8px;font-size:14px;z-index:1000;font-family:SourceSansProRegular,sans-serif;box-shadow:0 2px 10px 0 rgba(0,0,0,.2);line-height:18px}.bubble .close-icon{position:absolute;top:8px;right:8px;width:auto;height:auto;border:0;padding:0;margin:0;background:transparent}.bubble .triangle{width:0;height:0;border-left:10px solid transparent;border-right:10px solid transparent;border-bottom:10px solid #191919;left:20px;top:-10px;position:absolute}.alert-box{display:flex;flex-direction:row;width:auto;height:auto;border-radius:4px;background-color:rgba(247,204,0,.2);margin-bottom:16px;padding:10px 24px 10px 8px}.alert-box img{width:24px;height:24px;object-fit:contain}.alert-box .alert-message{padding:0;margin-left:8px;font-size:16px;color:#3d3d3d}.divider{width:100%;border-top:1px solid #cbcbcb;margin-bottom:24px}</style><meta http-equiv="origin-trial" content="A3v9QjmVUCOO7YqFMKHP/NKbn6kY1G1pa2S1TfeXJZUD/tysMONTy6lV0Jkou3rrCjSKRGbqTrgTaZkm1XJ7pQUAAACKeyJvcmlnaW4iOiJodHRwczovL2dvb2dsZXRhZ21hbmFnZXIuY29tOjQ0MyIsImZlYXR1cmUiOiJDb252ZXJzaW9uTWVhc3VyZW1lbnQiLCJleHBpcnkiOjE2NDMxNTUxOTksImlzU3ViZG9tYWluIjp0cnVlLCJpc1RoaXJkUGFydHkiOnRydWV9"><style id="onetrust-style">#onetrust-banner-sdk{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}#onetrust-banner-sdk .onetrust-vendors-list-handler{cursor:pointer;color:#1f96db;font-size:inherit;font-weight:bold;text-decoration:none;margin-left:5px}#onetrust-banner-sdk .onetrust-vendors-list-handler:hover{color:#1f96db}#onetrust-banner-sdk:focus{outline:2px solid #000;outline-offset:-2px}#onetrust-banner-sdk a:focus{outline:2px solid #000}#onetrust-banner-sdk #onetrust-accept-btn-handler,#onetrust-banner-sdk #onetrust-reject-all-handler,#onetrust-banner-sdk #onetrust-pc-btn-handler{outline-offset:1px}#onetrust-banner-sdk .ot-close-icon,#onetrust-pc-sdk .ot-close-icon,#ot-sync-ntfy .ot-close-icon{background-image:url("resources/sample/10.svg");background-size:contain;background-repeat:no-repeat;background-position:center;height:12px;width:12px}#onetrust-banner-sdk .powered-by-logo,#onetrust-banner-sdk .ot-pc-footer-logo a,#onetrust-pc-sdk .powered-by-logo,#onetrust-pc-sdk .ot-pc-footer-logo a,#ot-sync-ntfy .powered-by-logo,#ot-sync-ntfy .ot-pc-footer-logo a{background-size:contain;background-repeat:no-repeat;background-position:center;height:25px;width:152px;display:block}#onetrust-banner-sdk h3 *,#onetrust-banner-sdk h4 *,#onetrust-banner-sdk h6 *,#onetrust-banner-sdk button *,#onetrust-banner-sdk a[data-parent-id] *,#onetrust-pc-sdk h3 *,#onetrust-pc-sdk h4 *,#onetrust-pc-sdk h6 *,#onetrust-pc-sdk button *,#onetrust-pc-sdk a[data-parent-id] *,#ot-sync-ntfy h3 *,#ot-sync-ntfy h4 *,#ot-sync-ntfy h6 *,#ot-sync-ntfy button *,#ot-sync-ntfy a[data-parent-id] *{font-size:inherit;font-weight:inherit;color:inherit}#onetrust-banner-sdk .ot-hide,#onetrust-pc-sdk .ot-hide,#ot-sync-ntfy .ot-hide{display:none!important}#onetrust-pc-sdk .ot-sdk-row .ot-sdk-column{padding:0}#onetrust-pc-sdk .ot-sdk-container{padding-right:0}#onetrust-pc-sdk .ot-sdk-row{flex-direction:initial;width:100%}#onetrust-pc-sdk [type="checkbox"]:checked,#onetrust-pc-sdk [type="checkbox"]:not(:checked){pointer-events:initial}#onetrust-pc-sdk [type="checkbox"]:disabled+label::before,#onetrust-pc-sdk [type="checkbox"]:disabled+label:after,#onetrust-pc-sdk [type="checkbox"]:disabled+label{pointer-events:none;opacity:0.7}#onetrust-pc-sdk #vendor-list-content{transform:translate3d(0,0,0)}#onetrust-pc-sdk li input[type="checkbox"]{z-index:1}#onetrust-pc-sdk li .ot-checkbox label{z-index:2}#onetrust-pc-sdk li .ot-checkbox input[type="checkbox"]{height:auto;width:auto}#onetrust-pc-sdk li .host-title a,#onetrust-pc-sdk li .ot-host-name a,#onetrust-pc-sdk li .accordion-text,#onetrust-pc-sdk li .ot-acc-txt{z-index:2;position:relative}#onetrust-pc-sdk input{margin:3px 0.1ex}#onetrust-pc-sdk .pc-logo,#onetrust-pc-sdk .ot-pc-logo{height:60px;width:180px;background-position:center;background-size:contain;background-repeat:no-repeat}#onetrust-pc-sdk .screen-reader-only,#onetrust-pc-sdk .ot-scrn-rdr,.ot-sdk-cookie-policy .screen-reader-only,.ot-sdk-cookie-policy .ot-scrn-rdr{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}#onetrust-pc-sdk.ot-fade-in,.onetrust-pc-dark-filter.ot-fade-in,#onetrust-banner-sdk.ot-fade-in{animation-name:onetrust-fade-in;animation-duration:400ms;animation-timing-function:ease-in-out}#onetrust-pc-sdk.ot-hide{display:none!important}.onetrust-pc-dark-filter.ot-hide{display:none!important}#ot-sdk-btn.ot-sdk-show-settings,#ot-sdk-btn.optanon-show-settings{color:#68b631;border:1px solid #68b631;height:auto;white-space:normal;word-wrap:break-word;padding:0.8em 2em;font-size:0.8em;line-height:1.2;cursor:pointer;-moz-transition:0.1s ease;-o-transition:0.1s ease;-webkit-transition:1s ease;transition:0.1s ease}#ot-sdk-btn.ot-sdk-show-settings:hover,#ot-sdk-btn.optanon-show-settings:hover{color:#fff;background-color:#68b631}.onetrust-pc-dark-filter{background:rgba(0,0,0,0.5);z-index:2147483646;width:100%;height:100%;overflow:hidden;position:fixed;top:0;bottom:0;left:0}@keyframes onetrust-fade-in{0%{opacity:0}100%{opacity:1}}.ot-cookie-label{text-decoration:underline}@media only screen and (min-width:426px) and (max-width:896px) and (orientation:landscape){#onetrust-pc-sdk p{font-size:0.75em}}#onetrust-banner-sdk .banner-option-input:focus+label{outline:1px solid #000;outline-style:auto}.category-vendors-list-handler+a:focus,.category-vendors-list-handler+a:focus-visible{outline:2px solid #000}#onetrust-banner-sdk,#onetrust-pc-sdk,#ot-sdk-cookie-policy,#ot-sync-ntfy{font-size:16px}#onetrust-banner-sdk *,#onetrust-banner-sdk ::after,#onetrust-banner-sdk ::before,#onetrust-pc-sdk *,#onetrust-pc-sdk ::after,#onetrust-pc-sdk ::before,#ot-sdk-cookie-policy *,#ot-sdk-cookie-policy ::after,#ot-sdk-cookie-policy ::before,#ot-sync-ntfy *,#ot-sync-ntfy ::after,#ot-sync-ntfy ::before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}#onetrust-banner-sdk div,#onetrust-banner-sdk span,#onetrust-banner-sdk h1,#onetrust-banner-sdk h2,#onetrust-banner-sdk h3,#onetrust-banner-sdk h4,#onetrust-banner-sdk h5,#onetrust-banner-sdk h6,#onetrust-banner-sdk p,#onetrust-banner-sdk img,#onetrust-banner-sdk svg,#onetrust-banner-sdk button,#onetrust-banner-sdk section,#onetrust-banner-sdk a,#onetrust-banner-sdk label,#onetrust-banner-sdk input,#onetrust-banner-sdk ul,#onetrust-banner-sdk li,#onetrust-banner-sdk nav,#onetrust-banner-sdk table,#onetrust-banner-sdk thead,#onetrust-banner-sdk tr,#onetrust-banner-sdk td,#onetrust-banner-sdk tbody,#onetrust-banner-sdk .ot-main-content,#onetrust-banner-sdk .ot-toggle,#onetrust-banner-sdk #ot-content,#onetrust-banner-sdk #ot-pc-content,#onetrust-banner-sdk .checkbox,#onetrust-pc-sdk div,#onetrust-pc-sdk span,#onetrust-pc-sdk h1,#onetrust-pc-sdk h2,#onetrust-pc-sdk h3,#onetrust-pc-sdk h4,#onetrust-pc-sdk h5,#onetrust-pc-sdk h6,#onetrust-pc-sdk p,#onetrust-pc-sdk img,#onetrust-pc-sdk svg,#onetrust-pc-sdk button,#onetrust-pc-sdk section,#onetrust-pc-sdk a,#onetrust-pc-sdk label,#onetrust-pc-sdk input,#onetrust-pc-sdk ul,#onetrust-pc-sdk li,#onetrust-pc-sdk nav,#onetrust-pc-sdk table,#onetrust-pc-sdk thead,#onetrust-pc-sdk tr,#onetrust-pc-sdk td,#onetrust-pc-sdk tbody,#onetrust-pc-sdk .ot-main-content,#onetrust-pc-sdk .ot-toggle,#onetrust-pc-sdk #ot-content,#onetrust-pc-sdk #ot-pc-content,#onetrust-pc-sdk .checkbox,#ot-sdk-cookie-policy div,#ot-sdk-cookie-policy span,#ot-sdk-cookie-policy h1,#ot-sdk-cookie-policy h2,#ot-sdk-cookie-policy h3,#ot-sdk-cookie-policy h4,#ot-sdk-cookie-policy h5,#ot-sdk-cookie-policy h6,#ot-sdk-cookie-policy p,#ot-sdk-cookie-policy img,#ot-sdk-cookie-policy svg,#ot-sdk-cookie-policy button,#ot-sdk-cookie-policy section,#ot-sdk-cookie-policy a,#ot-sdk-cookie-policy label,#ot-sdk-cookie-policy input,#ot-sdk-cookie-policy ul,#ot-sdk-cookie-policy li,#ot-sdk-cookie-policy nav,#ot-sdk-cookie-policy table,#ot-sdk-cookie-policy thead,#ot-sdk-cookie-policy tr,#ot-sdk-cookie-policy td,#ot-sdk-cookie-policy tbody,#ot-sdk-cookie-policy .ot-main-content,#ot-sdk-cookie-policy .ot-toggle,#ot-sdk-cookie-policy #ot-content,#ot-sdk-cookie-policy #ot-pc-content,#ot-sdk-cookie-policy .checkbox,#ot-sync-ntfy div,#ot-sync-ntfy span,#ot-sync-ntfy h1,#ot-sync-ntfy h2,#ot-sync-ntfy h3,#ot-sync-ntfy h4,#ot-sync-ntfy h5,#ot-sync-ntfy h6,#ot-sync-ntfy p,#ot-sync-ntfy img,#ot-sync-ntfy svg,#ot-sync-ntfy button,#ot-sync-ntfy section,#ot-sync-ntfy a,#ot-sync-ntfy label,#ot-sync-ntfy input,#ot-sync-ntfy ul,#ot-sync-ntfy li,#ot-sync-ntfy nav,#ot-sync-ntfy table,#ot-sync-ntfy thead,#ot-sync-ntfy tr,#ot-sync-ntfy td,#ot-sync-ntfy tbody,#ot-sync-ntfy .ot-main-content,#ot-sync-ntfy .ot-toggle,#ot-sync-ntfy #ot-content,#ot-sync-ntfy #ot-pc-content,#ot-sync-ntfy .checkbox{font-family:inherit;font-weight:normal;-webkit-font-smoothing:auto;letter-spacing:normal;line-height:normal;padding:0;margin:0;height:auto;min-height:0;max-height:none;width:auto;min-width:0;max-width:none;border-radius:0;border:none;clear:none;float:none;position:static;bottom:auto;left:auto;right:auto;top:auto;text-align:left;text-decoration:none;text-indent:0;text-shadow:none;text-transform:none;white-space:normal;background:none;overflow:visible;vertical-align:baseline;visibility:visible;z-index:auto;box-shadow:none}#onetrust-banner-sdk label:before,#onetrust-banner-sdk label:after,#onetrust-banner-sdk .checkbox:after,#onetrust-banner-sdk .checkbox:before,#onetrust-pc-sdk label:before,#onetrust-pc-sdk label:after,#onetrust-pc-sdk .checkbox:after,#onetrust-pc-sdk .checkbox:before,#ot-sdk-cookie-policy label:before,#ot-sdk-cookie-policy label:after,#ot-sdk-cookie-policy .checkbox:after,#ot-sdk-cookie-policy .checkbox:before,#ot-sync-ntfy label:before,#ot-sync-ntfy label:after,#ot-sync-ntfy .checkbox:after,#ot-sync-ntfy .checkbox:before{content:"";content:none}#onetrust-banner-sdk .ot-sdk-container,#onetrust-pc-sdk .ot-sdk-container,#ot-sdk-cookie-policy .ot-sdk-container{position:relative;width:100%;max-width:100%;margin:0 auto;padding:0 20px;box-sizing:border-box}#onetrust-banner-sdk .ot-sdk-column,#onetrust-banner-sdk .ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-column,#onetrust-pc-sdk .ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-column,#ot-sdk-cookie-policy .ot-sdk-columns{width:100%;float:left;box-sizing:border-box;padding:0;display:initial}@media (min-width:400px){#onetrust-banner-sdk .ot-sdk-container,#onetrust-pc-sdk .ot-sdk-container,#ot-sdk-cookie-policy .ot-sdk-container{width:90%;padding:0}}@media (min-width:550px){#onetrust-banner-sdk .ot-sdk-container,#onetrust-pc-sdk .ot-sdk-container,#ot-sdk-cookie-policy .ot-sdk-container{width:100%}#onetrust-banner-sdk .ot-sdk-column,#onetrust-banner-sdk .ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-column,#onetrust-pc-sdk .ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-column,#ot-sdk-cookie-policy .ot-sdk-columns{margin-left:4%}#onetrust-banner-sdk .ot-sdk-column:first-child,#onetrust-banner-sdk .ot-sdk-columns:first-child,#onetrust-pc-sdk .ot-sdk-column:first-child,#onetrust-pc-sdk .ot-sdk-columns:first-child,#ot-sdk-cookie-policy .ot-sdk-column:first-child,#ot-sdk-cookie-policy .ot-sdk-columns:first-child{margin-left:0}#onetrust-banner-sdk .ot-sdk-two.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-two.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-two.ot-sdk-columns{width:13.3333333333%}#onetrust-banner-sdk .ot-sdk-three.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-three.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-three.ot-sdk-columns{width:22%}#onetrust-banner-sdk .ot-sdk-four.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-four.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-four.ot-sdk-columns{width:30.6666666667%}#onetrust-banner-sdk .ot-sdk-eight.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-eight.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-eight.ot-sdk-columns{width:65.3333333333%}#onetrust-banner-sdk .ot-sdk-nine.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-nine.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-nine.ot-sdk-columns{width:74%}#onetrust-banner-sdk .ot-sdk-ten.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-ten.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-ten.ot-sdk-columns{width:82.6666666667%}#onetrust-banner-sdk .ot-sdk-eleven.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-eleven.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-eleven.ot-sdk-columns{width:91.3333333333%}#onetrust-banner-sdk .ot-sdk-twelve.ot-sdk-columns,#onetrust-pc-sdk .ot-sdk-twelve.ot-sdk-columns,#ot-sdk-cookie-policy .ot-sdk-twelve.ot-sdk-columns{width:100%;margin-left:0}}#onetrust-banner-sdk h1,#onetrust-banner-sdk h2,#onetrust-banner-sdk h3,#onetrust-banner-sdk h4,#onetrust-banner-sdk h5,#onetrust-banner-sdk h6,#onetrust-pc-sdk h1,#onetrust-pc-sdk h2,#onetrust-pc-sdk h3,#onetrust-pc-sdk h4,#onetrust-pc-sdk h5,#onetrust-pc-sdk h6,#ot-sdk-cookie-policy h1,#ot-sdk-cookie-policy h2,#ot-sdk-cookie-policy h3,#ot-sdk-cookie-policy h4,#ot-sdk-cookie-policy h5,#ot-sdk-cookie-policy h6{margin-top:0;font-weight:600;font-family:inherit}#onetrust-banner-sdk h1,#onetrust-pc-sdk h1,#ot-sdk-cookie-policy h1{font-size:1.5rem;line-height:1.2}#onetrust-banner-sdk h2,#onetrust-pc-sdk h2,#ot-sdk-cookie-policy h2{font-size:1.5rem;line-height:1.25}#onetrust-banner-sdk h3,#onetrust-pc-sdk h3,#ot-sdk-cookie-policy h3{font-size:1.5rem;line-height:1.3}#onetrust-banner-sdk h4,#onetrust-pc-sdk h4,#ot-sdk-cookie-policy h4{font-size:1.5rem;line-height:1.35}#onetrust-banner-sdk h5,#onetrust-pc-sdk h5,#ot-sdk-cookie-policy h5{font-size:1.5rem;line-height:1.5}#onetrust-banner-sdk h6,#onetrust-pc-sdk h6,#ot-sdk-cookie-policy h6{font-size:1.5rem;line-height:1.6}@media (min-width:550px){#onetrust-banner-sdk h1,#onetrust-pc-sdk h1,#ot-sdk-cookie-policy h1{font-size:1.5rem}#onetrust-banner-sdk h2,#onetrust-pc-sdk h2,#ot-sdk-cookie-policy h2{font-size:1.5rem}#onetrust-banner-sdk h3,#onetrust-pc-sdk h3,#ot-sdk-cookie-policy h3{font-size:1.5rem}#onetrust-banner-sdk h4,#onetrust-pc-sdk h4,#ot-sdk-cookie-policy h4{font-size:1.5rem}#onetrust-banner-sdk h5,#onetrust-pc-sdk h5,#ot-sdk-cookie-policy h5{font-size:1.5rem}#onetrust-banner-sdk h6,#onetrust-pc-sdk h6,#ot-sdk-cookie-policy h6{font-size:1.5rem}}#onetrust-banner-sdk p,#onetrust-pc-sdk p,#ot-sdk-cookie-policy p{margin:0 0 1em 0;font-family:inherit;line-height:normal}#onetrust-banner-sdk a,#onetrust-pc-sdk a,#ot-sdk-cookie-policy a{color:#565656;text-decoration:underline}#onetrust-banner-sdk a:hover,#onetrust-pc-sdk a:hover,#ot-sdk-cookie-policy a:hover{color:#565656;text-decoration:none}#onetrust-banner-sdk .ot-sdk-button,#onetrust-banner-sdk button,#onetrust-pc-sdk .ot-sdk-button,#onetrust-pc-sdk button,#ot-sdk-cookie-policy .ot-sdk-button,#ot-sdk-cookie-policy button{margin-bottom:1rem;font-family:inherit}#onetrust-banner-sdk .ot-sdk-button,#onetrust-banner-sdk button,#onetrust-pc-sdk .ot-sdk-button,#onetrust-pc-sdk button,#ot-sdk-cookie-policy .ot-sdk-button,#ot-sdk-cookie-policy button{display:inline-block;height:38px;padding:0 30px;color:#555;text-align:center;font-size:0.9em;font-weight:400;line-height:38px;letter-spacing:0.01em;text-decoration:none;white-space:nowrap;background-color:transparent;border-radius:2px;border:1px solid #bbb;cursor:pointer;box-sizing:border-box}#onetrust-banner-sdk .ot-sdk-button:hover,#onetrust-banner-sdk :not(.ot-leg-btn-container)>button:hover,#onetrust-banner-sdk :not(.ot-leg-btn-container)>button:focus,#onetrust-pc-sdk .ot-sdk-button:hover,#onetrust-pc-sdk :not(.ot-leg-btn-container)>button:hover,#onetrust-pc-sdk :not(.ot-leg-btn-container)>button:focus,#ot-sdk-cookie-policy .ot-sdk-button:hover,#ot-sdk-cookie-policy :not(.ot-leg-btn-container)>button:hover,#ot-sdk-cookie-policy :not(.ot-leg-btn-container)>button:focus{color:#333;border-color:#888;opacity:0.7}#onetrust-banner-sdk .ot-sdk-button:focus,#onetrust-banner-sdk :not(.ot-leg-btn-container)>button:focus,#onetrust-pc-sdk .ot-sdk-button:focus,#onetrust-pc-sdk :not(.ot-leg-btn-container)>button:focus,#ot-sdk-cookie-policy .ot-sdk-button:focus,#ot-sdk-cookie-policy :not(.ot-leg-btn-container)>button:focus{outline:2px solid #000}#onetrust-banner-sdk .ot-sdk-button.ot-sdk-button-primary,#onetrust-banner-sdk button.ot-sdk-button-primary,#onetrust-banner-sdk input[type="submit"].ot-sdk-button-primary,#onetrust-banner-sdk input[type="reset"].ot-sdk-button-primary,#onetrust-banner-sdk input[type="button"].ot-sdk-button-primary,#onetrust-pc-sdk .ot-sdk-button.ot-sdk-button-primary,#onetrust-pc-sdk button.ot-sdk-button-primary,#onetrust-pc-sdk input[type="submit"].ot-sdk-button-primary,#onetrust-pc-sdk input[type="reset"].ot-sdk-button-primary,#onetrust-pc-sdk input[type="button"].ot-sdk-button-primary,#ot-sdk-cookie-policy .ot-sdk-button.ot-sdk-button-primary,#ot-sdk-cookie-policy button.ot-sdk-button-primary,#ot-sdk-cookie-policy input[type="submit"].ot-sdk-button-primary,#ot-sdk-cookie-policy input[type="reset"].ot-sdk-button-primary,#ot-sdk-cookie-policy input[type="button"].ot-sdk-button-primary{color:#fff;background-color:#33c3f0;border-color:#33c3f0}#onetrust-banner-sdk .ot-sdk-button.ot-sdk-button-primary:hover,#onetrust-banner-sdk button.ot-sdk-button-primary:hover,#onetrust-banner-sdk input[type="submit"].ot-sdk-button-primary:hover,#onetrust-banner-sdk input[type="reset"].ot-sdk-button-primary:hover,#onetrust-banner-sdk input[type="button"].ot-sdk-button-primary:hover,#onetrust-banner-sdk .ot-sdk-button.ot-sdk-button-primary:focus,#onetrust-banner-sdk button.ot-sdk-button-primary:focus,#onetrust-banner-sdk input[type="submit"].ot-sdk-button-primary:focus,#onetrust-banner-sdk input[type="reset"].ot-sdk-button-primary:focus,#onetrust-banner-sdk input[type="button"].ot-sdk-button-primary:focus,#onetrust-pc-sdk .ot-sdk-button.ot-sdk-button-primary:hover,#onetrust-pc-sdk button.ot-sdk-button-primary:hover,#onetrust-pc-sdk input[type="submit"].ot-sdk-button-primary:hover,#onetrust-pc-sdk input[type="reset"].ot-sdk-button-primary:hover,#onetrust-pc-sdk input[type="button"].ot-sdk-button-primary:hover,#onetrust-pc-sdk .ot-sdk-button.ot-sdk-button-primary:focus,#onetrust-pc-sdk button.ot-sdk-button-primary:focus,#onetrust-pc-sdk input[type="submit"].ot-sdk-button-primary:focus,#onetrust-pc-sdk input[type="reset"].ot-sdk-button-primary:focus,#onetrust-pc-sdk input[type="button"].ot-sdk-button-primary:focus,#ot-sdk-cookie-policy .ot-sdk-button.ot-sdk-button-primary:hover,#ot-sdk-cookie-policy button.ot-sdk-button-primary:hover,#ot-sdk-cookie-policy input[type="submit"].ot-sdk-button-primary:hover,#ot-sdk-cookie-policy input[type="reset"].ot-sdk-button-primary:hover,#ot-sdk-cookie-policy input[type="button"].ot-sdk-button-primary:hover,#ot-sdk-cookie-policy .ot-sdk-button.ot-sdk-button-primary:focus,#ot-sdk-cookie-policy button.ot-sdk-button-primary:focus,#ot-sdk-cookie-policy input[type="submit"].ot-sdk-button-primary:focus,#ot-sdk-cookie-policy input[type="reset"].ot-sdk-button-primary:focus,#ot-sdk-cookie-policy input[type="button"].ot-sdk-button-primary:focus{color:#fff;background-color:#1eaedb;border-color:#1eaedb}#onetrust-banner-sdk input[type="text"],#onetrust-pc-sdk input[type="text"],#ot-sdk-cookie-policy input[type="text"]{height:38px;padding:6px 10px;background-color:#fff;border:1px solid #d1d1d1;border-radius:4px;box-shadow:none;box-sizing:border-box}#onetrust-banner-sdk input[type="text"],#onetrust-pc-sdk input[type="text"],#ot-sdk-cookie-policy input[type="text"]{-webkit-appearance:none;-moz-appearance:none;appearance:none}#onetrust-banner-sdk input[type="text"]:focus,#onetrust-pc-sdk input[type="text"]:focus,#ot-sdk-cookie-policy input[type="text"]:focus{border:1px solid #000;outline:0}#onetrust-banner-sdk label,#onetrust-pc-sdk label,#ot-sdk-cookie-policy label{display:block;margin-bottom:0.5rem;font-weight:600}#onetrust-banner-sdk input[type="checkbox"],#onetrust-pc-sdk input[type="checkbox"],#ot-sdk-cookie-policy input[type="checkbox"]{display:inline}#onetrust-banner-sdk ul,#onetrust-pc-sdk ul,#ot-sdk-cookie-policy ul{list-style:circle inside}#onetrust-banner-sdk ul,#onetrust-pc-sdk ul,#ot-sdk-cookie-policy ul{padding-left:0;margin-top:0}#onetrust-banner-sdk ul ul,#onetrust-pc-sdk ul ul,#ot-sdk-cookie-policy ul ul{margin:1.5rem 0 1.5rem 3rem;font-size:90%}#onetrust-banner-sdk li,#onetrust-pc-sdk li,#ot-sdk-cookie-policy li{margin-bottom:1rem}#onetrust-banner-sdk th,#onetrust-banner-sdk td,#onetrust-pc-sdk th,#onetrust-pc-sdk td,#ot-sdk-cookie-policy th,#ot-sdk-cookie-policy td{padding:12px 15px;text-align:left;border-bottom:1px solid #e1e1e1}#onetrust-banner-sdk button,#onetrust-pc-sdk button,#ot-sdk-cookie-policy button{margin-bottom:1rem;font-family:inherit}#onetrust-banner-sdk .ot-sdk-container:after,#onetrust-banner-sdk .ot-sdk-row:after,#onetrust-pc-sdk .ot-sdk-container:after,#onetrust-pc-sdk .ot-sdk-row:after,#ot-sdk-cookie-policy .ot-sdk-container:after,#ot-sdk-cookie-policy .ot-sdk-row:after{content:"";display:table;clear:both}#onetrust-banner-sdk .ot-sdk-row,#onetrust-pc-sdk .ot-sdk-row,#ot-sdk-cookie-policy .ot-sdk-row{margin:0;max-width:none;display:block}#onetrust-banner-sdk{box-shadow:0 0 18px rgba(0,0,0,.2)}#onetrust-banner-sdk.otFlat{position:fixed;z-index:2147483645;bottom:0;right:0;left:0;background-color:#fff;max-height:90%;overflow-x:hidden;overflow-y:auto}#onetrust-banner-sdk.otFlat.top{top:0px;bottom:auto}#onetrust-banner-sdk.otRelFont{font-size:1rem}#onetrust-banner-sdk>.ot-sdk-container{overflow:hidden}#onetrust-banner-sdk::-webkit-scrollbar{width:11px}#onetrust-banner-sdk::-webkit-scrollbar-thumb{border-radius:10px;background:#c1c1c1}#onetrust-banner-sdk{scrollbar-arrow-color:#c1c1c1;scrollbar-darkshadow-color:#c1c1c1;scrollbar-face-color:#c1c1c1;scrollbar-shadow-color:#c1c1c1}#onetrust-banner-sdk #onetrust-policy{margin:1.25em 0 .625em 2em;overflow:hidden}#onetrust-banner-sdk #onetrust-policy .ot-gv-list-handler{float:left;font-size:.82em;padding:0;margin-bottom:0;border:0;line-height:normal;height:auto;width:auto}#onetrust-banner-sdk #onetrust-policy-title{font-size:1.2em;line-height:1.3;margin-bottom:10px}#onetrust-banner-sdk #onetrust-policy-text{clear:both;text-align:left;font-size:.88em;line-height:1.4}#onetrust-banner-sdk #onetrust-policy-text *{font-size:inherit;line-height:inherit}#onetrust-banner-sdk #onetrust-policy-text a{font-weight:bold;margin-left:5px}#onetrust-banner-sdk #onetrust-policy-title,#onetrust-banner-sdk #onetrust-policy-text{color:dimgray;float:left}#onetrust-banner-sdk #onetrust-button-group-parent{min-height:1px;text-align:center}#onetrust-banner-sdk #onetrust-button-group{display:inline-block}#onetrust-banner-sdk #onetrust-accept-btn-handler,#onetrust-banner-sdk #onetrust-reject-all-handler,#onetrust-banner-sdk #onetrust-pc-btn-handler{background-color:#68b631;color:#fff;border-color:#68b631;margin-right:1em;min-width:125px;height:auto;white-space:normal;word-break:break-word;word-wrap:break-word;padding:12px 10px;line-height:1.2;font-size:.813em;font-weight:600}#onetrust-banner-sdk #onetrust-pc-btn-handler.cookie-setting-link{background-color:#fff;border:none;color:#68b631;text-decoration:underline;padding-left:0;padding-right:0}#onetrust-banner-sdk .onetrust-close-btn-ui{width:44px;height:44px;background-size:12px;border:none;position:relative;margin:auto;padding:0}#onetrust-banner-sdk .banner_logo{display:none}#onetrust-banner-sdk .ot-b-addl-desc{clear:both;float:left;display:block}#onetrust-banner-sdk #banner-options{float:left;display:table;margin-right:0;margin-left:1em;width:calc(100% - 1em)}#onetrust-banner-sdk .banner-option-input{cursor:pointer;width:auto;height:auto;border:none;padding:0;padding-right:3px;margin:0 0 10px;font-size:.82em;line-height:1.4}#onetrust-banner-sdk .banner-option-input *{pointer-events:none;font-size:inherit;line-height:inherit}#onetrust-banner-sdk .banner-option-input[aria-expanded=true]~.banner-option-details{display:block;height:auto}#onetrust-banner-sdk .banner-option-input[aria-expanded=true] .ot-arrow-container{transform:rotate(90deg)}#onetrust-banner-sdk .banner-option{margin-bottom:12px;margin-left:0;border:none;float:left;padding:0}#onetrust-banner-sdk .banner-option:first-child{padding-left:2px}#onetrust-banner-sdk .banner-option:not(:first-child){padding:0;border:none}#onetrust-banner-sdk .banner-option-header{cursor:pointer;display:inline-block}#onetrust-banner-sdk .banner-option-header :first-child{color:dimgray;font-weight:bold;float:left}#onetrust-banner-sdk .banner-option-header .ot-arrow-container{display:inline-block;border-top:6px solid transparent;border-bottom:6px solid transparent;border-left:6px solid dimgray;margin-left:10px;vertical-align:middle}#onetrust-banner-sdk .banner-option-details{display:none;font-size:.83em;line-height:1.5;padding:10px 0px 5px 10px;margin-right:10px;height:0px}#onetrust-banner-sdk .banner-option-details *{font-size:inherit;line-height:inherit;color:dimgray}#onetrust-banner-sdk .ot-arrow-container,#onetrust-banner-sdk .banner-option-details{transition:all 300ms ease-in 0s;-webkit-transition:all 300ms ease-in 0s;-moz-transition:all 300ms ease-in 0s;-o-transition:all 300ms ease-in 0s}#onetrust-banner-sdk .ot-dpd-container{float:left}#onetrust-banner-sdk .ot-dpd-title{margin-bottom:10px}#onetrust-banner-sdk .ot-dpd-title,#onetrust-banner-sdk .ot-dpd-desc{font-size:.88em;line-height:1.4;color:dimgray}#onetrust-banner-sdk .ot-dpd-title *,#onetrust-banner-sdk .ot-dpd-desc *{font-size:inherit;line-height:inherit}#onetrust-banner-sdk.ot-iab-2 #onetrust-policy-text *{margin-bottom:0}#onetrust-banner-sdk.ot-iab-2 .onetrust-vendors-list-handler{display:block;margin-left:0;margin-top:5px;clear:both;margin-bottom:0;padding:0;border:0;height:auto;width:auto}#onetrust-banner-sdk.ot-iab-2 #onetrust-button-group button{display:block}#onetrust-banner-sdk.ot-close-btn-link{padding-top:25px}#onetrust-banner-sdk.ot-close-btn-link #onetrust-close-btn-container{top:15px;transform:none;right:15px}#onetrust-banner-sdk.ot-close-btn-link #onetrust-close-btn-container button{padding:0;white-space:pre-wrap;border:none;height:auto;line-height:1.5;text-decoration:underline;font-size:.69em}#onetrust-banner-sdk #onetrust-policy-text,#onetrust-banner-sdk .ot-dpd-desc,#onetrust-banner-sdk .ot-b-addl-desc{font-size:.813em;line-height:1.5}#onetrust-banner-sdk .ot-dpd-desc{margin-bottom:10px}#onetrust-banner-sdk .ot-dpd-desc>.ot-b-addl-desc{margin-top:10px;margin-bottom:10px;font-size:1em}@media only screen and (max-width:425px){#onetrust-banner-sdk #onetrust-close-btn-container{position:absolute;top:6px;right:2px}#onetrust-banner-sdk #onetrust-policy{margin-left:0;margin-top:3em}#onetrust-banner-sdk #onetrust-button-group{display:block}#onetrust-banner-sdk #onetrust-accept-btn-handler,#onetrust-banner-sdk #onetrust-reject-all-handler,#onetrust-banner-sdk #onetrust-pc-btn-handler{width:100%}#onetrust-banner-sdk .onetrust-close-btn-ui{top:auto;transform:none}#onetrust-banner-sdk #onetrust-policy-title{display:inline;float:none}#onetrust-banner-sdk #banner-options{margin:0;padding:0;width:100%}}@media only screen and (min-width:426px)and (max-width:896px){#onetrust-banner-sdk #onetrust-close-btn-container{position:absolute;top:0;right:0}#onetrust-banner-sdk #onetrust-policy{margin-left:1em;margin-right:1em}#onetrust-banner-sdk .onetrust-close-btn-ui{top:10px;right:10px}#onetrust-banner-sdk:not(.ot-iab-2) #onetrust-group-container{width:95%}#onetrust-banner-sdk.ot-iab-2 #onetrust-group-container{width:100%}#onetrust-banner-sdk #onetrust-button-group-parent{width:100%;position:relative;margin-left:0}#onetrust-banner-sdk #onetrust-button-group button{display:inline-block}#onetrust-banner-sdk #onetrust-button-group{margin-right:0;text-align:center}#onetrust-banner-sdk .has-reject-all-button #onetrust-pc-btn-handler{float:left}#onetrust-banner-sdk .has-reject-all-button #onetrust-reject-all-handler,#onetrust-banner-sdk .has-reject-all-button #onetrust-accept-btn-handler{float:right}#onetrust-banner-sdk .has-reject-all-button #onetrust-button-group{width:calc(100% - 2em);margin-right:0}#onetrust-banner-sdk .has-reject-all-button #onetrust-pc-btn-handler.cookie-setting-link{padding-left:0px;text-align:left}#onetrust-banner-sdk.ot-buttons-fw .ot-sdk-three button{width:100%;text-align:center}#onetrust-banner-sdk.ot-buttons-fw #onetrust-button-group-parent button{float:none}#onetrust-banner-sdk.ot-buttons-fw #onetrust-pc-btn-handler.cookie-setting-link{text-align:center}}@media only screen and (min-width:550px){#onetrust-banner-sdk .banner-option:not(:first-child){border-left:1px solid #d8d8d8;padding-left:25px}}@media only screen and (min-width:425px)and (max-width:550px){#onetrust-banner-sdk.ot-iab-2 #onetrust-button-group,#onetrust-banner-sdk.ot-iab-2 #onetrust-policy,#onetrust-banner-sdk.ot-iab-2 .banner-option{width:100%}}@media only screen and (min-width:769px){#onetrust-banner-sdk #onetrust-button-group{margin-right:30%}#onetrust-banner-sdk #banner-options{margin-left:2em;margin-right:5em;margin-bottom:1.25em;width:calc(100% - 7em)}}@media only screen and (min-width:897px)and (max-width:1023px){#onetrust-banner-sdk.vertical-align-content #onetrust-button-group-parent{position:absolute;top:50%;left:75%;transform:translateY(-50%)}#onetrust-banner-sdk #onetrust-close-btn-container{top:50%;margin:auto;transform:translate(-50%,-50%);position:absolute;padding:0;right:0}#onetrust-banner-sdk #onetrust-close-btn-container button{position:relative;margin:0;right:-22px;top:2px}}@media only screen and (min-width:1024px){#onetrust-banner-sdk #onetrust-close-btn-container{top:50%;margin:auto;transform:translate(-50%,-50%);position:absolute;right:0}#onetrust-banner-sdk #onetrust-close-btn-container button{right:-12px}#onetrust-banner-sdk #onetrust-policy{margin-left:2em}#onetrust-banner-sdk.vertical-align-content #onetrust-button-group-parent{position:absolute;top:50%;left:60%;transform:translateY(-50%)}#onetrust-banner-sdk.ot-iab-2 #onetrust-policy-title{width:50%}#onetrust-banner-sdk.ot-iab-2 #onetrust-policy-text,#onetrust-banner-sdk.ot-iab-2 :not(.ot-dpd-desc)>.ot-b-addl-desc{margin-bottom:1em;width:50%;border-right:1px solid #d8d8d8;padding-right:1rem}#onetrust-banner-sdk.ot-iab-2 #onetrust-policy-text{margin-bottom:0;padding-bottom:1em}#onetrust-banner-sdk.ot-iab-2 :not(.ot-dpd-desc)>.ot-b-addl-desc{margin-bottom:0;padding-bottom:1em}#onetrust-banner-sdk.ot-iab-2 .ot-dpd-container{width:45%;padding-left:1rem;display:inline-block;float:none}#onetrust-banner-sdk.ot-iab-2 .ot-dpd-title{line-height:1.7}#onetrust-banner-sdk.ot-iab-2 #onetrust-button-group-parent{left:auto;right:4%;margin-left:0}#onetrust-banner-sdk.ot-iab-2 #onetrust-button-group button{display:block}#onetrust-banner-sdk:not(.ot-iab-2) #onetrust-button-group-parent{margin:auto;width:30%}#onetrust-banner-sdk:not(.ot-iab-2) #onetrust-group-container{width:60%}#onetrust-banner-sdk #onetrust-button-group{margin-right:auto}#onetrust-banner-sdk #onetrust-accept-btn-handler,#onetrust-banner-sdk #onetrust-reject-all-handler,#onetrust-banner-sdk #onetrust-pc-btn-handler{margin-top:1em}}@media only screen and (min-width:890px){#onetrust-banner-sdk.ot-buttons-fw:not(.ot-iab-2) #onetrust-button-group-parent{padding-left:3%;padding-right:4%;margin-left:0}#onetrust-banner-sdk.ot-buttons-fw:not(.ot-iab-2) #onetrust-button-group{margin-right:0;margin-top:1.25em;width:100%}#onetrust-banner-sdk.ot-buttons-fw:not(.ot-iab-2) #onetrust-button-group button{width:100%;margin-bottom:5px;margin-top:5px}#onetrust-banner-sdk.ot-buttons-fw:not(.ot-iab-2) #onetrust-button-group button:last-of-type{margin-bottom:20px}}@media only screen and (min-width:1280px){#onetrust-banner-sdk:not(.ot-iab-2) #onetrust-group-container{width:55%}#onetrust-banner-sdk:not(.ot-iab-2) #onetrust-button-group-parent{width:44%;padding-left:2%;padding-right:2%}#onetrust-banner-sdk:not(.ot-iab-2).vertical-align-content #onetrust-button-group-parent{position:absolute;left:55%}}#onetrust-consent-sdk #onetrust-banner-sdk{background-color:#191919}#onetrust-consent-sdk #onetrust-policy-title,#onetrust-consent-sdk #onetrust-policy-text,#onetrust-consent-sdk .ot-b-addl-desc,#onetrust-consent-sdk .ot-dpd-desc,#onetrust-consent-sdk .ot-dpd-title,#onetrust-consent-sdk #onetrust-policy-text *:not(.onetrust-vendors-list-handler),#onetrust-consent-sdk .ot-dpd-desc *:not(.onetrust-vendors-list-handler),#onetrust-consent-sdk #onetrust-banner-sdk #banner-options *,#onetrust-banner-sdk .ot-cat-header{color:#FFFFFF}#onetrust-consent-sdk #onetrust-banner-sdk .banner-option-details{background-color:#E9E9E9}#onetrust-consent-sdk #onetrust-banner-sdk a[href],#onetrust-consent-sdk #onetrust-banner-sdk a[href] font,#onetrust-consent-sdk #onetrust-banner-sdk .ot-link-btn{color:#FFFFFF}#onetrust-consent-sdk #onetrust-accept-btn-handler,#onetrust-banner-sdk #onetrust-reject-all-handler{background-color:#FDC301;border-color:#FDC301;color:#000000}#onetrust-consent-sdk #onetrust-banner-sdk *:focus,#onetrust-consent-sdk #onetrust-banner-sdk:focus{outline-color:#000000;outline-width:1px}#onetrust-consent-sdk #onetrust-pc-btn-handler,#onetrust-consent-sdk #onetrust-pc-btn-handler.cookie-setting-link{color:#191919;border-color:#191919;background-color:#FDC301}#onetrust-consent-sdk{font-family:Arial,sans-serif}#onetrust-consent-sdk #onetrust-policy-title{font-size:12pt;margin-bottom:5px}#onetrust-consent-sdk #onetrust-policy-text{margin-bottom:0px}#onetrust-consent-sdk #onetrust-pc-btn-handler.cookie-setting-link{color:#fff}@media only screen and (max-width:1024px){#onetrust-banner-sdk #onetrust-policy{margin-top:5px}}@media only screen and (max-width:425px){#onetrust-banner-sdk #onetrust-button-group{display:flex}}#onetrust-pc-sdk.otPcCenter{overflow:hidden;position:fixed;margin:0 auto;top:5%;right:0;left:0;width:40%;max-width:575px;min-width:575px;border-radius:2.5px;z-index:2147483647;background-color:#fff;-webkit-box-shadow:0px 2px 10px -3px #999;-moz-box-shadow:0px 2px 10px -3px #999;box-shadow:0px 2px 10px -3px #999}#onetrust-pc-sdk.otPcCenter[dir=rtl]{right:0;left:0}#onetrust-pc-sdk.otRelFont{font-size:1rem}#onetrust-pc-sdk #ot-addtl-venlst .ot-arw-cntr,#onetrust-pc-sdk #ot-addtl-venlst .ot-plus-minus,#onetrust-pc-sdk .ot-hide-tgl{visibility:hidden}#onetrust-pc-sdk #ot-addtl-venlst .ot-arw-cntr *,#onetrust-pc-sdk #ot-addtl-venlst .ot-plus-minus *,#onetrust-pc-sdk .ot-hide-tgl *{visibility:hidden}#onetrust-pc-sdk #ot-gn-venlst .ot-ven-item .ot-acc-hdr{min-height:40px}#onetrust-pc-sdk .ot-pc-header{height:39px;padding:10px 0 10px 30px;border-bottom:1px solid #e9e9e9}#onetrust-pc-sdk #ot-pc-title,#onetrust-pc-sdk #ot-category-title,#onetrust-pc-sdk .ot-cat-header,#onetrust-pc-sdk #ot-lst-title,#onetrust-pc-sdk .ot-ven-hdr .ot-ven-name,#onetrust-pc-sdk .ot-always-active{font-weight:bold;color:dimgray}#onetrust-pc-sdk .ot-cat-header{float:left;font-weight:600;font-size:.875em;line-height:1.5;max-width:90%;vertical-align:middle}#onetrust-pc-sdk .ot-always-active-group .ot-cat-header{width:55%;font-weight:700}#onetrust-pc-sdk .ot-cat-item p{clear:both;float:left;margin-top:10px;margin-bottom:5px;line-height:1.5;font-size:.812em;color:dimgray}#onetrust-pc-sdk .ot-close-icon{height:44px;width:44px;background-size:10px}#onetrust-pc-sdk #ot-pc-title{float:left;font-size:1em;line-height:1.5;margin-bottom:10px;margin-top:10px;width:100%}#onetrust-pc-sdk #accept-recommended-btn-handler{margin-right:10px;margin-bottom:25px;outline-offset:-1px}#onetrust-pc-sdk #ot-pc-desc{clear:both;width:100%;font-size:.812em;line-height:1.5;margin-bottom:25px}#onetrust-pc-sdk #ot-pc-desc a{margin-left:5px}#onetrust-pc-sdk #ot-pc-desc *{font-size:inherit;line-height:inherit}#onetrust-pc-sdk #ot-pc-desc ul li{padding:10px 0px}#onetrust-pc-sdk a{color:#656565;cursor:pointer}#onetrust-pc-sdk a:hover{color:#3860be}#onetrust-pc-sdk label{margin-bottom:0}#onetrust-pc-sdk #vdr-lst-dsc{font-size:.812em;line-height:1.5;padding:10px 15px 5px 15px}#onetrust-pc-sdk button{max-width:394px;padding:12px 30px;line-height:1;word-break:break-word;word-wrap:break-word;white-space:normal;font-weight:bold;height:auto}#onetrust-pc-sdk .ot-link-btn{padding:0;margin-bottom:0;border:0;font-weight:normal;line-height:normal;width:auto;height:auto}#onetrust-pc-sdk #ot-pc-content{position:absolute;overflow-y:scroll;padding-left:0px;padding-right:30px;top:60px;bottom:110px;margin:1px 3px 0 30px;width:calc(100% - 63px)}#onetrust-pc-sdk .ot-cat-grp .ot-always-active{float:right;clear:none;color:#3860be;margin:0;font-size:.813em;line-height:1.3}#onetrust-pc-sdk .ot-pc-scrollbar::-webkit-scrollbar-track{margin-right:20px}#onetrust-pc-sdk .ot-pc-scrollbar::-webkit-scrollbar{width:11px}#onetrust-pc-sdk .ot-pc-scrollbar::-webkit-scrollbar-thumb{border-radius:10px;background:#d8d8d8}#onetrust-pc-sdk input[type=checkbox]:focus+.ot-acc-hdr{outline:#000 1px solid}#onetrust-pc-sdk .ot-pc-scrollbar{scrollbar-arrow-color:#d8d8d8;scrollbar-darkshadow-color:#d8d8d8;scrollbar-face-color:#d8d8d8;scrollbar-shadow-color:#d8d8d8}#onetrust-pc-sdk .save-preference-btn-handler{margin-right:20px}#onetrust-pc-sdk .ot-pc-refuse-all-handler{margin-right:10px}#onetrust-pc-sdk #ot-pc-desc .privacy-notice-link{margin-left:0}#onetrust-pc-sdk .ot-subgrp-cntr{display:inline-block;clear:both;width:100%;padding-top:15px}#onetrust-pc-sdk .ot-switch+.ot-subgrp-cntr{padding-top:10px}#onetrust-pc-sdk ul.ot-subgrps{margin:0;font-size:initial}#onetrust-pc-sdk ul.ot-subgrps li p,#onetrust-pc-sdk ul.ot-subgrps li h5{font-size:.813em;line-height:1.4;color:dimgray}#onetrust-pc-sdk ul.ot-subgrps .ot-switch{min-height:auto}#onetrust-pc-sdk ul.ot-subgrps .ot-switch-nob{top:0}#onetrust-pc-sdk ul.ot-subgrps .ot-acc-hdr{display:inline-block;width:100%}#onetrust-pc-sdk ul.ot-subgrps .ot-acc-txt{margin:0}#onetrust-pc-sdk ul.ot-subgrps li{padding:0;border:none}#onetrust-pc-sdk ul.ot-subgrps li h5{position:relative;top:5px;font-weight:bold;margin-bottom:0;float:left}#onetrust-pc-sdk li.ot-subgrp{margin-left:20px;overflow:auto}#onetrust-pc-sdk li.ot-subgrp>h5{width:calc(100% - 100px)}#onetrust-pc-sdk .ot-cat-item p>ul,#onetrust-pc-sdk li.ot-subgrp p>ul{margin:0px;list-style:disc;margin-left:15px;font-size:inherit}#onetrust-pc-sdk .ot-cat-item p>ul li,#onetrust-pc-sdk li.ot-subgrp p>ul li{font-size:inherit;padding-top:10px;padding-left:0px;padding-right:0px;border:none}#onetrust-pc-sdk .ot-cat-item p>ul li:last-child,#onetrust-pc-sdk li.ot-subgrp p>ul li:last-child{padding-bottom:10px}#onetrust-pc-sdk .ot-pc-logo{height:40px;width:120px;display:inline-block}#onetrust-pc-sdk .ot-pc-footer{position:absolute;bottom:0px;width:100%;max-height:160px;border-top:1px solid #d8d8d8}#onetrust-pc-sdk.ot-ftr-stacked .ot-pc-refuse-all-handler{margin-bottom:0px}#onetrust-pc-sdk.ot-ftr-stacked #ot-pc-content{bottom:160px}#onetrust-pc-sdk.ot-ftr-stacked .ot-pc-footer button{width:100%;max-width:none}#onetrust-pc-sdk.ot-ftr-stacked .ot-btn-container{margin:0 30px;width:calc(100% - 60px);padding-right:0}#onetrust-pc-sdk .ot-pc-footer-logo{height:30px;width:100%;text-align:right;background:#f4f4f4}#onetrust-pc-sdk .ot-pc-footer-logo a{display:inline-block;margin-top:5px;margin-right:10px}#onetrust-pc-sdk[dir=rtl] .ot-pc-footer-logo{direction:rtl}#onetrust-pc-sdk[dir=rtl] .ot-pc-footer-logo a{margin-right:25px}#onetrust-pc-sdk .ot-tgl{float:right;position:relative;z-index:1}#onetrust-pc-sdk .ot-tgl input:checked+.ot-switch .ot-switch-nob{background-color:#cddcf2;border:1px solid #3860be}#onetrust-pc-sdk .ot-tgl input:checked+.ot-switch .ot-switch-nob:before{-webkit-transform:translateX(20px);-ms-transform:translateX(20px);transform:translateX(20px);background-color:#3860be;border-color:#3860be}#onetrust-pc-sdk .ot-tgl input:focus+.ot-switch{outline:#000 solid 1px}#onetrust-pc-sdk .ot-switch{position:relative;display:inline-block;width:45px;height:25px}#onetrust-pc-sdk .ot-switch-nob{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#f2f1f1;border:1px solid #ddd;transition:all .2s ease-in 0s;-moz-transition:all .2s ease-in 0s;-o-transition:all .2s ease-in 0s;-webkit-transition:all .2s ease-in 0s;border-radius:20px}#onetrust-pc-sdk .ot-switch-nob:before{position:absolute;content:"";height:21px;width:21px;bottom:1px;background-color:#7d7d7d;-webkit-transition:.4s;transition:.4s;border-radius:20px}#onetrust-pc-sdk .ot-chkbox input:checked~label::before{background-color:#3860be}#onetrust-pc-sdk .ot-chkbox input+label::after{content:none;color:#fff}#onetrust-pc-sdk .ot-chkbox input:checked+label::after{content:""}#onetrust-pc-sdk .ot-chkbox input:focus+label::before{outline-style:solid;outline-width:2px;outline-style:auto}#onetrust-pc-sdk .ot-chkbox label{position:relative;display:inline-block;padding-left:30px;cursor:pointer;font-weight:500}#onetrust-pc-sdk .ot-chkbox label::before,#onetrust-pc-sdk .ot-chkbox label::after{position:absolute;content:"";display:inline-block;border-radius:3px}#onetrust-pc-sdk .ot-chkbox label::before{height:18px;width:18px;border:1px solid #3860be;left:0px;top:auto}#onetrust-pc-sdk .ot-chkbox label::after{height:5px;width:9px;border-left:3px solid;border-bottom:3px solid;transform:rotate(-45deg);-o-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-webkit-transform:rotate(-45deg);left:4px;top:5px}#onetrust-pc-sdk .ot-label-txt{display:none}#onetrust-pc-sdk .ot-chkbox input,#onetrust-pc-sdk .ot-tgl input{position:absolute;opacity:0;width:0;height:0}#onetrust-pc-sdk .ot-arw-cntr{float:right;position:relative;pointer-events:none}#onetrust-pc-sdk .ot-arw-cntr .ot-arw{width:16px;height:16px;margin-left:5px;color:dimgray;display:inline-block;vertical-align:middle;-webkit-transition:all 150ms ease-in 0s;-moz-transition:all 150ms ease-in 0s;-o-transition:all 150ms ease-in 0s;transition:all 150ms ease-in 0s}#onetrust-pc-sdk input:checked~.ot-acc-hdr .ot-arw,#onetrust-pc-sdk button[aria-expanded=true]~.ot-acc-hdr .ot-arw-cntr svg{transform:rotate(90deg);-o-transform:rotate(90deg);-ms-transform:rotate(90deg);-webkit-transform:rotate(90deg)}#onetrust-pc-sdk input[type=checkbox]:focus+.ot-acc-hdr{outline:#000 1px solid}#onetrust-pc-sdk .ot-tgl-cntr,#onetrust-pc-sdk .ot-arw-cntr{display:inline-block}#onetrust-pc-sdk .ot-tgl-cntr{width:45px;float:right;margin-top:2px}#onetrust-pc-sdk #ot-lst-cnt .ot-tgl-cntr{margin-top:10px}#onetrust-pc-sdk .ot-always-active-subgroup{width:auto;padding-left:0px!important;top:3px;position:relative}#onetrust-pc-sdk .ot-label-status{padding-left:5px;font-size:.75em;display:none}#onetrust-pc-sdk .ot-arw-cntr{margin-top:-1px}#onetrust-pc-sdk .ot-arw-cntr svg{-webkit-transition:all 300ms ease-in 0s;-moz-transition:all 300ms ease-in 0s;-o-transition:all 300ms ease-in 0s;transition:all 300ms ease-in 0s;height:10px;width:10px}#onetrust-pc-sdk input:checked~.ot-acc-hdr .ot-arw{transform:rotate(90deg);-o-transform:rotate(90deg);-ms-transform:rotate(90deg);-webkit-transform:rotate(90deg)}#onetrust-pc-sdk .ot-arw{width:10px;margin-left:15px;transition:all 300ms ease-in 0s;-webkit-transition:all 300ms ease-in 0s;-moz-transition:all 300ms ease-in 0s;-o-transition:all 300ms ease-in 0s}#onetrust-pc-sdk .ot-vlst-cntr{margin-bottom:0}#onetrust-pc-sdk .ot-hlst-cntr{margin-top:5px;display:inline-block;width:100%}#onetrust-pc-sdk .category-vendors-list-handler,#onetrust-pc-sdk .category-vendors-list-handler+a,#onetrust-pc-sdk .category-host-list-handler{clear:both;color:#3860be;margin-left:0;font-size:.813em;text-decoration:none;float:left;overflow:hidden}#onetrust-pc-sdk .category-vendors-list-handler:hover,#onetrust-pc-sdk .category-vendors-list-handler+a:hover,#onetrust-pc-sdk .category-host-list-handler:hover{color:#1883fd}#onetrust-pc-sdk .category-vendors-list-handler+a{clear:none}#onetrust-pc-sdk .category-vendors-list-handler+a::after{content:"";height:15px;width:15px;background-repeat:no-repeat;margin-left:5px;float:right;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 511.626 511.627'%3E%3Cg fill='%231276CE'%3E%3Cpath d='M392.857 292.354h-18.274c-2.669 0-4.859.855-6.563 2.573-1.718 1.708-2.573 3.897-2.573 6.563v91.361c0 12.563-4.47 23.315-13.415 32.262-8.945 8.945-19.701 13.414-32.264 13.414H82.224c-12.562 0-23.317-4.469-32.264-13.414-8.945-8.946-13.417-19.698-13.417-32.262V155.31c0-12.562 4.471-23.313 13.417-32.259 8.947-8.947 19.702-13.418 32.264-13.418h200.994c2.669 0 4.859-.859 6.57-2.57 1.711-1.713 2.566-3.9 2.566-6.567V82.221c0-2.662-.855-4.853-2.566-6.563-1.711-1.713-3.901-2.568-6.57-2.568H82.224c-22.648 0-42.016 8.042-58.102 24.125C8.042 113.297 0 132.665 0 155.313v237.542c0 22.647 8.042 42.018 24.123 58.095 16.086 16.084 35.454 24.13 58.102 24.13h237.543c22.647 0 42.017-8.046 58.101-24.13 16.085-16.077 24.127-35.447 24.127-58.095v-91.358c0-2.669-.856-4.859-2.574-6.57-1.713-1.718-3.903-2.573-6.565-2.573z'/%3E%3Cpath d='M506.199 41.971c-3.617-3.617-7.905-5.424-12.85-5.424H347.171c-4.948 0-9.233 1.807-12.847 5.424-3.617 3.615-5.428 7.898-5.428 12.847s1.811 9.233 5.428 12.85l50.247 50.248-186.147 186.151c-1.906 1.903-2.856 4.093-2.856 6.563 0 2.479.953 4.668 2.856 6.571l32.548 32.544c1.903 1.903 4.093 2.852 6.567 2.852s4.665-.948 6.567-2.852l186.148-186.148 50.251 50.248c3.614 3.617 7.898 5.426 12.847 5.426s9.233-1.809 12.851-5.426c3.617-3.616 5.424-7.898 5.424-12.847V54.818c-.001-4.952-1.814-9.232-5.428-12.847z'/%3E%3C/g%3E%3C/svg%3E")}#onetrust-pc-sdk .back-btn-handler{font-size:1em;text-decoration:none}#onetrust-pc-sdk .back-btn-handler:hover{opacity:.6}#onetrust-pc-sdk #ot-lst-title h3{display:inline-block;word-break:break-word;word-wrap:break-word;margin-bottom:0;color:#656565;font-size:1em;font-weight:bold;margin-left:15px}#onetrust-pc-sdk #ot-lst-title{margin:10px 0 10px 0px;font-size:1em;text-align:left}#onetrust-pc-sdk #ot-pc-hdr{margin:0 0 0 30px;height:auto;width:auto}#onetrust-pc-sdk #ot-pc-hdr input::placeholder{color:#d4d4d4;font-style:italic}#onetrust-pc-sdk #vendor-search-handler{height:31px;width:100%;border-radius:50px;font-size:.8em;padding-right:35px;padding-left:15px;float:left;margin-left:15px}#onetrust-pc-sdk .ot-ven-name{display:block;width:auto;padding-right:5px}#onetrust-pc-sdk #ot-lst-cnt{overflow-y:auto;margin-left:20px;margin-right:7px;width:calc(100% - 27px);max-height:calc(100% - 80px);height:100%;transform:translate3d(0,0,0)}#onetrust-pc-sdk #ot-pc-lst{width:100%;bottom:100px;position:absolute;top:60px}#onetrust-pc-sdk #ot-pc-lst:not(.ot-enbl-chr) .ot-tgl-cntr .ot-arw-cntr,#onetrust-pc-sdk #ot-pc-lst:not(.ot-enbl-chr) .ot-tgl-cntr .ot-arw-cntr *{visibility:hidden}#onetrust-pc-sdk #ot-pc-lst .ot-tgl-cntr{right:12px;position:absolute}#onetrust-pc-sdk #ot-pc-lst .ot-arw-cntr{float:right;position:relative}#onetrust-pc-sdk #ot-pc-lst .ot-arw{margin-left:10px}#onetrust-pc-sdk #ot-pc-lst .ot-acc-hdr{overflow:hidden;cursor:pointer}#onetrust-pc-sdk .ot-vlst-cntr{overflow:hidden}#onetrust-pc-sdk #ot-sel-blk{overflow:hidden;width:100%;position:sticky;position:-webkit-sticky;top:0;z-index:3}#onetrust-pc-sdk #ot-back-arw{height:12px;width:12px}#onetrust-pc-sdk .ot-lst-subhdr{width:100%;display:inline-block}#onetrust-pc-sdk .ot-search-cntr{float:left;width:78%;position:relative}#onetrust-pc-sdk .ot-search-cntr>svg{width:30px;height:30px;position:absolute;float:left;right:-15px}#onetrust-pc-sdk .ot-fltr-cntr{float:right;right:50px;position:relative}#onetrust-pc-sdk #filter-btn-handler{background-color:#3860be;border-radius:17px;display:inline-block;position:relative;width:32px;height:32px;-moz-transition:.1s ease;-o-transition:.1s ease;-webkit-transition:1s ease;transition:.1s ease;padding:0;margin:0}#onetrust-pc-sdk #filter-btn-handler:hover{background-color:#3860be}#onetrust-pc-sdk #filter-btn-handler svg{width:12px;height:12px;margin:3px 10px 0 10px;display:block;position:static;right:auto;top:auto}#onetrust-pc-sdk .ot-ven-link{color:#3860be;text-decoration:none;font-weight:100;display:inline-block;padding-top:10px;transform:translate(0,1%);-o-transform:translate(0,1%);-ms-transform:translate(0,1%);-webkit-transform:translate(0,1%);position:relative;z-index:2}#onetrust-pc-sdk .ot-ven-link *{font-size:inherit}#onetrust-pc-sdk .ot-ven-link:hover{text-decoration:underline}#onetrust-pc-sdk .ot-ven-hdr{width:calc(100% - 160px);height:auto;float:left;word-break:break-word;word-wrap:break-word;vertical-align:middle;padding-bottom:3px}#onetrust-pc-sdk .ot-ven-link{letter-spacing:.03em;font-size:.75em;font-weight:400}#onetrust-pc-sdk .ot-ven-dets{border-radius:2px;background-color:#f8f8f8}#onetrust-pc-sdk .ot-ven-dets li:first-child p:first-child{border-top:none}#onetrust-pc-sdk .ot-ven-dets .ot-ven-disc:not(:first-child){border-top:1px solid #e9e9e9}#onetrust-pc-sdk .ot-ven-dets .ot-ven-disc:nth-child(n+3) p{display:inline-block}#onetrust-pc-sdk .ot-ven-dets .ot-ven-disc:nth-child(n+3) p:nth-of-type(odd){width:30%}#onetrust-pc-sdk .ot-ven-dets .ot-ven-disc:nth-child(n+3) p:nth-of-type(even){width:50%;word-break:break-word;word-wrap:break-word}#onetrust-pc-sdk .ot-ven-dets .ot-ven-disc p,#onetrust-pc-sdk .ot-ven-dets .ot-ven-disc h4{padding-top:5px;padding-bottom:5px;display:block}#onetrust-pc-sdk .ot-ven-dets .ot-ven-disc h4{display:inline-block}#onetrust-pc-sdk .ot-ven-dets p,#onetrust-pc-sdk .ot-ven-dets h4,#onetrust-pc-sdk .ot-ven-dets span{font-size:.69em;text-align:left;vertical-align:middle;word-break:break-word;word-wrap:break-word;margin:0;padding-bottom:10px;padding-left:15px;color:#2e3644}#onetrust-pc-sdk .ot-ven-dets h4{padding-top:5px}#onetrust-pc-sdk .ot-ven-dets span{color:dimgray;padding:0;vertical-align:baseline}#onetrust-pc-sdk .ot-ven-dets .ot-ven-pur h4{border-top:1px solid #e9e9e9;border-bottom:1px solid #e9e9e9;padding-bottom:5px;margin-bottom:5px;font-weight:bold}#onetrust-pc-sdk #ot-host-lst .ot-sel-all{float:right;position:relative;margin-right:42px;top:10px}#onetrust-pc-sdk #ot-host-lst .ot-sel-all input[type=checkbox]{width:auto;height:auto}#onetrust-pc-sdk #ot-host-lst .ot-sel-all label{height:20px;width:20px;padding-left:0px}#onetrust-pc-sdk #ot-host-lst .ot-acc-txt{overflow:hidden;width:95%}#onetrust-pc-sdk .ot-host-hdr{position:relative;z-index:1;pointer-events:none;width:calc(100% - 125px);float:left}#onetrust-pc-sdk .ot-host-name,#onetrust-pc-sdk .ot-host-desc{display:inline-block;width:90%}#onetrust-pc-sdk .ot-host-name{pointer-events:none}#onetrust-pc-sdk .ot-host-hdr>a{text-decoration:underline;font-size:.82em;position:relative;z-index:2;float:left;margin-bottom:5px;pointer-events:initial}#onetrust-pc-sdk .ot-host-name+a{margin-top:5px}#onetrust-pc-sdk .ot-host-name,#onetrust-pc-sdk .ot-host-name a,#onetrust-pc-sdk .ot-host-desc,#onetrust-pc-sdk .ot-host-info{color:dimgray;word-break:break-word;word-wrap:break-word}#onetrust-pc-sdk .ot-host-name,#onetrust-pc-sdk .ot-host-name a{font-weight:bold;font-size:.82em;line-height:1.3}#onetrust-pc-sdk .ot-host-name a{font-size:1em}#onetrust-pc-sdk .ot-host-expand{margin-top:3px;margin-bottom:3px;clear:both;display:block;color:#3860be;font-size:.72em;font-weight:normal}#onetrust-pc-sdk .ot-host-expand *{font-size:inherit}#onetrust-pc-sdk .ot-host-desc,#onetrust-pc-sdk .ot-host-info{font-size:.688em;line-height:1.4;font-weight:normal}#onetrust-pc-sdk .ot-host-desc{margin-top:10px}#onetrust-pc-sdk .ot-host-opt{margin:0;font-size:inherit;display:inline-block;width:100%}#onetrust-pc-sdk .ot-host-opt li>div div{font-size:.8em;padding:5px 0}#onetrust-pc-sdk .ot-host-opt li>div div:nth-child(1){width:30%;float:left}#onetrust-pc-sdk .ot-host-opt li>div div:nth-child(2){width:70%;float:left;word-break:break-word;word-wrap:break-word}#onetrust-pc-sdk .ot-host-info{border:none;display:inline-block;width:calc(100% - 10px);padding:10px;margin-bottom:10px;background-color:#f8f8f8}#onetrust-pc-sdk .ot-host-info>div{overflow:auto}#onetrust-pc-sdk #no-results{text-align:center;margin-top:30px}#onetrust-pc-sdk #no-results p{font-size:1em;color:#2e3644;word-break:break-word;word-wrap:break-word}#onetrust-pc-sdk #no-results p span{font-weight:bold}#onetrust-pc-sdk #ot-fltr-modal{width:100%;height:auto;display:none;-moz-transition:.2s ease;-o-transition:.2s ease;-webkit-transition:2s ease;transition:.2s ease;overflow:hidden;opacity:1;right:0}#onetrust-pc-sdk #ot-fltr-modal .ot-label-txt{display:inline-block;font-size:.85em;color:dimgray}#onetrust-pc-sdk #ot-fltr-cnt{z-index:2147483646;background-color:#fff;position:absolute;height:90%;max-height:300px;width:325px;left:210px;margin-top:10px;margin-bottom:20px;padding-right:10px;border-radius:3px;-webkit-box-shadow:0px 0px 12px 2px #c7c5c7;-moz-box-shadow:0px 0px 12px 2px #c7c5c7;box-shadow:0px 0px 12px 2px #c7c5c7}#onetrust-pc-sdk .ot-fltr-scrlcnt{overflow-y:auto;overflow-x:hidden;clear:both;max-height:calc(100% - 60px)}#onetrust-pc-sdk #ot-anchor{border:12px solid transparent;display:none;position:absolute;z-index:2147483647;right:55px;top:75px;transform:rotate(45deg);-o-transform:rotate(45deg);-ms-transform:rotate(45deg);-webkit-transform:rotate(45deg);background-color:#fff;-webkit-box-shadow:-3px -3px 5px -2px #c7c5c7;-moz-box-shadow:-3px -3px 5px -2px #c7c5c7;box-shadow:-3px -3px 5px -2px #c7c5c7}#onetrust-pc-sdk .ot-fltr-btns{margin-left:15px}#onetrust-pc-sdk #filter-apply-handler{margin-right:15px}#onetrust-pc-sdk .ot-fltr-opt{margin-bottom:25px;margin-left:15px;width:75%;position:relative}#onetrust-pc-sdk .ot-fltr-opt p{display:inline-block;margin:0;font-size:.9em;color:#2e3644}#onetrust-pc-sdk .ot-chkbox label span{font-size:.85em;color:dimgray}#onetrust-pc-sdk .ot-chkbox input[type=checkbox]+label::after{content:none;color:#fff}#onetrust-pc-sdk .ot-chkbox input[type=checkbox]:checked+label::after{content:""}#onetrust-pc-sdk .ot-chkbox input[type=checkbox]:focus+label::before{outline-style:solid;outline-width:2px;outline-style:auto}#onetrust-pc-sdk #ot-selall-vencntr,#onetrust-pc-sdk #ot-selall-adtlvencntr,#onetrust-pc-sdk #ot-selall-hostcntr,#onetrust-pc-sdk #ot-selall-licntr,#onetrust-pc-sdk #ot-selall-gnvencntr{right:15px;position:relative;width:20px;height:20px;float:right}#onetrust-pc-sdk #ot-selall-vencntr label,#onetrust-pc-sdk #ot-selall-adtlvencntr label,#onetrust-pc-sdk #ot-selall-hostcntr label,#onetrust-pc-sdk #ot-selall-licntr label,#onetrust-pc-sdk #ot-selall-gnvencntr label{float:left;padding-left:0}#onetrust-pc-sdk #ot-ven-lst:first-child{border-top:1px solid #e2e2e2}#onetrust-pc-sdk ul{list-style:none;padding:0}#onetrust-pc-sdk ul li{position:relative;margin:0;padding:15px 15px 15px 10px;border-bottom:1px solid #e2e2e2}#onetrust-pc-sdk ul li h3{font-size:.75em;color:#656565;margin:0;display:inline-block;width:70%;height:auto;word-break:break-word;word-wrap:break-word}#onetrust-pc-sdk ul li p{margin:0;font-size:.7em}#onetrust-pc-sdk ul li input[type=checkbox]{position:absolute;cursor:pointer;width:100%;height:100%;opacity:0;margin:0;top:0;left:0}#onetrust-pc-sdk .ot-cat-item>button:focus,#onetrust-pc-sdk .ot-acc-cntr>button:focus,#onetrust-pc-sdk li>button:focus{outline:#000 solid 2px}#onetrust-pc-sdk .ot-cat-item>button,#onetrust-pc-sdk .ot-acc-cntr>button,#onetrust-pc-sdk li>button{position:absolute;cursor:pointer;width:100%;height:100%;margin:0;top:0;left:0;z-index:1;max-width:none;border:none}#onetrust-pc-sdk .ot-cat-item>button[aria-expanded=false]~.ot-acc-txt,#onetrust-pc-sdk .ot-acc-cntr>button[aria-expanded=false]~.ot-acc-txt,#onetrust-pc-sdk li>button[aria-expanded=false]~.ot-acc-txt{margin-top:0;max-height:0;opacity:0;overflow:hidden;width:100%;transition:.25s ease-out;display:none}#onetrust-pc-sdk .ot-cat-item>button[aria-expanded=true]~.ot-acc-txt,#onetrust-pc-sdk .ot-acc-cntr>button[aria-expanded=true]~.ot-acc-txt,#onetrust-pc-sdk li>button[aria-expanded=true]~.ot-acc-txt{transition:.1s ease-in;margin-top:10px;width:100%;overflow:auto;display:block}#onetrust-pc-sdk .ot-cat-item>button[aria-expanded=true]~.ot-acc-grpcntr,#onetrust-pc-sdk .ot-acc-cntr>button[aria-expanded=true]~.ot-acc-grpcntr,#onetrust-pc-sdk li>button[aria-expanded=true]~.ot-acc-grpcntr{width:auto;margin-top:0px;padding-bottom:10px}#onetrust-pc-sdk .ot-host-item>button:focus,#onetrust-pc-sdk .ot-ven-item>button:focus{outline:0;border:2px solid #000}#onetrust-pc-sdk .ot-hide-acc>button{pointer-events:none}#onetrust-pc-sdk .ot-hide-acc .ot-plus-minus>*,#onetrust-pc-sdk .ot-hide-acc .ot-arw-cntr>*{visibility:hidden}#onetrust-pc-sdk .ot-hide-acc .ot-acc-hdr{min-height:30px}#onetrust-pc-sdk.ot-addtl-vendors #ot-lst-cnt:not(.ot-host-cnt){padding-right:10px;width:calc(100% - 37px);margin-top:10px;max-height:calc(100% - 90px)}#onetrust-pc-sdk.ot-addtl-vendors #ot-lst-cnt:not(.ot-host-cnt) #ot-sel-blk{background-color:#f9f9fc;border:1px solid #e2e2e2;width:calc(100% - 2px);padding-bottom:5px;padding-top:5px}#onetrust-pc-sdk.ot-addtl-vendors #ot-lst-cnt:not(.ot-host-cnt) .ot-sel-all{padding-right:34px}#onetrust-pc-sdk.ot-addtl-vendors #ot-lst-cnt:not(.ot-host-cnt) .ot-sel-all-chkbox{width:auto}#onetrust-pc-sdk.ot-addtl-vendors #ot-lst-cnt:not(.ot-host-cnt) ul li{border:1px solid #e2e2e2;margin-bottom:10px}#onetrust-pc-sdk.ot-addtl-vendors #ot-lst-cnt:not(.ot-host-cnt) .ot-acc-cntr>.ot-acc-hdr{padding:10px 0 10px 15px}#onetrust-pc-sdk.ot-addtl-vendors .ot-sel-all-chkbox{float:right}#onetrust-pc-sdk.ot-addtl-vendors .ot-plus-minus~.ot-sel-all-chkbox{right:34px}#onetrust-pc-sdk.ot-addtl-vendors #ot-ven-lst:first-child{border-top:none}#onetrust-pc-sdk .ot-acc-cntr{position:relative;border-left:1px solid #e2e2e2;border-right:1px solid #e2e2e2;border-bottom:1px solid #e2e2e2}#onetrust-pc-sdk .ot-acc-cntr input{z-index:1}#onetrust-pc-sdk .ot-acc-cntr>.ot-acc-hdr{background-color:#f9f9fc;padding:5px 0 5px 15px;width:auto}#onetrust-pc-sdk .ot-acc-cntr>.ot-acc-hdr .ot-plus-minus{vertical-align:middle;top:auto}#onetrust-pc-sdk .ot-acc-cntr>.ot-acc-hdr .ot-arw-cntr{right:10px}#onetrust-pc-sdk .ot-acc-cntr>.ot-acc-hdr input{z-index:2}#onetrust-pc-sdk .ot-acc-cntr>input[type=checkbox]:checked~.ot-acc-hdr{border-bottom:1px solid #e2e2e2}#onetrust-pc-sdk .ot-acc-cntr>.ot-acc-txt{padding-left:10px;padding-right:10px}#onetrust-pc-sdk .ot-acc-cntr button[aria-expanded=true]~.ot-acc-txt{width:auto}#onetrust-pc-sdk .ot-acc-cntr .ot-addtl-venbox{display:none}#onetrust-pc-sdk .ot-vlst-cntr{margin-bottom:0;width:100%}#onetrust-pc-sdk .ot-vensec-title{font-size:.813em;vertical-align:middle;display:inline-block}#onetrust-pc-sdk .category-vendors-list-handler,#onetrust-pc-sdk .category-vendors-list-handler+a{margin-left:0;margin-top:10px}#onetrust-pc-sdk #ot-selall-vencntr.line-through label::after,#onetrust-pc-sdk #ot-selall-adtlvencntr.line-through label::after,#onetrust-pc-sdk #ot-selall-licntr.line-through label::after,#onetrust-pc-sdk #ot-selall-hostcntr.line-through label::after,#onetrust-pc-sdk #ot-selall-gnvencntr.line-through label::after{height:auto;border-left:0;transform:none;-o-transform:none;-ms-transform:none;-webkit-transform:none;left:5px;top:9px}#onetrust-pc-sdk #ot-category-title{float:left;padding-bottom:10px;font-size:1em;width:100%}#onetrust-pc-sdk .ot-cat-grp{margin-top:10px}#onetrust-pc-sdk .ot-cat-item{line-height:1.1;margin-top:10px;display:inline-block;width:100%}#onetrust-pc-sdk .ot-btn-container{text-align:right}#onetrust-pc-sdk .ot-btn-container button{display:inline-block;font-size:.75em;letter-spacing:.08em;margin-top:19px}#onetrust-pc-sdk #close-pc-btn-handler.ot-close-icon{position:absolute;top:10px;right:0;z-index:1;padding:0;background-color:transparent;border:none}#onetrust-pc-sdk #close-pc-btn-handler.ot-close-icon:hover{opacity:.7}#onetrust-pc-sdk #close-pc-btn-handler.ot-close-icon svg{display:block;height:10px;width:10px}#onetrust-pc-sdk #clear-filters-handler{margin-top:20px;margin-bottom:10px;float:right;max-width:200px;text-decoration:none;color:#3860be;font-size:.9em;font-weight:bold;background-color:transparent;border-color:transparent;padding:1px}#onetrust-pc-sdk #clear-filters-handler:hover{color:#2285f7}#onetrust-pc-sdk #clear-filters-handler:focus{outline:#000 solid 1px}#onetrust-pc-sdk .ot-accordion-layout.ot-cat-item{position:relative;border-radius:2px;margin:0;padding:0;border:1px solid #d8d8d8;border-top:none;width:calc(100% - 2px);float:left}#onetrust-pc-sdk .ot-accordion-layout.ot-cat-item:first-of-type{margin-top:10px;border-top:1px solid #d8d8d8}#onetrust-pc-sdk .ot-accordion-layout .ot-acc-grpdesc{padding-left:20px;padding-right:20px;width:calc(100% - 40px);font-size:.812em;margin-bottom:10px;margin-top:15px}#onetrust-pc-sdk .ot-accordion-layout .ot-acc-grpdesc>ul{padding-top:10px}#onetrust-pc-sdk .ot-accordion-layout .ot-acc-grpdesc>ul li{padding-top:0;line-height:1.5;padding-bottom:10px}#onetrust-pc-sdk .ot-accordion-layout div+.ot-acc-grpdesc{margin-top:5px}#onetrust-pc-sdk .ot-accordion-layout .ot-vlst-cntr:first-child{margin-top:10px}#onetrust-pc-sdk .ot-accordion-layout .ot-vlst-cntr:last-child,#onetrust-pc-sdk .ot-accordion-layout .ot-hlst-cntr:last-child{margin-bottom:5px}#onetrust-pc-sdk .ot-accordion-layout .ot-acc-hdr{padding-top:11.5px;padding-bottom:11.5px;padding-left:20px;padding-right:20px;width:calc(100% - 40px);display:inline-block}#onetrust-pc-sdk .ot-accordion-layout .ot-acc-txt{width:100%;padding:0px}#onetrust-pc-sdk .ot-accordion-layout .ot-subgrp-cntr{padding-left:20px;padding-right:15px;padding-bottom:0;width:calc(100% - 35px)}#onetrust-pc-sdk .ot-accordion-layout .ot-subgrp{padding-right:5px}#onetrust-pc-sdk .ot-accordion-layout .ot-acc-grpcntr{z-index:1;position:relative}#onetrust-pc-sdk .ot-accordion-layout .ot-cat-header+.ot-arw-cntr{position:absolute;top:50%;transform:translateY(-50%);right:20px;margin-top:-2px}#onetrust-pc-sdk .ot-accordion-layout .ot-cat-header+.ot-arw-cntr .ot-arw{width:15px;height:20px;margin-left:5px;color:dimgray}#onetrust-pc-sdk .ot-accordion-layout .ot-cat-header{float:none;color:#2e3644;margin:0;display:inline-block;height:auto;word-wrap:break-word;min-height:inherit}#onetrust-pc-sdk .ot-accordion-layout .ot-vlst-cntr,#onetrust-pc-sdk .ot-accordion-layout .ot-hlst-cntr{padding-left:20px;width:calc(100% - 20px);display:inline-block;margin-top:0px;padding-bottom:2px}#onetrust-pc-sdk .ot-accordion-layout .ot-acc-hdr{position:relative;min-height:25px}#onetrust-pc-sdk .ot-accordion-layout h4~.ot-tgl,#onetrust-pc-sdk .ot-accordion-layout h4~.ot-always-active{position:absolute;top:50%;transform:translateY(-50%);right:20px}#onetrust-pc-sdk .ot-accordion-layout h4~.ot-tgl+.ot-tgl{right:95px}#onetrust-pc-sdk .ot-accordion-layout .category-vendors-list-handler,#onetrust-pc-sdk .ot-accordion-layout .category-vendors-list-handler+a{margin-top:5px}#onetrust-pc-sdk .ot-enbl-chr h4~.ot-tgl,#onetrust-pc-sdk .ot-enbl-chr h4~.ot-always-active{right:45px}#onetrust-pc-sdk .ot-enbl-chr h4~.ot-tgl+.ot-tgl{right:120px}#onetrust-pc-sdk .ot-enbl-chr .ot-pli-hdr.ot-leg-border-color span:first-child{width:90px}#onetrust-pc-sdk .ot-enbl-chr li.ot-subgrp>h5+.ot-tgl-cntr{padding-right:25px}#onetrust-pc-sdk .ot-plus-minus{width:20px;height:20px;font-size:1.5em;position:relative;display:inline-block;margin-right:5px;top:3px}#onetrust-pc-sdk .ot-plus-minus span{position:absolute;background:#27455c;border-radius:1px}#onetrust-pc-sdk .ot-plus-minus span:first-of-type{top:25%;bottom:25%;width:10%;left:45%}#onetrust-pc-sdk .ot-plus-minus span:last-of-type{left:25%;right:25%;height:10%;top:45%}#onetrust-pc-sdk button[aria-expanded=true]~.ot-acc-hdr .ot-arw,#onetrust-pc-sdk button[aria-expanded=true]~.ot-acc-hdr .ot-plus-minus span:first-of-type,#onetrust-pc-sdk button[aria-expanded=true]~.ot-acc-hdr .ot-plus-minus span:last-of-type{transform:rotate(90deg)}#onetrust-pc-sdk button[aria-expanded=true]~.ot-acc-hdr .ot-plus-minus span:last-of-type{left:50%;right:50%}#onetrust-pc-sdk #ot-selall-vencntr label,#onetrust-pc-sdk #ot-selall-adtlvencntr label,#onetrust-pc-sdk #ot-selall-hostcntr label,#onetrust-pc-sdk #ot-selall-licntr label{position:relative;display:inline-block;width:20px;height:20px}#onetrust-pc-sdk .ot-host-item .ot-plus-minus,#onetrust-pc-sdk .ot-ven-item .ot-plus-minus{float:left;margin-right:8px;top:10px}#onetrust-pc-sdk .ot-ven-item ul{list-style:none inside;font-size:100%;margin:0}#onetrust-pc-sdk .ot-ven-item ul li{margin:0!important;padding:0;border:none!important}#onetrust-pc-sdk .ot-pli-hdr{color:#77808e;overflow:hidden;padding-top:7.5px;padding-bottom:7.5px;width:calc(100% - 2px);border-top-left-radius:3px;border-top-right-radius:3px}#onetrust-pc-sdk .ot-pli-hdr span:first-child{top:50%;transform:translateY(50%);max-width:90px}#onetrust-pc-sdk .ot-pli-hdr span:last-child{padding-right:10px;max-width:95px;text-align:center}#onetrust-pc-sdk .ot-li-title{float:right;font-size:.813em}#onetrust-pc-sdk .ot-pli-hdr.ot-leg-border-color{background-color:#f4f4f4;border:1px solid #d8d8d8}#onetrust-pc-sdk .ot-pli-hdr.ot-leg-border-color span:first-child{text-align:left;width:70px}#onetrust-pc-sdk li.ot-subgrp>h5,#onetrust-pc-sdk .ot-cat-header{width:calc(100% - 130px)}#onetrust-pc-sdk li.ot-subgrp>h5+.ot-tgl-cntr{padding-left:13px}#onetrust-pc-sdk .ot-acc-grpcntr .ot-acc-grpdesc{margin-bottom:5px}#onetrust-pc-sdk .ot-acc-grpcntr .ot-subgrp-cntr{border-top:1px solid #d8d8d8}#onetrust-pc-sdk .ot-acc-grpcntr .ot-vlst-cntr+.ot-subgrp-cntr{border-top:none}#onetrust-pc-sdk .ot-acc-hdr .ot-arw-cntr+.ot-tgl-cntr,#onetrust-pc-sdk .ot-acc-txt h4+.ot-tgl-cntr{padding-left:13px}#onetrust-pc-sdk .ot-pli-hdr~.ot-cat-item .ot-subgrp>h5,#onetrust-pc-sdk .ot-pli-hdr~.ot-cat-item .ot-cat-header{width:calc(100% - 145px)}#onetrust-pc-sdk .ot-pli-hdr~.ot-cat-item h5+.ot-tgl-cntr,#onetrust-pc-sdk .ot-pli-hdr~.ot-cat-item .ot-cat-header+.ot-tgl{padding-left:28px}#onetrust-pc-sdk .ot-sel-all-hdr,#onetrust-pc-sdk .ot-sel-all-chkbox{display:inline-block;width:100%;position:relative}#onetrust-pc-sdk .ot-sel-all-chkbox{z-index:1}#onetrust-pc-sdk .ot-sel-all{margin:0;position:relative;padding-right:23px;float:right}#onetrust-pc-sdk .ot-consent-hdr,#onetrust-pc-sdk .ot-li-hdr{float:right;font-size:.812em;line-height:normal;text-align:center;word-break:break-word;word-wrap:break-word}#onetrust-pc-sdk .ot-li-hdr{max-width:100px;padding-right:10px}#onetrust-pc-sdk .ot-consent-hdr{max-width:55px}#onetrust-pc-sdk #ot-selall-licntr{display:block;width:21px;height:auto;float:right;position:relative;right:80px}#onetrust-pc-sdk #ot-selall-licntr label{position:absolute}#onetrust-pc-sdk .ot-ven-ctgl{margin-left:66px}#onetrust-pc-sdk .ot-ven-litgl+.ot-arw-cntr{margin-left:81px}#onetrust-pc-sdk .ot-enbl-chr .ot-host-cnt .ot-tgl-cntr{width:auto}#onetrust-pc-sdk #ot-lst-cnt:not(.ot-host-cnt) .ot-tgl-cntr{width:auto;top:auto;height:20px}#onetrust-pc-sdk #ot-lst-cnt .ot-chkbox{position:relative;display:inline-block;width:20px;height:20px}#onetrust-pc-sdk #ot-lst-cnt .ot-chkbox label{position:absolute;padding:0;width:20px;height:20px}#onetrust-pc-sdk .ot-acc-grpdesc+.ot-leg-btn-container{padding-left:20px;padding-right:20px;width:calc(100% - 40px);margin-bottom:5px}#onetrust-pc-sdk .ot-subgrp .ot-leg-btn-container{margin-bottom:5px}#onetrust-pc-sdk #ot-ven-lst .ot-leg-btn-container{margin-top:10px}#onetrust-pc-sdk .ot-leg-btn-container{display:inline-block;width:100%;margin-bottom:10px}#onetrust-pc-sdk .ot-leg-btn-container button{height:auto;padding:6.5px 8px;margin-bottom:0;letter-spacing:0;font-size:.75em;line-height:normal}#onetrust-pc-sdk .ot-leg-btn-container svg{display:none;height:14px;width:14px;padding-right:5px;vertical-align:sub}#onetrust-pc-sdk .ot-active-leg-btn{cursor:default;pointer-events:none}#onetrust-pc-sdk .ot-active-leg-btn svg{display:inline-block}#onetrust-pc-sdk .ot-remove-objection-handler{text-decoration:underline;padding:0;font-size:.75em;font-weight:600;line-height:1;padding-left:10px}#onetrust-pc-sdk .ot-obj-leg-btn-handler span{font-weight:bold;text-align:center;font-size:inherit;line-height:1.5}#onetrust-pc-sdk.ot-close-btn-link #close-pc-btn-handler{border:none;height:auto;line-height:1.5;text-decoration:underline;font-size:.69em;background:none;right:15px;top:15px;width:auto;font-weight:normal}#onetrust-pc-sdk[dir=rtl] #ot-back-arw,#onetrust-pc-sdk[dir=rtl] input~.ot-acc-hdr .ot-arw{transform:rotate(180deg);-o-transform:rotate(180deg);-ms-transform:rotate(180deg);-webkit-transform:rotate(180deg)}#onetrust-pc-sdk[dir=rtl] input:checked~.ot-acc-hdr .ot-arw{transform:rotate(270deg);-o-transform:rotate(270deg);-ms-transform:rotate(270deg);-webkit-transform:rotate(270deg)}#onetrust-pc-sdk[dir=rtl] .ot-chkbox label::after{transform:rotate(45deg);-webkit-transform:rotate(45deg);-o-transform:rotate(45deg);-ms-transform:rotate(45deg);border-left:0;border-right:3px solid}#onetrust-pc-sdk[dir=rtl] .ot-search-cntr>svg{right:0}@media only screen and (max-width:600px){#onetrust-pc-sdk.otPcCenter{left:0;min-width:100%;height:100%;top:0;border-radius:0}#onetrust-pc-sdk #ot-pc-content,#onetrust-pc-sdk.ot-ftr-stacked .ot-btn-container{margin:1px 3px 0 10px;padding-right:10px;width:calc(100% - 23px)}#onetrust-pc-sdk .ot-btn-container button{max-width:none;letter-spacing:.01em}#onetrust-pc-sdk #close-pc-btn-handler{top:10px;right:17px}#onetrust-pc-sdk p{font-size:.7em}#onetrust-pc-sdk #ot-pc-hdr{margin:10px 10px 0 5px;width:calc(100% - 15px)}#onetrust-pc-sdk .vendor-search-handler{font-size:1em}#onetrust-pc-sdk #ot-back-arw{margin-left:12px}#onetrust-pc-sdk #ot-lst-cnt{margin:0;padding:0 5px 0 10px;min-width:95%}#onetrust-pc-sdk .switch+p{max-width:80%}#onetrust-pc-sdk .ot-ftr-stacked button{width:100%}#onetrust-pc-sdk #ot-fltr-cnt{max-width:320px;width:90%;border-top-right-radius:0;border-bottom-right-radius:0;margin:0;margin-left:15px;left:auto;right:40px;top:85px}#onetrust-pc-sdk .ot-fltr-opt{margin-left:25px;margin-bottom:10px}#onetrust-pc-sdk .ot-pc-refuse-all-handler{margin-bottom:0}#onetrust-pc-sdk #ot-fltr-cnt{right:40px}}@media only screen and (max-width:476px){#onetrust-pc-sdk .ot-fltr-cntr,#onetrust-pc-sdk #ot-fltr-cnt{right:10px}#onetrust-pc-sdk #ot-anchor{right:25px}#onetrust-pc-sdk button{width:100%}#onetrust-pc-sdk:not(.ot-addtl-vendors) #ot-pc-lst:not(.ot-enbl-chr) .ot-sel-all{padding-right:9px}#onetrust-pc-sdk:not(.ot-addtl-vendors) #ot-pc-lst:not(.ot-enbl-chr) .ot-tgl-cntr{right:0}}@media only screen and (max-width:896px)and (max-height:425px)and (orientation:landscape){#onetrust-pc-sdk.otPcCenter{left:0;top:0;min-width:100%;height:100%;border-radius:0}#onetrust-pc-sdk #ot-anchor{left:initial;right:50px}#onetrust-pc-sdk #ot-lst-title{margin-top:12px}#onetrust-pc-sdk #ot-lst-title *{font-size:inherit}#onetrust-pc-sdk #ot-pc-hdr input{margin-right:0;padding-right:45px}#onetrust-pc-sdk .switch+p{max-width:85%}#onetrust-pc-sdk #ot-sel-blk{position:static}#onetrust-pc-sdk #ot-pc-lst{overflow:auto}#onetrust-pc-sdk #ot-lst-cnt{max-height:none;overflow:initial}#onetrust-pc-sdk #ot-lst-cnt.no-results{height:auto}#onetrust-pc-sdk input{font-size:1em!important}#onetrust-pc-sdk p{font-size:.6em}#onetrust-pc-sdk #ot-fltr-modal{width:100%;top:0}#onetrust-pc-sdk ul li p,#onetrust-pc-sdk .category-vendors-list-handler,#onetrust-pc-sdk .category-vendors-list-handler+a,#onetrust-pc-sdk .category-host-list-handler{font-size:.6em}#onetrust-pc-sdk.ot-shw-fltr #ot-anchor{display:none!important}#onetrust-pc-sdk.ot-shw-fltr #ot-pc-lst{height:100%!important;overflow:hidden;top:0px}#onetrust-pc-sdk.ot-shw-fltr #ot-fltr-cnt{margin:0;height:100%;max-height:none;padding:10px;top:0;width:calc(100% - 20px);position:absolute;right:0;left:0;max-width:none}#onetrust-pc-sdk.ot-shw-fltr .ot-fltr-scrlcnt{max-height:calc(100% - 65px)}}#onetrust-consent-sdk #onetrust-pc-sdk,#onetrust-consent-sdk #ot-search-cntr,#onetrust-consent-sdk #onetrust-pc-sdk .ot-switch.ot-toggle,#onetrust-consent-sdk #onetrust-pc-sdk ot-grp-hdr1 .checkbox,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-title:after,#onetrust-consent-sdk #onetrust-pc-sdk #ot-sel-blk,#onetrust-consent-sdk #onetrust-pc-sdk #ot-fltr-cnt,#onetrust-consent-sdk #onetrust-pc-sdk #ot-anchor{background-color:#FFFFFF}#onetrust-consent-sdk #onetrust-pc-sdk h3,#onetrust-consent-sdk #onetrust-pc-sdk h4,#onetrust-consent-sdk #onetrust-pc-sdk h5,#onetrust-consent-sdk #onetrust-pc-sdk h6,#onetrust-consent-sdk #onetrust-pc-sdk p,#onetrust-consent-sdk #onetrust-pc-sdk #ot-ven-lst .ot-ven-opts p,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-desc,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-title,#onetrust-consent-sdk #onetrust-pc-sdk .ot-li-title,#onetrust-consent-sdk #onetrust-pc-sdk .ot-sel-all-hdr span,#onetrust-consent-sdk #onetrust-pc-sdk #ot-host-lst .ot-host-info,#onetrust-consent-sdk #onetrust-pc-sdk #ot-fltr-modal #modal-header,#onetrust-consent-sdk #onetrust-pc-sdk .ot-checkbox label span,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-lst #ot-sel-blk p,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-lst #ot-lst-title h3,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-lst .back-btn-handler p,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-lst .ot-ven-name,#onetrust-consent-sdk #onetrust-pc-sdk #ot-pc-lst #ot-ven-lst .consent-category,#onetrust-consent-sdk #onetrust-pc-sdk .ot-leg-btn-container .ot-inactive-leg-btn,#onetrust-consent-sdk #onetrust-pc-sdk .ot-label-status,#onetrust-consent-sdk #onetrust-pc-sdk .ot-chkbox label span,#onetrust-consent-sdk #onetrust-pc-sdk #clear-filters-handler{color:#696969}#onetrust-consent-sdk #onetrust-pc-sdk .privacy-notice-link,#onetrust-consent-sdk #onetrust-pc-sdk .category-vendors-list-handler,#onetrust-consent-sdk #onetrust-pc-sdk .category-vendors-list-handler+a,#onetrust-consent-sdk #onetrust-pc-sdk .category-host-list-handler,#onetrust-consent-sdk #onetrust-pc-sdk .ot-ven-link,#onetrust-consent-sdk #onetrust-pc-sdk #ot-host-lst .ot-host-name a,#onetrust-consent-sdk #onetrust-pc-sdk #ot-host-lst .ot-acc-hdr .ot-host-expand,#onetrust-consent-sdk #onetrust-pc-sdk #ot-host-lst .ot-host-info a{color:#3860BE}#onetrust-consent-sdk #onetrust-pc-sdk .category-vendors-list-handler:hover{opacity:.7}#onetrust-consent-sdk #onetrust-pc-sdk .ot-acc-grpcntr.ot-acc-txt,#onetrust-consent-sdk #onetrust-pc-sdk .ot-acc-txt .ot-subgrp-tgl .ot-switch.ot-toggle{background-color:#F8F8F8}#onetrust-consent-sdk #onetrust-pc-sdk #ot-host-lst .ot-host-info,#onetrust-consent-sdk #onetrust-pc-sdk .ot-acc-txt .ot-ven-dets{background-color:#F8F8F8}#onetrust-consent-sdk #onetrust-pc-sdk button:not(#clear-filters-handler):not(.ot-close-icon):not(#filter-btn-handler):not(.ot-remove-objection-handler):not(.ot-obj-leg-btn-handler):not([aria-expanded]):not(.ot-link-btn),#onetrust-consent-sdk #onetrust-pc-sdk .ot-leg-btn-container .ot-active-leg-btn{background-color:#FDC301;border-color:#FDC301;color:#0e100e}#onetrust-consent-sdk #onetrust-pc-sdk .ot-active-menu{border-color:#FDC301}#onetrust-consent-sdk #onetrust-pc-sdk .ot-leg-btn-container .ot-remove-objection-handler{background-color:transparent;border:1px solid transparent}#onetrust-consent-sdk #onetrust-pc-sdk .ot-leg-btn-container .ot-inactive-leg-btn{background-color:#FFFFFF;color:#78808E;border-color:#78808E}#onetrust-consent-sdk #onetrust-pc-sdk .ot-tgl input:focus+.ot-switch,.ot-switch .ot-switch-nob,.ot-switch .ot-switch-nob:before,#onetrust-pc-sdk .ot-checkbox input[type="checkbox"]:focus+label::before,#onetrust-pc-sdk .ot-chkbox input[type="checkbox"]:focus+label::before{outline-color:#000000;outline-width:1px}#onetrust-pc-sdk .ot-host-item>button:focus,#onetrust-pc-sdk .ot-ven-item>button:focus{border:1px solid #000000}#onetrust-consent-sdk #onetrust-pc-sdk *:focus,#onetrust-consent-sdk #onetrust-pc-sdk .ot-vlst-cntr>a:focus{outline:1px solid #000000}#onetrust-consent-sdk{font-family:Arial,sans-serif}#onetrust-pc-sdk .ot-always-active{font-size:14px;font-weight:bold;color:#09a501}#onetrust-consent-sdk #onetrust-pc-sdk .active-group{border-color:#FFFFFF}#onetrust-pc-sdk .ot-button-group button{font-weight:bold}#onetrust-consent-sdk #onetrust-pc-sdk .ot-button-group .onetrust-close-btn-handler{background-color:#d8d8d8!important;border-color:#d8d8d8!important}#onetrust-consent-sdk h3{font-size:14px!important}#onetrust-pc-sdk .pc-header{padding:10px}#onetrust-pc-sdk .ot-toggle .checkbox input:checked+label:after{background:#6cc04a}#onetrust-pc-sdk #pc-policy-text a{color:inherit}#onetrust-pc-sdk #close-pc-btn-handler.ot-close-icon{display:none}#onetrust-pc-sdk .group-description ul{font-size:inherit}#onetrust-pc-sdk .group-description ul li{list-style:none}.ot-sdk-cookie-policy{font-family:inherit;font-size:16px}.ot-sdk-cookie-policy.otRelFont{font-size:1rem}.ot-sdk-cookie-policy h3,.ot-sdk-cookie-policy h4,.ot-sdk-cookie-policy h6,.ot-sdk-cookie-policy p,.ot-sdk-cookie-policy li,.ot-sdk-cookie-policy a,.ot-sdk-cookie-policy th,.ot-sdk-cookie-policy #cookie-policy-description,.ot-sdk-cookie-policy .ot-sdk-cookie-policy-group,.ot-sdk-cookie-policy #cookie-policy-title{color:dimgray}.ot-sdk-cookie-policy #cookie-policy-description{margin-bottom:1em}.ot-sdk-cookie-policy h4{font-size:1.2em}.ot-sdk-cookie-policy h6{font-size:1em;margin-top:2em}.ot-sdk-cookie-policy th{min-width:75px}.ot-sdk-cookie-policy a,.ot-sdk-cookie-policy a:hover{background:#fff}.ot-sdk-cookie-policy thead{background-color:#f6f6f4;font-weight:bold}.ot-sdk-cookie-policy .ot-mobile-border{display:none}.ot-sdk-cookie-policy section{margin-bottom:2em}.ot-sdk-cookie-policy table{border-collapse:inherit}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy{font-family:inherit;font-size:1rem}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy h3,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy h4,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy h6,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy p,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy li,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy a,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy th,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy #cookie-policy-description,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-cookie-policy-group,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy #cookie-policy-title{color:dimgray}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy #cookie-policy-description{margin-bottom:1em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-subgroup{margin-left:1.5em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy #cookie-policy-description,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-cookie-policy-group-desc,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-table-header,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy a,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy span,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy td{font-size:.9em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy td span,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy td a{font-size:inherit}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-cookie-policy-group{font-size:1em;margin-bottom:.6em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-cookie-policy-title{margin-bottom:1.2em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy>section{margin-bottom:1em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy th{min-width:75px}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy a,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy a:hover{background:#fff}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy thead{background-color:#f6f6f4;font-weight:bold}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-mobile-border{display:none}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy section{margin-bottom:2em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-subgroup ul li{list-style:disc;margin-left:1.5em}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-subgroup ul li h4{display:inline-block}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table{border-collapse:inherit;margin:auto;border:1px solid #d7d7d7;border-radius:5px;border-spacing:initial;width:100%;overflow:hidden}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table th,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table td{border-bottom:1px solid #d7d7d7;border-right:1px solid #d7d7d7}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table tr:last-child td{border-bottom:0px}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table tr th:last-child,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table tr td:last-child{border-right:0px}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table .ot-host,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table .ot-cookies-type{width:25%}.ot-sdk-cookie-policy[dir=rtl]{text-align:left}#ot-sdk-cookie-policy h3{font-size:1.5em}@media only screen and (max-width:530px){.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) table,.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) thead,.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) tbody,.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) th,.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) td,.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) tr{display:block}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) thead tr{position:absolute;top:-9999px;left:-9999px}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) tr{margin:0 0 1em 0}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) tr:nth-child(odd),.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) tr:nth-child(odd) a{background:#f6f6f4}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) td{border:none;border-bottom:1px solid #eee;position:relative;padding-left:50%}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) td:before{position:absolute;height:100%;left:6px;width:40%;padding-right:10px}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) .ot-mobile-border{display:inline-block;background-color:#e4e4e4;position:absolute;height:100%;top:0;left:45%;width:2px}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) td:before{content:attr(data-label);font-weight:bold}.ot-sdk-cookie-policy:not(#ot-sdk-cookie-policy-v2) li{word-break:break-word;word-wrap:break-word}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table{overflow:hidden}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table td{border:none;border-bottom:1px solid #d7d7d7}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy thead,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy tbody,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy th,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy td,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy tr{display:block}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table .ot-host,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table .ot-cookies-type{width:auto}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy tr{margin:0 0 1em 0}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy td:before{height:100%;width:40%;padding-right:10px}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy td:before{content:attr(data-label);font-weight:bold}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy li{word-break:break-word;word-wrap:break-word}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy thead tr{position:absolute;top:-9999px;left:-9999px;z-index:-9999}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table tr:last-child td{border-bottom:1px solid #d7d7d7;border-right:0px}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table tr:last-child td:last-child{border-bottom:0px}}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy h5,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy h6,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy li,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy p,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy a,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy span,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy td,#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy #cookie-policy-description{color:#696969}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy th{color:#696969}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy .ot-sdk-cookie-policy-group{color:#696969}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy #cookie-policy-title{color:#696969}#ot-sdk-cookie-policy-v2.ot-sdk-cookie-policy table th{background-color:#F8F8F8}.ot-floating-button__front{background-image:url(resources/sample/11.png)}#ot-sdk-btn-floating.ot-floating-button{position:fixed;bottom:10px;opacity:0;width:50px;height:50px;line-height:15px;cursor:pointer;background-color:transparent;transform-style:preserve-3d;transition:all 300ms ease;perspective:1000px;z-index:2147483646;animation:otFloatingBtnIntro 800ms ease 0ms 1 forwards}#ot-sdk-btn-floating.ot-floating-button.ot-hide{display:none}#ot-sdk-btn-floating.ot-floating-button::before,#ot-sdk-btn-floating.ot-floating-button::after{text-transform:none;line-height:1;user-select:none;pointer-events:none;position:absolute;transform:scale(0);opacity:0;transition:all 300ms ease;display:block;height:auto}#ot-sdk-btn-floating.ot-floating-button::before{content:"";border:5px solid transparent;z-index:1001;top:50%;border-left-width:0;border-right-color:#333;right:calc(0em - 5px);transform:translate(10px,-50%)}#ot-sdk-btn-floating.ot-floating-button::after{content:attr(title);position:absolute;text-align:center;top:50%;left:calc(100% + 5px);transform:translate(10px,-50%);font-size:.75rem;min-width:3em;max-width:21em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:5px;border-radius:.3ch;box-shadow:0 1em 2em -0.5em rgba(0,0,0,.35);background:#333;color:#fff;z-index:2147483645}#ot-sdk-btn-floating.ot-floating-button:hover::before,#ot-sdk-btn-floating.ot-floating-button:hover::after{opacity:1}#ot-sdk-btn-floating.ot-floating-button:hover::before{transform:translate(0.5em,-50%) scale(1)}#ot-sdk-btn-floating.ot-floating-button:hover::after{transform:translate(0.5em,-50%) scale(1)}#ot-sdk-btn-floating.ot-floating-button.ot-pc-open .ot-floating-button__front{transform:rotateY(-180deg)}#ot-sdk-btn-floating.ot-floating-button.ot-pc-open .ot-floating-button__back{transform:rotateY(0)}#ot-sdk-btn-floating .ot-floating-button__front,#ot-sdk-btn-floating .ot-floating-button__back{position:absolute;width:100%;height:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:#6aaae4;border-radius:10px;box-shadow:0 4px 8px 0 rgba(0,0,0,.2);transition:transform .6s;transform-style:preserve-3d}#ot-sdk-btn-floating .ot-floating-button__front{background-color:#6aaae4;transform:rotateY(0)}#ot-sdk-btn-floating .ot-floating-button__front.custom-persistent-icon{background-position:center center;background-repeat:no-repeat;background-size:100%;border-radius:100px}#ot-sdk-btn-floating .ot-floating-button__front svg{width:30px;height:37px}#ot-sdk-btn-floating .ot-floating-button__back{background-color:#69c;transform:rotateY(-180deg)}#ot-sdk-btn-floating .ot-floating-button__back.custom-persistent-icon{background-position:center center;background-repeat:no-repeat;background-size:100%;border-radius:100px}#ot-sdk-btn-floating .ot-floating-button__back svg{width:24px;height:24px}#ot-sdk-btn-floating.ot-floating-button button{background-color:transparent;border:0;width:100%;height:100%;cursor:pointer}@keyframes otFloatingBtnIntro{0%{opacity:0;left:-75px}100%{opacity:1;left:1%}}@keyframes otFloatingBtnImageIntro{0%{opacity:0;transform:scale(0) rotate(-270deg)}100%{opacity:100%;transform:scale(0.95) rotate(0deg)}}</style><link type="image/x-icon" rel="shortcut icon" href="resources/sample/12.gif"><link rel="canonical" href="https://www.asda.com/account?request_origin=asda&amp;redirect_uri=https%3A%2F%2Fwww.asda.com%2Fgood-living%2Ftag%2Frecipes"><meta http-equiv="content-security-policy" content="default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;"></head><body><div id="app"><main class="theme-ghs" style><div class="app"><header><div class="header-container"><a id="logo" href="https://www.asda.com/" target="_self" tabindex="1" rel="noreferrer"><img src="resources/sample/13.svg" alt="Asda.com homepage"></a><nav><button id="need-help" class="basic need-help" type="button" tabindex="2" aria-live="polite">Need help?</button><button id="back-shop" class="basic back-shop" type="button" tabindex="3" aria-live="polite">Home</button></nav></div></header><div class="co-account"><h1 class="co-account__title">Account settings</h1><h4 class="co-account__description">Select a section below to view and edit your details</h4><div class="panel "><button class="panel__header "><h3 class="panel__title">Personal details</h3><span class="panel__toggle">View</span></button></div><div class="panel "><button class="panel__header "><h3 class="panel__title">Sign in details</h3><span class="panel__toggle">View</span></button></div><div class="acc-block"><div class="panel address-phone-book "><button class="panel__header "><h3 class="panel__title">Address &amp; phone book</h3><span class="panel__toggle">View</span></button></div></div><div class="wallet-card"><div class="panel open"><button class="panel__header add-bottom-border"><h3 class="panel__title">Wallet</h3><span class="panel__toggle">Close</span></button><main class="panel__main"><h3 class="empty-header-name">Payment Cards</h3><div class="co-add-card"><div class="custom-icon input-box"><label for="card-number">Card number</label><div class="input-box__wrapper false"><input type="tel" class=" error" placeholder spellcheck="false" autocomplete="off" maxlength="255" id="card-number" tabindex="0" aria-label="Card number" value data-fathom="cc-number"><img src="resources/sample/14.svg" alt="alert" class="alert"></div><div class="input-error" role="alert">Sorry, that's not a valid card number</div></div><div class="input-box"><label for="add-card-name">Name on card:</label><div class="input-box__wrapper false"><input type="text" class=" error" placeholder spellcheck="false" autocomplete="off" maxlength="255" id="add-card-name" tabindex="0" aria-label="Name on card:" value data-fathom="cc-name"><img src="resources/sample/14.svg" alt="alert" class="alert"></div><div class="input-error" role="alert">We do not recognise that name</div></div><div class="input-box"><label for="add-card-expiry">Expiry date</label><div class="input-box__wrapper false"><input type="tel" class=" error" placeholder="MM/YY" spellcheck="false" autocomplete="off" maxlength="5" id="add-card-expiry" tabindex="0" aria-label="Expiry date" value data-fathom="cc-exp"><img src="resources/sample/14.svg" alt="alert" class="alert"></div><div class="input-error" role="alert">Enter expiry date</div></div><h4 class="address-form-title">Search for your postcode or <button type="button" class="link-toggle">type out address in full</button></h4><div class="half input-box"><label for="account-postcode-finder">Your postcode*</label><div class="input-box__wrapper false"><input type="text" class="half-width error" placeholder spellcheck="false" autocomplete="off" maxlength="255" id="account-postcode-finder" tabindex="0" aria-label="Your postcode*" value data-fathom="zip"><img src="resources/sample/14.svg" alt="alert" class="alert"></div><div class="input-error" role="alert">Enter your postcode</div></div><div class="half last input-box"><label for="account-postcode-finder-house">House number or name</label><div class="input-box__wrapper false"><input type="text" class="half-width " placeholder spellcheck="false" autocomplete="off" maxlength="255" id="account-postcode-finder-house" tabindex="0" aria-label="House number or name" value></div></div><button id class="secondary full find-button" type="button" tabindex="0" aria-live="polite">Find</button></div></main></div></div><div class="permission-centre"><div class="panel "><button class="panel__header "><h3 class="panel__title">Permissions</h3><span class="panel__toggle">View</span></button></div></div></div><footer><div class="footer-wrapper"><div class="footer-top"><a class="website-feedback-link" href="https://ghs-optin-issues.asda.com/" target="_blank" rel="noopener noreferrer"><img class="feedback-icon" src="resources/sample/15.svg" alt="feedback icon"><span>Website Feedback</span></a></div><div class="footer-main"><div class="footer-logo"><img src="resources/sample/16.svg" alt="Asda"></div><div class="footer-links"><ul><li>© ASDA 2022</li><li><a href="https://groceries.asda.com/terms-and-conditions" target="_blank" rel="noopener noreferrer">Terms &amp; Conditions</a></li><li><a href="https://www.asda.com/privacy" target="_blank" rel="noopener noreferrer">Privacy Centre</a></li><li><a href="https://www.asda.com/help/company-details" target="_blank" rel="noopener noreferrer">Asda Company Details</a></li></ul></div></div></div></footer></div></main></div><noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-NHVQ6SB" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript><iframe style="display:none;visibility:hidden" sandbox="allow-popups allow-top-navigation allow-top-navigation-by-user-activation" srcdoc="<!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;> <html><head><meta charset=&quot;utf-8&quot;><title></title><meta http-equiv=&quot;content-security-policy&quot; content=&quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&quot;></head><body style=&quot;background-color:transparent&quot;><iframe style=&quot;display:none&quot; sandbox=&quot;allow-popups allow-top-navigation allow-top-navigation-by-user-activation&quot; srcdoc=&quot;<!DOCTYPE html PUBLIC &amp;quot;-//W3C//DTD HTML 4.01 Transitional//EN&amp;quot; &amp;quot;http://www.w3.org/TR/html4/loose.dtd&amp;quot;> <html><head><meta charset=&amp;quot;utf-8&amp;quot;><title></title><meta http-equiv=&amp;quot;content-security-policy&amp;quot; content=&amp;quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&amp;quot;></head><body style=&amp;quot;background-color:transparent&amp;quot;><iframe style=&amp;quot;display:none&amp;quot; sandbox=&amp;quot;allow-popups allow-top-navigation allow-top-navigation-by-user-activation&amp;quot; srcdoc=&amp;quot;<!DOCTYPE html PUBLIC &amp;amp;quot;-//W3C//DTD HTML 4.01 Transitional//EN&amp;amp;quot; &amp;amp;quot;http://www.w3.org/TR/html4/loose.dtd&amp;amp;quot;> <html><head><meta charset=&amp;amp;quot;utf-8&amp;amp;quot;><title></title><meta http-equiv=&amp;amp;quot;content-security-policy&amp;amp;quot; content=&amp;amp;quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&amp;amp;quot;></head><body style=&amp;amp;quot;background-color:transparent&amp;amp;quot;></body></html>&amp;quot; width=&amp;quot;1&amp;quot; height=&amp;quot;1&amp;quot; frameborder=&amp;quot;0&amp;quot;></iframe></body></html>&quot; width=&quot;1&quot; height=&quot;1&quot; frameborder=&quot;0&quot;></iframe></body></html>" width="0" height="0"></iframe><iframe style="display:none;visibility:hidden" sandbox="allow-popups allow-top-navigation allow-top-navigation-by-user-activation" srcdoc="<!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;> <html><head><meta charset=&quot;utf-8&quot;><title></title><meta http-equiv=&quot;content-security-policy&quot; content=&quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&quot;></head><body style=&quot;background-color:transparent&quot;><iframe style=&quot;display:none&quot; sandbox=&quot;allow-popups allow-top-navigation allow-top-navigation-by-user-activation&quot; srcdoc=&quot;<!DOCTYPE html PUBLIC &amp;quot;-//W3C//DTD HTML 4.01 Transitional//EN&amp;quot; &amp;quot;http://www.w3.org/TR/html4/loose.dtd&amp;quot;> <html><head><meta charset=&amp;quot;utf-8&amp;quot;><title></title><meta http-equiv=&amp;quot;content-security-policy&amp;quot; content=&amp;quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&amp;quot;></head><body style=&amp;quot;background-color:transparent&amp;quot;><iframe style=&amp;quot;display:none&amp;quot; sandbox=&amp;quot;allow-popups allow-top-navigation allow-top-navigation-by-user-activation&amp;quot; srcdoc=&amp;quot;<!DOCTYPE html PUBLIC &amp;amp;quot;-//W3C//DTD HTML 4.01 Transitional//EN&amp;amp;quot; &amp;amp;quot;http://www.w3.org/TR/html4/loose.dtd&amp;amp;quot;> <html><head><meta charset=&amp;amp;quot;utf-8&amp;amp;quot;><title></title><meta http-equiv=&amp;amp;quot;content-security-policy&amp;amp;quot; content=&amp;amp;quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&amp;amp;quot;></head><body style=&amp;amp;quot;background-color:transparent&amp;amp;quot;></body></html>&amp;quot; width=&amp;quot;1&amp;quot; height=&amp;quot;1&amp;quot; frameborder=&amp;quot;0&amp;quot;></iframe></body></html>&quot; width=&quot;1&quot; height=&quot;1&quot; frameborder=&quot;0&quot;></iframe></body></html>" width="0" height="0"></iframe><iframe style="display:none" name="__tcfapiLocator" title="CMP Locator" sandbox="allow-popups allow-top-navigation allow-top-navigation-by-user-activation" srcdoc="<html><head><meta charset=&quot;utf-8&quot;><meta http-equiv=&quot;content-security-policy&quot; content=&quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&quot;></head><body></body></html>"></iframe><div id="onetrust-consent-sdk" class="onetrust-consent-sdk-box" data-apply-ot-banner-popup="false"><div class="onetrust-pc-dark-filter ot-hide ot-fade-in"></div><div id="onetrust-pc-sdk" class="otPcCenter ot-hide ot-fade-in ot-sdk-not-webkit" aria-modal="true" role="alertdialog" aria-label="How we protect your privacy" lang="en"><!-- Close Button --><div class="ot-pc-header"><!-- Logo Tag --><div class="ot-pc-logo" role="img" aria-label="Company Logo" style="background-image:url(resources/sample/17.bin);background-position:left"></div></div><!-- Close Button --><div id="ot-pc-content" class="ot-pc-scrollbar"><h2 id="ot-pc-title">How we protect your privacy</h2><div id="ot-pc-desc">We process your data to deliver content or advertisements and measure the delivery of such content or advertisements to extract insights about our website. We share this information with our partners on the basis of consent. You may exercise your right to consent, based on a specific purpose below or at a partner level in the link under each purpose. These choices will be signaled to our vendors participating in the Transparency and Consent Framework.</div><button id="accept-recommended-btn-handler">Allow All</button><section class="ot-sdk-row ot-cat-grp"><h3 id="ot-category-title"> Manage Consent Preferences</h3><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="1"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-1" aria-labelledby="ot-header-id-1"></button><!-- Accordion header --><div class="ot-acc-hdr ot-always-active-group"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-1">Essential Cookies</h4><div class="ot-always-active">Always Active</div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-1">Essential cookies are always on. These are technical cookies that are required for the operation of our sites. Without using these our sites can’t operate properly.</p></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="2"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-2" aria-labelledby="ot-header-id-2"></button><!-- Accordion header --><div class="ot-acc-hdr"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-2">Experience Cookies</h4><div class="ot-tgl"><input type="checkbox" name="ot-group-id-2" id="ot-group-id-2" aria-checked="true" role="switch" class="category-switch-handler" data-optanongroupid="2" checked aria-labelledby="ot-header-id-2"> <label class="ot-switch" for="ot-group-id-2"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Experience Cookies</span></label> </div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-2">Right now only some of these can be turned off. We’re working on the rest. Experience cookies allow Asda to recognise and count visitors to our sites.
+This means we can check our online services are working and help us make improvements on our online services.
+Experience cookies also allow us to remember the choices you make, such as the items in your basket or your display preferences.</p></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="4"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-4" aria-labelledby="ot-header-id-4"></button><!-- Accordion header --><div class="ot-acc-hdr"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-4">Asda’s Advertising Cookies</h4><div class="ot-tgl"><input type="checkbox" name="ot-group-id-4" id="ot-group-id-4" aria-checked="true" role="switch" class="category-switch-handler" data-optanongroupid="4" checked aria-labelledby="ot-header-id-4"> <label class="ot-switch" for="ot-group-id-4"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Asda’s Advertising Cookies</span></label> </div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-4">We may record your visits to our websites, the pages you have visited and the links you have followed.
+We use this information, along with other information about you as a customer, to help make our advertising relevant to your interests both on our sites and other sites you may visit.
+We also use these to limit the number of times that you see an ad and to inform us how effective a particular ad has been.
+</p></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="STACK42"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-STACK42" aria-labelledby="ot-header-id-STACK42"></button><!-- Accordion header --><div class="ot-acc-hdr"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-STACK42">Personalised ads and content, ad and content measurement, audience insights and product development</h4><div class="ot-tgl"><input type="checkbox" name="ot-group-id-STACK42" id="ot-group-id-STACK42" aria-checked="true" role="switch" class="category-switch-handler" data-optanongroupid="STACK42" checked aria-labelledby="ot-header-id-STACK42"> <label class="ot-switch" for="ot-group-id-STACK42"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Personalised ads and content, ad and content measurement, audience insights and product development</span></label> </div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_2"><h5>Select basic ads</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_2" aria-checked="false" role="switch" data-optanongroupid="IABV2_2" class="cookie-subgroup-handler" aria-label="Select basic ads" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_2"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">Ads can be shown to you based on the content you’re viewing, the app you’re using, your approximate location, or your device type.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_3"><h5>Create a personalised ads profile</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_3" aria-checked="false" role="switch" data-optanongroupid="IABV2_3" class="cookie-subgroup-handler" aria-label="Create a personalised ads profile" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_3"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">A profile can be built about you and your interests to show you personalised ads that are relevant to you.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_4"><h5>Select personalised ads</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_4" aria-checked="false" role="switch" data-optanongroupid="IABV2_4" class="cookie-subgroup-handler" aria-label="Select personalised ads" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_4"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">Personalised ads can be shown to you based on a profile about you.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_5"><h5>Create a personalised content profile</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_5" aria-checked="false" role="switch" data-optanongroupid="IABV2_5" class="cookie-subgroup-handler" aria-label="Create a personalised content profile" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_5"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">A profile can be built about you and your interests to show you personalised content that is relevant to you.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_6"><h5>Select personalised content</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_6" aria-checked="false" role="switch" data-optanongroupid="IABV2_6" class="cookie-subgroup-handler" aria-label="Select personalised content" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_6"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">Personalised content can be shown to you based on a profile about you.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_7"><h5>Measure ad performance</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_7" aria-checked="false" role="switch" data-optanongroupid="IABV2_7" class="cookie-subgroup-handler" aria-label="Measure ad performance" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_7"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">The performance and effectiveness of ads that you see or interact with can be measured.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_8"><h5>Measure content performance</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_8" aria-checked="false" role="switch" data-optanongroupid="IABV2_8" class="cookie-subgroup-handler" aria-label="Measure content performance" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_8"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">The performance and effectiveness of content that you see or interact with can be measured.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_9"><h5>Apply market research to generate audience insights</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_9" aria-checked="false" role="switch" data-optanongroupid="IABV2_9" class="cookie-subgroup-handler" aria-label="Apply market research to generate audience insights" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_9"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">Market research can be used to learn more about the audiences who visit sites/apps and view ads.</p></li></ul></div><div class="ot-subgrp-cntr"><ul class="ot-subgrps"><li class="ot-subgrp" data-optanongroupid="IABV2_10"><h5>Develop and improve products</h5><div class="ot-tgl-cntr ot-subgrp-tgl"><div class="ot-tgl"><input type="checkbox" name="switch" id="ot-sub-group-id-IABV2_10" aria-checked="false" role="switch" data-optanongroupid="IABV2_10" class="cookie-subgroup-handler" aria-label="Develop and improve products" checked> <label class="ot-switch" for="ot-sub-group-id-IABV2_10"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Switch Label</span></label> </div></div><p class="ot-subgrp-desc">Your data can be used to improve existing systems and software, and to develop new products</p></li></ul></div><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="STACK42">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="IABV2_1"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-IABV2_1" aria-labelledby="ot-header-id-IABV2_1"></button><!-- Accordion header --><div class="ot-acc-hdr"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-IABV2_1">Store and/or access information on a device</h4><div class="ot-tgl"><input type="checkbox" name="ot-group-id-IABV2_1" id="ot-group-id-IABV2_1" aria-checked="true" role="switch" class="category-switch-handler" data-optanongroupid="IABV2_1" checked aria-labelledby="ot-header-id-IABV2_1"> <label class="ot-switch" for="ot-group-id-IABV2_1"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Store and/or access information on a device</span></label> </div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-IABV2_1">Cookies, device identifiers, or other information can be stored or accessed on your device for the purposes presented to you.</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="IABV2_1">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="ISFV2_1"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-ISFV2_1" aria-labelledby="ot-header-id-ISFV2_1"></button><!-- Accordion header --><div class="ot-acc-hdr"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-ISFV2_1">Use precise geolocation data</h4><div class="ot-tgl"><input type="checkbox" name="ot-group-id-ISFV2_1" id="ot-group-id-ISFV2_1" aria-checked="true" role="switch" class="category-switch-handler" data-optanongroupid="ISFV2_1" checked aria-labelledby="ot-header-id-ISFV2_1"> <label class="ot-switch" for="ot-group-id-ISFV2_1"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Use precise geolocation data</span></label> </div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-ISFV2_1">Your precise geolocation data can be used in support of one or more purposes. This means your location can be accurate to within several meters.</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="ISFV2_1">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="ISFV2_2"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-ISFV2_2" aria-labelledby="ot-header-id-ISFV2_2"></button><!-- Accordion header --><div class="ot-acc-hdr"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-ISFV2_2">Actively scan device characteristics for identification</h4><div class="ot-tgl"><input type="checkbox" name="ot-group-id-ISFV2_2" id="ot-group-id-ISFV2_2" aria-checked="true" role="switch" class="category-switch-handler" data-optanongroupid="ISFV2_2" checked aria-labelledby="ot-header-id-ISFV2_2"> <label class="ot-switch" for="ot-group-id-ISFV2_2"><span class="ot-switch-nob"></span> <span class="ot-label-txt">Actively scan device characteristics for identification</span></label> </div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-ISFV2_2">Your device can be identified based on a scan of your device's unique combination of characteristics.</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="ISFV2_2">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="ISPV2_1"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-ISPV2_1" aria-labelledby="ot-header-id-ISPV2_1"></button><!-- Accordion header --><div class="ot-acc-hdr ot-always-active-group"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-ISPV2_1">Ensure security, prevent fraud, and debug</h4><div class="ot-always-active">Always Active</div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-ISPV2_1">Your data can be used to monitor for and prevent fraudulent activity, and ensure systems and processes work properly and securely.</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="ISPV2_1">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="ISPV2_2"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-ISPV2_2" aria-labelledby="ot-header-id-ISPV2_2"></button><!-- Accordion header --><div class="ot-acc-hdr ot-always-active-group"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-ISPV2_2">Technically deliver ads or content</h4><div class="ot-always-active">Always Active</div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-ISPV2_2">Your device can receive and send information that allows you to see and interact with ads and content.</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="ISPV2_2">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="IFEV2_1"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-IFEV2_1" aria-labelledby="ot-header-id-IFEV2_1"></button><!-- Accordion header --><div class="ot-acc-hdr ot-always-active-group"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-IFEV2_1">Match and combine offline data sources</h4><div class="ot-always-active">Always Active</div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-IFEV2_1">Data from offline data sources can be combined with your online activity in support of one or more purposes</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="IFEV2_1">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="IFEV2_2"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-IFEV2_2" aria-labelledby="ot-header-id-IFEV2_2"></button><!-- Accordion header --><div class="ot-acc-hdr ot-always-active-group"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-IFEV2_2">Link different devices</h4><div class="ot-always-active">Always Active</div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-IFEV2_2">Different devices can be determined as belonging to you or your household in support of one or more of purposes.</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="IFEV2_2">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><div class="ot-accordion-layout ot-cat-item" data-optanongroupid="IFEV2_3"><button aria-expanded="false" ot-accordion="true" aria-controls="ot-desc-id-IFEV2_3" aria-labelledby="ot-header-id-IFEV2_3"></button><!-- Accordion header --><div class="ot-acc-hdr ot-always-active-group"><div class="ot-plus-minus"><span></span><span></span></div><h4 class="ot-cat-header" id="ot-header-id-IFEV2_3">Receive and use automatically-sent device characteristics for identification</h4><div class="ot-always-active">Always Active</div></div><!-- accordion detail --><div class="ot-acc-grpcntr ot-acc-txt"><p class="ot-acc-grpdesc ot-category-desc" id="ot-desc-id-IFEV2_3">Your device might be distinguished from other devices based on information it automatically sends, such as IP address or browser type.</p><div class="ot-vlst-cntr"><button class="ot-link-btn category-vendors-list-handler" aria-label="IAB Vendor Details button opens Vendor List menu" data-parent-id="IFEV2_3">List of IAB Vendors‎</button><a href="https://tcf.cookiepedia.co.uk/?lang=en" rel="noopener" target="_blank">&nbsp;|&nbsp;View Full Legal Text&nbsp;<span class="ot-scrn-rdr">Opens in a new Tab</span></a></div></div></div><!-- Groups sections starts --><!-- Group section ends --><!-- Accordion Group section starts --><!-- Accordion Group section ends --></section></div><section id="ot-pc-lst" class="ot-hide ot-hosts-ui ot-pc-scrollbar"><div id="ot-pc-hdr"><div id="ot-lst-title"><button class="ot-link-btn back-btn-handler" aria-label="Back"><svg id="ot-back-arw" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 444.531 444.531" xml:space="preserve"><title>Back Button</title><g><path fill="#656565" d="M213.13,222.409L351.88,83.653c7.05-7.043,10.567-15.657,10.567-25.841c0-10.183-3.518-18.793-10.567-25.835
+ l-21.409-21.416C323.432,3.521,314.817,0,304.637,0s-18.791,3.521-25.841,10.561L92.649,196.425
+ c-7.044,7.043-10.566,15.656-10.566,25.841s3.521,18.791,10.566,25.837l186.146,185.864c7.05,7.043,15.66,10.564,25.841,10.564
+ s18.795-3.521,25.834-10.564l21.409-21.412c7.05-7.039,10.567-15.604,10.567-25.697c0-10.085-3.518-18.746-10.567-25.978
+ L213.13,222.409z"></path></g></svg></button><h3>Performance Cookies</h3></div><div class="ot-lst-subhdr"><div class="ot-search-cntr"><p role="status" class="ot-scrn-rdr"></p><label for="vendor-search-handler" class="ot-scrn-rdr"></label> <input id="vendor-search-handler" type="text" name="vendor-search-handler" value> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 -30 110 110" aria-hidden="true"><title>Search Icon</title><path fill="#2e3644" d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23
+ s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92
+ c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17
+ s-17-7.626-17-17S14.61,6,23.984,6z"></path></svg></div><div class="ot-fltr-cntr"><button id="filter-btn-handler" aria-label="Filter" aria-haspopup="true"><svg role="presentation" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 402.577 402.577" xml:space="preserve"><title>Filter Icon</title><g><path fill="#fff" d="M400.858,11.427c-3.241-7.421-8.85-11.132-16.854-11.136H18.564c-7.993,0-13.61,3.715-16.846,11.136
+ c-3.234,7.801-1.903,14.467,3.999,19.985l140.757,140.753v138.755c0,4.955,1.809,9.232,5.424,12.854l73.085,73.083
+ c3.429,3.614,7.71,5.428,12.851,5.428c2.282,0,4.66-0.479,7.135-1.43c7.426-3.238,11.14-8.851,11.14-16.845V172.166L396.861,31.413
+ C402.765,25.895,404.093,19.231,400.858,11.427z"></path></g></svg></button></div><div id="ot-anchor"></div><section id="ot-fltr-modal"><div id="ot-fltr-cnt"><button id="clear-filters-handler">Clear</button><div class="ot-fltr-scrlcnt ot-pc-scrollbar"><div class="ot-fltr-opts"><div class="ot-fltr-opt"><div class="ot-chkbox"><input id="chkbox-id" type="checkbox" aria-checked="false" class="category-filter-handler"> <label for="chkbox-id"><span class="ot-label-txt">checkbox label</span></label> <span class="ot-label-status">label</span></div></div></div><div class="ot-fltr-btns"><button id="filter-apply-handler">Apply</button> <button id="filter-cancel-handler">Cancel</button></div></div></div></section></div></div><section id="ot-lst-cnt" class="ot-host-cnt ot-pc-scrollbar"><div id="ot-sel-blk"><div class="ot-sel-all"><div class="ot-sel-all-hdr"><span class="ot-consent-hdr">Consent</span> <span class="ot-li-hdr">Leg.Interest</span></div><div class="ot-sel-all-chkbox"><div class="ot-chkbox" id="ot-selall-hostcntr"><input id="select-all-hosts-groups-handler" type="checkbox" aria-checked="false"> <label for="select-all-hosts-groups-handler"><span class="ot-label-txt">checkbox label</span></label> <span class="ot-label-status">label</span></div><div class="ot-chkbox" id="ot-selall-vencntr"><input id="select-all-vendor-groups-handler" type="checkbox" aria-checked="false"> <label for="select-all-vendor-groups-handler"><span class="ot-label-txt">checkbox label</span></label> <span class="ot-label-status">label</span></div><div class="ot-chkbox" id="ot-selall-licntr"><input id="select-all-vendor-leg-handler" type="checkbox" aria-checked="false"> <label for="select-all-vendor-leg-handler"><span class="ot-label-txt">checkbox label</span></label> <span class="ot-label-status">label</span></div></div></div></div><div class="ot-sdk-row"><div class="ot-sdk-column"><ul id="ot-ven-lst"></ul></div></div></section></section><div class="ot-pc-footer"><div class="ot-btn-container"> <button class="save-preference-btn-handler onetrust-close-btn-handler">Confirm My Choices</button></div><!-- Footer logo --><div class="ot-pc-footer-logo"><a href="https://www.onetrust.com/products/cookie-consent/" target="_blank" rel="noopener noreferrer" style="background-image:url(resources/sample/18.svg)" aria-label="Powered by OneTrust Opens in a new Tab"></a></div></div><!-- Cookie subgroup container --><!-- Vendor list link --><!-- Cookie lost link --><!-- Toggle HTML element --><!-- Checkbox HTML --><!-- plus minus--><!-- Arrow SVG element --><!-- Accordion basic element --><span class="ot-scrn-rdr" aria-atomic="true" aria-live="polite"></span><iframe class="ot-text-resize" title="onetrust-text-resize" style="position:absolute;top:-50000px;width:100em" aria-hidden="true" sandbox="allow-popups allow-top-navigation allow-top-navigation-by-user-activation" srcdoc="<html><head><meta charset=&quot;utf-8&quot;><meta http-equiv=&quot;content-security-policy&quot; content=&quot;default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;&quot;></head><body></body></html>"></iframe></div><div id="ot-sdk-btn-floating" title="Manage Preferences" class="ot-floating-button"><div class="ot-floating-button__front custom-persistent-icon"><button type="button" class="ot-floating-button__open" aria-label="Open Preferences"></button></div><div class="ot-floating-button__back custom-persistent-icon"><button type="button" class="ot-floating-button__close"><!--?xml version="1.0" encoding="UTF-8"?--> <svg role="presentation" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg"><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Banner_02" class="ot-floating-button__svg-fill" transform="translate(-318.000000, -725.000000)" fill="#ffffff" fill-rule="nonzero"><g id="Group-2" transform="translate(305.000000, 712.000000)"><g id="icon/16px/white/close"><polygon id="Line1" points="13.3333333 14.9176256 35.0823744 36.6666667 36.6666667 35.0823744 14.9176256 13.3333333"></polygon><polygon id="Line2" transform="translate(25.000000, 25.000000) scale(-1, 1) translate(-25.000000, -25.000000) " points="13.3333333 14.9176256 35.0823744 36.6666667 36.6666667 35.0823744 14.9176256 13.3333333"></polygon></g></g></g></g></svg></button></div></div></div></body></html>
diff --git a/browser/extensions/formautofill/test/browser/focus-leak/browser.toml b/browser/extensions/formautofill/test/browser/focus-leak/browser.toml
new file mode 100644
index 0000000000..1c7d3df240
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/focus-leak/browser.toml
@@ -0,0 +1,13 @@
+[DEFAULT]
+support-files = [
+ "doc_iframe_typecontent_input_focus.xhtml",
+ "doc_iframe_typecontent_input_focus_frame.html",
+ "../head.js",
+]
+
+# This test is used to detect a leak.
+# Keep it isolated in a dedicated test folder to make sure the leak is cleaned
+# up as a sideeffect of another test.
+
+["browser_iframe_typecontent_input_focus.js"]
+skip-if = ["apple_silicon"] # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
diff --git a/browser/extensions/formautofill/test/browser/focus-leak/browser_iframe_typecontent_input_focus.js b/browser/extensions/formautofill/test/browser/focus-leak/browser_iframe_typecontent_input_focus.js
new file mode 100644
index 0000000000..407c2b5163
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/focus-leak/browser_iframe_typecontent_input_focus.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL_ROOT =
+ "chrome://mochitests/content/browser/browser/extensions/formautofill/test/browser/focus-leak/";
+
+const XUL_FRAME_URI = URL_ROOT + "doc_iframe_typecontent_input_focus.xhtml";
+const INNER_HTML_FRAME_URI =
+ URL_ROOT + "doc_iframe_typecontent_input_focus_frame.html";
+
+/**
+ * Check that focusing an input in a frame with type=content embedded in a xul
+ * document does not leak.
+ */
+add_task(async function () {
+ const doc = gBrowser.ownerDocument;
+ const linkedBrowser = gBrowser.selectedTab.linkedBrowser;
+ const browserContainer = gBrowser.getBrowserContainer(linkedBrowser);
+
+ info("Load the test page in a frame with type content");
+ const frame = doc.createXULElement("iframe");
+ frame.setAttribute("type", "content");
+ browserContainer.appendChild(frame);
+
+ info("Wait for the xul iframe to be loaded");
+ const onXulFrameLoad = BrowserTestUtils.waitForEvent(frame, "load", true);
+ frame.setAttribute("src", XUL_FRAME_URI);
+ await onXulFrameLoad;
+
+ const panelFrame = frame.contentDocument.querySelector("#html-iframe");
+
+ info("Wait for the html iframe to be loaded");
+ const onFrameLoad = BrowserTestUtils.waitForEvent(panelFrame, "load", true);
+ panelFrame.setAttribute("src", INNER_HTML_FRAME_URI);
+ await onFrameLoad;
+
+ info("Focus an input inside the iframe");
+ const focusMeInput = panelFrame.contentDocument.querySelector(".focusme");
+ const onFocus = BrowserTestUtils.waitForEvent(focusMeInput, "focus");
+ await SimpleTest.promiseFocus(panelFrame);
+ focusMeInput.focus();
+ await onFocus;
+
+ // This assert is not really meaningful, the main purpose of the test is
+ // to check against leaks.
+ is(
+ focusMeInput,
+ panelFrame.contentDocument.activeElement,
+ "The .focusme input is the active element"
+ );
+
+ info("Remove the focused input");
+ focusMeInput.remove();
+});
diff --git a/browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus.xhtml b/browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus.xhtml
new file mode 100644
index 0000000000..ec3aaa2648
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus.xhtml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+ <box>
+ <html:iframe id="html-iframe"/>
+ </box>
+</window>
diff --git a/browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus_frame.html b/browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus_frame.html
new file mode 100644
index 0000000000..00853d8eec
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/focus-leak/doc_iframe_typecontent_input_focus_frame.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <input type="text" name="test" class="focusme">
+ </body>
+</html>
diff --git a/browser/extensions/formautofill/test/browser/head.js b/browser/extensions/formautofill/test/browser/head.js
new file mode 100644
index 0000000000..7a58b8a202
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/head.js
@@ -0,0 +1,1257 @@
+"use strict";
+
+const { OSKeyStore } = ChromeUtils.importESModule(
+ "resource://gre/modules/OSKeyStore.sys.mjs"
+);
+const { OSKeyStoreTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/OSKeyStoreTestUtils.sys.mjs"
+);
+
+const { FormAutofillParent } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillParent.sys.mjs"
+);
+
+const { AutofillDoorhanger, AddressEditDoorhanger, AddressSaveDoorhanger } =
+ ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillPrompter.sys.mjs"
+ );
+
+const { FormAutofillNameUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs"
+);
+
+const MANAGE_ADDRESSES_DIALOG_URL =
+ "chrome://formautofill/content/manageAddresses.xhtml";
+const MANAGE_CREDIT_CARDS_DIALOG_URL =
+ "chrome://formautofill/content/manageCreditCards.xhtml";
+const EDIT_ADDRESS_DIALOG_URL =
+ "chrome://formautofill/content/editAddress.xhtml";
+const EDIT_CREDIT_CARD_DIALOG_URL =
+ "chrome://formautofill/content/editCreditCard.xhtml";
+const PRIVACY_PREF_URL = "about:preferences#privacy";
+
+const HTTP_TEST_PATH = "/browser/browser/extensions/formautofill/test/browser/";
+const BASE_URL = "http://mochi.test:8888" + HTTP_TEST_PATH;
+const FORM_URL = BASE_URL + "autocomplete_basic.html";
+const ADDRESS_FORM_URL =
+ "https://example.org" +
+ HTTP_TEST_PATH +
+ "address/autocomplete_address_basic.html";
+const ADDRESS_FORM_WITHOUT_AUTOCOMPLETE_URL =
+ "https://example.org" +
+ HTTP_TEST_PATH +
+ "address/without_autocomplete_address_basic.html";
+const ADDRESS_FORM_WITH_PAGE_NAVIGATION_BUTTONS =
+ "https://example.org" +
+ HTTP_TEST_PATH +
+ "address/capture_address_on_page_navigation.html";
+const CREDITCARD_FORM_URL =
+ "https://example.org" +
+ HTTP_TEST_PATH +
+ "creditCard/autocomplete_creditcard_basic.html";
+const CREDITCARD_FORM_IFRAME_URL =
+ "https://example.org" +
+ HTTP_TEST_PATH +
+ "creditCard/autocomplete_creditcard_iframe.html";
+const CREDITCARD_FORM_COMBINED_EXPIRY_URL =
+ "https://example.org" +
+ HTTP_TEST_PATH +
+ "creditCard/autocomplete_creditcard_cc_exp_field.html";
+const CREDITCARD_FORM_WITHOUT_AUTOCOMPLETE_URL =
+ "https://example.org" +
+ HTTP_TEST_PATH +
+ "creditCard/without_autocomplete_creditcard_basic.html";
+const CREDITCARD_FORM_WITH_PAGE_NAVIGATION_BUTTONS =
+ "https://example.org" +
+ HTTP_TEST_PATH +
+ "creditCard/capture_creditCard_on_page_navigation.html";
+const EMPTY_URL = "https://example.org" + HTTP_TEST_PATH + "empty.html";
+
+const ENABLED_AUTOFILL_ADDRESSES_PREF =
+ "extensions.formautofill.addresses.enabled";
+const ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF =
+ "extensions.formautofill.addresses.capture.enabled";
+const AUTOFILL_ADDRESSES_AVAILABLE_PREF =
+ "extensions.formautofill.addresses.supported";
+const ENABLED_AUTOFILL_ADDRESSES_SUPPORTED_COUNTRIES_PREF =
+ "extensions.formautofill.addresses.supportedCountries";
+const AUTOFILL_CREDITCARDS_AVAILABLE_PREF =
+ "extensions.formautofill.creditCards.supported";
+const ENABLED_AUTOFILL_CREDITCARDS_PREF =
+ "extensions.formautofill.creditCards.enabled";
+const SUPPORTED_COUNTRIES_PREF = "extensions.formautofill.supportedCountries";
+const SYNC_USERNAME_PREF = "services.sync.username";
+const SYNC_ADDRESSES_PREF = "services.sync.engine.addresses";
+const SYNC_CREDITCARDS_PREF = "services.sync.engine.creditcards";
+const SYNC_CREDITCARDS_AVAILABLE_PREF =
+ "services.sync.engine.creditcards.available";
+
+const TEST_ADDRESS_1 = {
+ "given-name": "John",
+ "additional-name": "R.",
+ "family-name": "Smith",
+ organization: "World Wide Web Consortium",
+ "street-address": "32 Vassar Street\nMIT Room 32-G524",
+ "address-level2": "Cambridge",
+ "address-level1": "MA",
+ "postal-code": "02139",
+ country: "US",
+ tel: "+16172535702",
+ email: "timbl@w3.org",
+};
+
+const TEST_ADDRESS_2 = {
+ "given-name": "Anonymouse",
+ "street-address": "Some Address",
+ country: "US",
+};
+
+const TEST_ADDRESS_3 = {
+ "given-name": "John",
+ "street-address": "Other Address",
+ "postal-code": "12345",
+};
+
+const TEST_ADDRESS_4 = {
+ "given-name": "Timothy",
+ "family-name": "Berners-Lee",
+ organization: "World Wide Web Consortium",
+ "street-address": "32 Vassar Street\nMIT Room 32-G524",
+ country: "US",
+ email: "timbl@w3.org",
+};
+
+// TODO: Number of field less than AUTOFILL_FIELDS_THRESHOLD
+// need to confirm whether this is intentional
+const TEST_ADDRESS_5 = {
+ tel: "+16172535702",
+};
+
+const TEST_ADDRESS_CA_1 = {
+ "given-name": "John",
+ "additional-name": "R.",
+ "family-name": "Smith",
+ organization: "Mozilla",
+ "street-address": "163 W Hastings\nSuite 209",
+ "address-level2": "Vancouver",
+ "address-level1": "BC",
+ "postal-code": "V6B 1H5",
+ country: "CA",
+ tel: "+17787851540",
+ email: "timbl@w3.org",
+};
+
+const TEST_ADDRESS_DE_1 = {
+ "given-name": "John",
+ "additional-name": "R.",
+ "family-name": "Smith",
+ organization: "Mozilla",
+ "street-address":
+ "Geb\u00E4ude 3, 4. Obergeschoss\nSchlesische Stra\u00DFe 27",
+ "address-level2": "Berlin",
+ "postal-code": "10997",
+ country: "DE",
+ tel: "+4930983333000",
+ email: "timbl@w3.org",
+};
+
+const TEST_ADDRESS_IE_1 = {
+ "given-name": "Bob",
+ "additional-name": "Z.",
+ "family-name": "Builder",
+ organization: "Best Co.",
+ "street-address": "123 Kilkenny St.",
+ "address-level3": "Some Townland",
+ "address-level2": "Dublin",
+ "address-level1": "Co. Dublin",
+ "postal-code": "A65 F4E2",
+ country: "IE",
+ tel: "+13534564947391",
+ email: "ie@example.com",
+};
+
+const TEST_CREDIT_CARD_1 = {
+ "cc-name": "John Doe",
+ "cc-number": "4111111111111111",
+ "cc-exp-month": 4,
+ "cc-exp-year": new Date().getFullYear(),
+};
+
+const TEST_CREDIT_CARD_2 = {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": 12,
+ "cc-exp-year": new Date().getFullYear() + 10,
+};
+
+const TEST_CREDIT_CARD_3 = {
+ "cc-number": "5103059495477870",
+ "cc-exp-month": 1,
+ "cc-exp-year": 2000,
+};
+
+const TEST_CREDIT_CARD_4 = {
+ "cc-number": "5105105105105100",
+};
+
+const TEST_CREDIT_CARD_5 = {
+ "cc-name": "Chris P. Bacon",
+ "cc-number": "4012888888881881",
+};
+
+const MAIN_BUTTON = "button";
+const SECONDARY_BUTTON = "secondaryButton";
+const MENU_BUTTON = "menubutton";
+const EDIT_ADDRESS_BUTTON = "edit";
+const ADDRESS_MENU_BUTTON = "addressMenuButton";
+const ADDRESS_MENU_LEARN_MORE = "learnMore";
+const ADDRESS_MENU_PREFENCE = "preference";
+
+/**
+ * Collection of timeouts that are used to ensure something should not happen.
+ */
+const TIMEOUT_ENSURE_PROFILE_NOT_SAVED = 1000;
+const TIMEOUT_ENSURE_CC_DIALOG_NOT_CLOSED = 500;
+const TIMEOUT_ENSURE_AUTOCOMPLETE_NOT_SHOWN = 1000;
+const TIMEOUT_ENSURE_DOORHANGER_NOT_SHOWN = 1000;
+
+async function ensureCreditCardDialogNotClosed(win) {
+ const unloadHandler = () => {
+ ok(false, "Credit card dialog shouldn't be closed");
+ };
+ win.addEventListener("unload", unloadHandler);
+ await new Promise(resolve =>
+ setTimeout(resolve, TIMEOUT_ENSURE_CC_DIALOG_NOT_CLOSED)
+ );
+ win.removeEventListener("unload", unloadHandler);
+}
+
+function getDisplayedPopupItems(
+ browser,
+ selector = ".autocomplete-richlistitem"
+) {
+ info("getDisplayedPopupItems");
+ const {
+ autoCompletePopup: { richlistbox: itemsBox },
+ } = browser;
+ const listItemElems = itemsBox.querySelectorAll(selector);
+
+ return [...listItemElems].filter(
+ item => item.getAttribute("collapsed") != "true"
+ );
+}
+
+async function sleep(ms = 500) {
+ await new Promise(resolve => setTimeout(resolve, ms));
+}
+
+async function ensureNoAutocompletePopup(browser) {
+ await new Promise(resolve =>
+ setTimeout(resolve, TIMEOUT_ENSURE_AUTOCOMPLETE_NOT_SHOWN)
+ );
+ const items = getDisplayedPopupItems(browser);
+ ok(!items.length, "Should not find autocomplete items");
+}
+
+async function ensureNoDoorhanger(browser) {
+ await new Promise(resolve =>
+ setTimeout(resolve, TIMEOUT_ENSURE_DOORHANGER_NOT_SHOWN)
+ );
+
+ let notifications = PopupNotifications.panel.childNodes;
+ ok(!notifications.length, "Should not find a doorhanger");
+}
+
+/**
+ * Wait for "formautofill-storage-changed" events
+ *
+ * @param {Array<string>} eventTypes
+ * eventType must be one of the following:
+ * `add`, `update`, `remove`, `notifyUsed`, `removeAll`, `reconcile`
+ *
+ * @returns {Promise} resolves when all events are received
+ */
+async function waitForStorageChangedEvents(...eventTypes) {
+ return Promise.all(
+ eventTypes.map(type =>
+ TestUtils.topicObserved(
+ "formautofill-storage-changed",
+ (subject, data) => {
+ return data == type;
+ }
+ )
+ )
+ );
+}
+
+/**
+ * Wait until the element found matches the expected autofill value
+ *
+ * @param {object} target
+ * The target in which to run the task.
+ * @param {string} selector
+ * A selector used to query the element.
+ * @param {string} value
+ * The expected autofilling value for the element
+ */
+async function waitForAutofill(target, selector, value) {
+ await SpecialPowers.spawn(
+ target,
+ [selector, value],
+ async function (selector, val) {
+ await ContentTaskUtils.waitForCondition(() => {
+ let element = content.document.querySelector(selector);
+ return element.value == val;
+ }, "Autofill never fills");
+ }
+ );
+}
+
+/**
+ * Waits for the subDialog to be loaded
+ *
+ * @param {Window} win The window of the dialog
+ * @param {string} dialogUrl The url of the dialog that we are waiting for
+ *
+ * @returns {Promise} resolves when the sub dialog is loaded
+ */
+function waitForSubDialogLoad(win, dialogUrl) {
+ return new Promise((resolve, reject) => {
+ win.gSubDialog._dialogStack.addEventListener(
+ "dialogopen",
+ async function dialogopen(evt) {
+ let cwin = evt.detail.dialog._frame.contentWindow;
+ if (cwin.location != dialogUrl) {
+ return;
+ }
+ content.gSubDialog._dialogStack.removeEventListener(
+ "dialogopen",
+ dialogopen
+ );
+
+ resolve(cwin);
+ }
+ );
+ });
+}
+
+/**
+ * Use this function when you want to update the value of elements in
+ * a form and then submit the form. This function makes sure the form
+ * is "identified" (`identifyAutofillFields` is called) before submitting
+ * the form.
+ * This is guaranteed by first focusing on an element in the form to trigger
+ * the 'FormAutofill:FieldsIdentified' message.
+ *
+ * @param {object} target
+ * The target in which to run the task.
+ * @param {object} args
+ * @param {string} args.focusSelector
+ * A selector used to query the element to be focused
+ * @param {string} args.formId
+ * The id of the form to be updated. This function uses "form" if
+ * this argument is not present
+ * @param {string} args.formSelector
+ * A selector used to query the form element
+ * @param {object} args.newValues
+ * Elements to be updated. Key is the element selector, value is the
+ * new value of the element.
+ *
+ * @param {boolean} submit
+ * Set to true to submit the form after the task is done, false otherwise.
+ */
+async function focusUpdateSubmitForm(target, args, submit = true) {
+ let fieldsIdentifiedPromiseResolver;
+ let fieldsIdentifiedObserver = {
+ fieldsIdentified() {
+ FormAutofillParent.removeMessageObserver(fieldsIdentifiedObserver);
+ fieldsIdentifiedPromiseResolver();
+ },
+ };
+
+ let fieldsIdentifiedPromise = new Promise(resolve => {
+ fieldsIdentifiedPromiseResolver = resolve;
+ FormAutofillParent.addMessageObserver(fieldsIdentifiedObserver);
+ });
+
+ let alreadyFocused = await SpecialPowers.spawn(target, [args], obj => {
+ let focused = false;
+
+ let form;
+ if (obj.formSelector) {
+ form = content.document.querySelector(obj.formSelector);
+ } else {
+ form = content.document.getElementById(obj.formId ?? "form");
+ }
+ let element = form.querySelector(obj.focusSelector);
+ if (element != content.document.activeElement) {
+ info(`focus on element (id=${element.id})`);
+ element.focus();
+ } else {
+ focused = true;
+ }
+
+ for (const [selector, value] of Object.entries(obj.newValues)) {
+ element = form.querySelector(selector);
+ if (content.HTMLInputElement.isInstance(element)) {
+ element.setUserInput(value);
+ } else {
+ element.value = value;
+ }
+ }
+
+ return focused;
+ });
+
+ if (alreadyFocused) {
+ // If the element is already focused, assume the FieldsIdentified message
+ // was sent before.
+ FormAutofillParent.removeMessageObserver(fieldsIdentifiedObserver);
+ fieldsIdentifiedPromiseResolver();
+ }
+
+ await fieldsIdentifiedPromise;
+
+ if (submit) {
+ await SpecialPowers.spawn(target, [args], obj => {
+ let form;
+ if (obj.formSelector) {
+ form = content.document.querySelector(obj.formSelector);
+ } else {
+ form = content.document.getElementById(obj.formId ?? "form");
+ }
+ info(`submit form (id=${form.id})`);
+ form.querySelector("input[type=submit]").click();
+ });
+ }
+}
+
+async function focusAndWaitForFieldsIdentified(browserOrContext, selector) {
+ info("expecting the target input being focused and identified");
+ /* eslint no-shadow: ["error", { "allow": ["selector", "previouslyFocused", "previouslyIdentified"] }] */
+
+ // If the input is previously focused, no more notifications will be
+ // sent as the notification goes along with focus event.
+ let fieldsIdentifiedPromiseResolver;
+ let fieldsIdentifiedObserver = {
+ fieldsIdentified() {
+ fieldsIdentifiedPromiseResolver();
+ },
+ };
+
+ let fieldsIdentifiedPromise = new Promise(resolve => {
+ fieldsIdentifiedPromiseResolver = resolve;
+ FormAutofillParent.addMessageObserver(fieldsIdentifiedObserver);
+ });
+
+ const { previouslyFocused, previouslyIdentified } = await SpecialPowers.spawn(
+ browserOrContext,
+ [selector],
+ async function (selector) {
+ const { FormLikeFactory } = ChromeUtils.importESModule(
+ "resource://gre/modules/FormLikeFactory.sys.mjs"
+ );
+ const input = content.document.querySelector(selector);
+ const rootElement = FormLikeFactory.findRootForField(input);
+ const previouslyFocused = content.document.activeElement == input;
+ const previouslyIdentified = rootElement.hasAttribute(
+ "test-formautofill-identified"
+ );
+
+ input.focus();
+
+ return { previouslyFocused, previouslyIdentified };
+ }
+ );
+
+ // Only wait for the fields identified notification if the
+ // focus was not previously assigned to the input.
+ if (previouslyFocused) {
+ fieldsIdentifiedPromiseResolver();
+ } else {
+ info("!previouslyFocused");
+ }
+
+ // If a browsing context was supplied, focus its parent frame as well.
+ if (
+ BrowsingContext.isInstance(browserOrContext) &&
+ browserOrContext.parent != browserOrContext
+ ) {
+ await SpecialPowers.spawn(
+ browserOrContext.parent,
+ [browserOrContext],
+ async function (browsingContext) {
+ browsingContext.embedderElement.focus();
+ }
+ );
+ }
+
+ if (previouslyIdentified) {
+ info("previouslyIdentified");
+ FormAutofillParent.removeMessageObserver(fieldsIdentifiedObserver);
+ return;
+ }
+
+ // Wait 500ms to ensure that "markAsAutofillField" is completely finished.
+ await fieldsIdentifiedPromise;
+ info("FieldsIdentified");
+ FormAutofillParent.removeMessageObserver(fieldsIdentifiedObserver);
+
+ await sleep();
+ await SpecialPowers.spawn(browserOrContext, [], async function () {
+ const { FormLikeFactory } = ChromeUtils.importESModule(
+ "resource://gre/modules/FormLikeFactory.sys.mjs"
+ );
+ FormLikeFactory.findRootForField(
+ content.document.activeElement
+ ).setAttribute("test-formautofill-identified", "true");
+ });
+}
+
+/**
+ * Run the task and wait until the autocomplete popup is opened.
+ *
+ * @param {object} browser A xul:browser.
+ * @param {Function} taskFn Task that will trigger the autocomplete popup
+ */
+async function runAndWaitForAutocompletePopupOpen(browser, taskFn) {
+ info("runAndWaitForAutocompletePopupOpen");
+ let popupShown = BrowserTestUtils.waitForPopupEvent(
+ browser.autoCompletePopup,
+ "shown"
+ );
+
+ // Run the task will open the autocomplete popup
+ await taskFn();
+
+ await popupShown;
+ await BrowserTestUtils.waitForMutationCondition(
+ browser.autoCompletePopup.richlistbox,
+ { childList: true, subtree: true, attributes: true },
+ () => {
+ const listItemElems = getDisplayedPopupItems(browser);
+ return (
+ !![...listItemElems].length &&
+ [...listItemElems].every(item => {
+ return (
+ (item.getAttribute("originaltype") == "autofill-profile" ||
+ item.getAttribute("originaltype") == "autofill-insecureWarning" ||
+ item.getAttribute("originaltype") == "autofill-clear-button" ||
+ item.getAttribute("originaltype") == "autofill-footer") &&
+ item.hasAttribute("formautofillattached")
+ );
+ })
+ );
+ }
+ );
+}
+
+async function waitForPopupEnabled(browser) {
+ const {
+ autoCompletePopup: { richlistbox: itemsBox },
+ } = browser;
+ info("Wait for list elements to become enabled");
+ await BrowserTestUtils.waitForMutationCondition(
+ itemsBox,
+ { subtree: true, attributes: true, attributeFilter: ["disabled"] },
+ () => !itemsBox.querySelectorAll(".autocomplete-richlistitem")[0].disabled
+ );
+}
+
+// Wait for the popup state change notification to happen in a child process.
+function waitPopupStateInChild(bc, messageName) {
+ return SpecialPowers.spawn(bc, [messageName], expectedMessage => {
+ return new Promise(resolve => {
+ const { AutoCompleteChild } = ChromeUtils.importESModule(
+ "resource://gre/actors/AutoCompleteChild.sys.mjs"
+ );
+
+ let listener = {
+ popupStateChanged: name => {
+ if (name != expectedMessage) {
+ info("Expected " + expectedMessage + " but received " + name);
+ return;
+ }
+
+ AutoCompleteChild.removePopupStateListener(listener);
+ resolve();
+ },
+ };
+ AutoCompleteChild.addPopupStateListener(listener);
+ });
+ });
+}
+
+async function openPopupOn(browser, selector) {
+ let childNotifiedPromise = waitPopupStateInChild(
+ browser,
+ "FormAutoComplete:PopupOpened"
+ );
+ await SimpleTest.promiseFocus(browser);
+
+ await runAndWaitForAutocompletePopupOpen(browser, async () => {
+ await focusAndWaitForFieldsIdentified(browser, selector);
+ if (!selector.includes("cc-")) {
+ info(`openPopupOn: before VK_DOWN on ${selector}`);
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+ }
+ });
+
+ await childNotifiedPromise;
+}
+
+async function openPopupOnSubframe(browser, frameBrowsingContext, selector) {
+ let childNotifiedPromise = waitPopupStateInChild(
+ frameBrowsingContext,
+ "FormAutoComplete:PopupOpened"
+ );
+
+ await SimpleTest.promiseFocus(browser);
+
+ await runAndWaitForAutocompletePopupOpen(browser, async () => {
+ await focusAndWaitForFieldsIdentified(frameBrowsingContext, selector);
+ if (!selector.includes("cc-")) {
+ info(`openPopupOnSubframe: before VK_DOWN on ${selector}`);
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, frameBrowsingContext);
+ }
+ });
+
+ await childNotifiedPromise;
+}
+
+async function closePopup(browser) {
+ // Return if the popup isn't open.
+ if (!browser.autoCompletePopup.popupOpen) {
+ return;
+ }
+
+ let childNotifiedPromise = waitPopupStateInChild(
+ browser,
+ "FormAutoComplete:PopupClosed"
+ );
+ let popupClosePromise = BrowserTestUtils.waitForPopupEvent(
+ browser.autoCompletePopup,
+ "hidden"
+ );
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.document.activeElement.blur();
+ });
+
+ await popupClosePromise;
+ await childNotifiedPromise;
+}
+
+async function closePopupForSubframe(browser, frameBrowsingContext) {
+ let childNotifiedPromise = waitPopupStateInChild(
+ browser,
+ "FormAutoComplete:PopupClosed"
+ );
+
+ let popupClosePromise = BrowserTestUtils.waitForPopupEvent(
+ browser.autoCompletePopup,
+ "hidden"
+ );
+
+ await SpecialPowers.spawn(frameBrowsingContext, [], async function () {
+ content.document.activeElement.blur();
+ });
+
+ await popupClosePromise;
+ await childNotifiedPromise;
+}
+
+function emulateMessageToBrowser(name, data) {
+ let actor =
+ gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
+ "FormAutofill"
+ );
+ return actor.receiveMessage({ name, data });
+}
+
+function getRecords(data) {
+ info(`expecting record retrievals: ${data.collectionName}`);
+ return emulateMessageToBrowser("FormAutofill:GetRecords", data).then(
+ result => result.records
+ );
+}
+
+function getAddresses() {
+ return getRecords({ collectionName: "addresses" });
+}
+
+async function ensureNoAddressSaved() {
+ await new Promise(resolve =>
+ setTimeout(resolve, TIMEOUT_ENSURE_PROFILE_NOT_SAVED)
+ );
+ const addresses = await getAddresses();
+ is(addresses.length, 0, "No address was saved");
+}
+
+function getCreditCards() {
+ return getRecords({ collectionName: "creditCards" });
+}
+
+async function saveAddress(address) {
+ info("expecting address saved");
+ let observePromise = TestUtils.topicObserved("formautofill-storage-changed");
+ await emulateMessageToBrowser("FormAutofill:SaveAddress", { address });
+ await observePromise;
+}
+
+async function saveCreditCard(creditcard) {
+ info("expecting credit card saved");
+ let creditcardClone = Object.assign({}, creditcard);
+ let observePromise = TestUtils.topicObserved("formautofill-storage-changed");
+ await emulateMessageToBrowser("FormAutofill:SaveCreditCard", {
+ creditcard: creditcardClone,
+ });
+ await observePromise;
+}
+
+async function removeAddresses(guids) {
+ info("expecting address removed");
+ let observePromise = TestUtils.topicObserved("formautofill-storage-changed");
+ await emulateMessageToBrowser("FormAutofill:RemoveAddresses", { guids });
+ await observePromise;
+}
+
+async function removeCreditCards(guids) {
+ info("expecting credit card removed");
+ let observePromise = TestUtils.topicObserved("formautofill-storage-changed");
+ await emulateMessageToBrowser("FormAutofill:RemoveCreditCards", { guids });
+ await observePromise;
+}
+
+function getNotification(index = 0) {
+ let notifications = PopupNotifications.panel.childNodes;
+ ok(!!notifications.length, "at least one notification displayed");
+ ok(true, notifications.length + " notification(s)");
+ return notifications[index];
+}
+
+function waitForPopupShown() {
+ return BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+}
+
+/**
+ * Clicks the popup notification button and wait for popup hidden.
+ *
+ * @param {string} buttonType The button type in popup notification.
+ * @param {number} index The action's index in menu list.
+ */
+async function clickDoorhangerButton(buttonType, index = 0) {
+ let popuphidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+
+ let button;
+ if (buttonType == MAIN_BUTTON || buttonType == SECONDARY_BUTTON) {
+ button = getNotification()[buttonType];
+ } else if (buttonType == MENU_BUTTON) {
+ // Click the dropmarker arrow and wait for the menu to show up.
+ info("expecting notification menu button present");
+ await BrowserTestUtils.waitForCondition(() => getNotification().menubutton);
+ await sleep(2000); // menubutton needs extra time for binding
+ let notification = getNotification();
+
+ ok(notification.menubutton, "notification menupopup displayed");
+ let dropdownPromise = BrowserTestUtils.waitForEvent(
+ notification.menupopup,
+ "popupshown"
+ );
+
+ notification.menubutton.click();
+ info("expecting notification popup show up");
+ await dropdownPromise;
+
+ button = notification.querySelectorAll("menuitem")[index];
+ }
+
+ button.click();
+ info("expecting notification popup hidden");
+ await popuphidden;
+}
+
+async function clickAddressDoorhangerButton(buttonType, subType) {
+ const notification = getNotification();
+ let button;
+ if (buttonType == EDIT_ADDRESS_BUTTON) {
+ button = AddressSaveDoorhanger.editButton(notification);
+ } else if (buttonType == ADDRESS_MENU_BUTTON) {
+ const menu = AutofillDoorhanger.menuButton(notification);
+ const menupopup = AutofillDoorhanger.menuPopup(notification);
+ const promise = BrowserTestUtils.waitForEvent(menupopup, "popupshown");
+ menu.click();
+ await promise;
+ if (subType == ADDRESS_MENU_PREFENCE) {
+ button = AutofillDoorhanger.preferenceButton(notification);
+ } else if (subType == ADDRESS_MENU_LEARN_MORE) {
+ button = AutofillDoorhanger.learnMoreButton(notification);
+ }
+ } else {
+ await clickDoorhangerButton(buttonType);
+ return;
+ }
+
+ EventUtils.synthesizeMouseAtCenter(button, {});
+}
+
+function getDoorhangerCheckbox() {
+ return getNotification().checkbox;
+}
+
+function getDoorhangerButton(button) {
+ return getNotification()[button];
+}
+
+/**
+ * Removes all addresses and credit cards from storage.
+ *
+ * **NOTE: If you add or update a record in a test, then you must wait for the
+ * respective storage event to fire before calling this function.**
+ * This is because this function doesn't guarantee that a record that
+ * is about to be added or update will also be removed,
+ * since the add or update is triggered by an asynchronous call.
+ *
+ * @see waitForStorageChangedEvents for more details about storage events to wait for
+ */
+async function removeAllRecords() {
+ let addresses = await getAddresses();
+ if (addresses.length) {
+ await removeAddresses(addresses.map(address => address.guid));
+ }
+ let creditCards = await getCreditCards();
+ if (creditCards.length) {
+ await removeCreditCards(creditCards.map(cc => cc.guid));
+ }
+}
+
+async function waitForFocusAndFormReady(win) {
+ return Promise.all([
+ new Promise(resolve => waitForFocus(resolve, win)),
+ BrowserTestUtils.waitForEvent(win, "FormReady"),
+ ]);
+}
+
+// Verify that the warning in the autocomplete popup has the expected text.
+async function expectWarningText(browser, expectedText) {
+ const {
+ autoCompletePopup: { richlistbox: itemsBox },
+ } = browser;
+ let warningBox = itemsBox.querySelector(
+ ".autocomplete-richlistitem:last-child"
+ );
+
+ while (warningBox.collapsed) {
+ warningBox = warningBox.previousSibling;
+ }
+ warningBox = warningBox._warningTextBox;
+
+ await BrowserTestUtils.waitForMutationCondition(
+ warningBox,
+ { childList: true, characterData: true },
+ () => warningBox.textContent == expectedText
+ );
+ ok(true, `Got expected warning text: ${expectedText}`);
+}
+
+async function testDialog(url, testFn, arg = undefined) {
+ // Skip this step for test cards that lack an encrypted
+ // number since they will fail to decrypt.
+ if (
+ url == EDIT_CREDIT_CARD_DIALOG_URL &&
+ arg &&
+ arg.record &&
+ arg.record["cc-number-encrypted"]
+ ) {
+ arg.record = Object.assign({}, arg.record, {
+ "cc-number": await OSKeyStore.decrypt(arg.record["cc-number-encrypted"]),
+ });
+ }
+ let win = window.openDialog(url, null, "width=600,height=600", arg);
+ await waitForFocusAndFormReady(win);
+ let unloadPromise = BrowserTestUtils.waitForEvent(win, "unload");
+ await testFn(win);
+ return unloadPromise;
+}
+
+/**
+ * Initializes the test storage for a task.
+ *
+ * @param {...object} items Can either be credit card or address objects
+ */
+async function setStorage(...items) {
+ for (let item of items) {
+ if (item["cc-number"]) {
+ await saveCreditCard(item);
+ } else {
+ await saveAddress(item);
+ }
+ }
+}
+
+function verifySectionAutofillResult(sections, expectedSectionsInfo) {
+ sections.forEach((section, index) => {
+ const expectedSection = expectedSectionsInfo[index];
+
+ const fieldDetails = section.fieldDetails;
+ const expectedFieldDetails = expectedSection.fields;
+
+ info(`verify autofill section[${index}]`);
+
+ fieldDetails.forEach((field, fieldIndex) => {
+ const expeceted = expectedFieldDetails[fieldIndex];
+
+ Assert.equal(
+ field.element.value,
+ expeceted.autofill ?? "",
+ `Autofilled value for element(id=${field.element.id}, field name=${field.fieldName}) should be equal`
+ );
+ });
+ });
+}
+
+function verifySectionFieldDetails(sections, expectedSectionsInfo) {
+ sections.forEach((section, index) => {
+ const expectedSection = expectedSectionsInfo[index];
+
+ const fieldDetails = section.fieldDetails;
+ const expectedFieldDetails = expectedSection.fields;
+
+ info(`section[${index}] ${expectedSection.description ?? ""}:`);
+ info(`FieldName Prediction Results: ${fieldDetails.map(i => i.fieldName)}`);
+ info(
+ `FieldName Expected Results: ${expectedFieldDetails.map(
+ detail => detail.fieldName
+ )}`
+ );
+ Assert.equal(
+ fieldDetails.length,
+ expectedFieldDetails.length,
+ `Expected field count.`
+ );
+
+ fieldDetails.forEach((field, fieldIndex) => {
+ const expectedFieldDetail = expectedFieldDetails[fieldIndex];
+
+ const expected = {
+ ...{
+ reason: "autocomplete",
+ section: "",
+ contactType: "",
+ addressType: "",
+ credentialType: "",
+ },
+ ...expectedSection.default,
+ ...expectedFieldDetail,
+ };
+
+ const keys = new Set([...Object.keys(field), ...Object.keys(expected)]);
+ [
+ "identifier",
+ "autofill",
+ "elementWeakRef",
+ "confidence",
+ "part",
+ ].forEach(k => keys.delete(k));
+
+ for (const key of keys) {
+ const expectedValue = expected[key];
+ const actualValue = field[key];
+ Assert.equal(
+ actualValue,
+ expectedValue,
+ `${key} should be equal, expect ${expectedValue}, got ${actualValue}`
+ );
+ }
+ });
+
+ Assert.equal(
+ section.isValidSection(),
+ !expectedSection.invalid,
+ `Should be an ${expectedSection.invalid ? "invalid" : "valid"} section`
+ );
+ });
+}
+
+/**
+ * Discards all recorded Glean telemetry in parent and child processes
+ * and resets FOG and the Glean SDK.
+ *
+ * @param {boolean} onlyInParent Whether we only discard the metric data in the parent process
+ *
+ * Since the current method Services.fog.testResetFOG only discards metrics recorded in the parent process,
+ * we would like to keep this option in our method as well.
+ */
+async function clearGleanTelemetry(onlyInParent = false) {
+ if (!onlyInParent) {
+ await Services.fog.testFlushAllChildren();
+ }
+ Services.fog.testResetFOG();
+}
+
+/**
+ * Runs heuristics test for form autofill on given patterns.
+ *
+ * @param {Array<object>} patterns - An array of test patterns to run the heuristics test on.
+ * @param {string} pattern.description - Description of this heuristic test
+ * @param {string} pattern.fixurePath - The path of the test document
+ * @param {string} pattern.fixureData - Test document by string. Use either fixurePath or fixtureData.
+ * @param {object} pattern.profile - The profile to autofill. This is required only when running autofill test
+ * @param {Array} pattern.expectedResult - The expected result of this heuristic test. See below for detailed explanation
+ *
+ * @param {string} [fixturePathPrefix=""] - The prefix to the path of fixture files.
+ * @param {object} [options={ testAutofill: false }] - An options object containing additional configuration for running the test.
+ * @param {boolean} [options.testAutofill=false] - A boolean indicating whether to run the test for autofill or not.
+ * @returns {Promise} A promise that resolves when all the tests are completed.
+ *
+ * The `patterns.expectedResult` array contains test data for different address or credit card sections.
+ * Each section in the array is represented by an object and can include the following properties:
+ * - description (optional): A string describing the section, primarily used for debugging purposes.
+ * - default (optional): An object that sets the default values for all the fields within this section.
+ * The default object contains the same keys as the individual field objects.
+ * - fields: An array of field details (class FieldDetails) within the section.
+ *
+ * Each field object can have the following keys:
+ * - fieldName: The name of the field (e.g., "street-name", "cc-name" or "cc-number").
+ * - reason: The reason for the field value (e.g., "autocomplete", "regex-heuristic" or "fathom").
+ * - section: The section to which the field belongs (e.g., "billing", "shipping").
+ * - part: The part of the field.
+ * - contactType: The contact type of the field.
+ * - addressType: The address type of the field.
+ * - autofill: Set the expected autofill value when running autofill test
+ *
+ * For more information on the field object properties, refer to the FieldDetails class.
+ *
+ * Example test data:
+ * add_heuristic_tests(
+ * [{
+ * description: "first test pattern",
+ * fixuturePath: "autocomplete_off.html",
+ * profile: {organization: "Mozilla", country: "US", tel: "123"},
+ * expectedResult: [
+ * {
+ * description: "First section"
+ * fields: [
+ * { fieldName: "organization", reason: "autocomplete", autofill: "Mozilla" },
+ * { fieldName: "country", reason: "regex-heuristic", autofill: "US" },
+ * { fieldName: "tel", reason: "regex-heuristic", autofill: "123" },
+ * ]
+ * },
+ * {
+ * default: {
+ * reason: "regex-heuristic",
+ * section: "billing",
+ * },
+ * fields: [
+ * { fieldName: "cc-number", reason: "fathom" },
+ * { fieldName: "cc-nane" },
+ * { fieldName: "cc-exp" },
+ * ],
+ * }],
+ * },
+ * {
+ * // second test pattern //
+ * }
+ * ],
+ * "/fixturepath",
+ * {testAutofill: true} // test options
+ * )
+ */
+
+async function add_heuristic_tests(
+ patterns,
+ fixturePathPrefix = "",
+ options = { testAutofill: false }
+) {
+ async function runTest(testPattern) {
+ const TEST_URL = testPattern.fixtureData
+ ? `data:text/html,${testPattern.fixtureData}`
+ : `${BASE_URL}../${fixturePathPrefix}${testPattern.fixturePath}`;
+
+ if (testPattern.fixtureData) {
+ info(`Starting test with fixture data`);
+ } else {
+ info(`Starting test fixture: ${testPattern.fixturePath ?? ""}`);
+ }
+
+ if (testPattern.description) {
+ info(`Test "${testPattern.description}"`);
+ }
+
+ if (testPattern.prefs) {
+ await SpecialPowers.pushPrefEnv({
+ set: testPattern.prefs,
+ });
+ }
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ testPattern,
+ verifySection: verifySectionFieldDetails.toString(),
+ verifyAutofill: options.testAutofill
+ ? verifySectionAutofillResult.toString()
+ : null,
+ },
+ ],
+ async obj => {
+ const { FormLikeFactory } = ChromeUtils.importESModule(
+ "resource://gre/modules/FormLikeFactory.sys.mjs"
+ );
+ const { FormAutofillHandler } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillHandler.sys.mjs"
+ );
+
+ const elements = Array.from(
+ content.document.querySelectorAll("input, select")
+ );
+
+ // Bug 1834768. We should simulate user behavior instead of
+ // using internal APIs.
+ const forms = elements.reduce((acc, element) => {
+ const formLike = FormLikeFactory.createFromField(element);
+ if (!acc.some(form => form.rootElement === formLike.rootElement)) {
+ acc.push(formLike);
+ }
+ return acc;
+ }, []);
+
+ const sections = forms.flatMap(form => {
+ const handler = new FormAutofillHandler(form);
+ handler.collectFormFields(false /* ignoreInvalid */);
+ return handler.sections;
+ });
+
+ Assert.equal(
+ sections.length,
+ obj.testPattern.expectedResult.length,
+ "Expected section count."
+ );
+
+ // eslint-disable-next-line no-eval
+ let verify = eval(`(() => {return (${obj.verifySection});})();`);
+ verify(sections, obj.testPattern.expectedResult);
+
+ if (obj.verifyAutofill) {
+ for (const section of sections) {
+ if (!section.isValidSection()) {
+ continue;
+ }
+
+ section.focusedInput = section.fieldDetails[0].element;
+ await section.autofillFields(
+ section.getAdaptedProfiles([obj.testPattern.profile])[0]
+ );
+ }
+
+ // eslint-disable-next-line no-eval
+ verify = eval(`(() => {return (${obj.verifyAutofill});})();`);
+ verify(sections, obj.testPattern.expectedResult);
+ }
+ }
+ );
+ });
+
+ if (testPattern.prefs) {
+ await SpecialPowers.popPrefEnv();
+ }
+ }
+
+ patterns.forEach(testPattern => {
+ add_task(() => runTest(testPattern));
+ });
+}
+
+async function add_autofill_heuristic_tests(patterns, fixturePathPrefix = "") {
+ add_heuristic_tests(patterns, fixturePathPrefix, { testAutofill: true });
+}
+
+function fillEditDoorhanger(record) {
+ const notification = getNotification();
+
+ for (const [key, value] of Object.entries(record)) {
+ const id = AddressEditDoorhanger.getInputId(key);
+ const element = notification.querySelector(`#${id}`);
+ element.value = value;
+ }
+}
+
+// TODO: This function should be removed. We should make normalizeFields in
+// FormAutofillStorageBase.sys.mjs static and using it directly
+function normalizeAddressFields(record) {
+ let normalized = { ...record };
+
+ if (normalized.name != undefined) {
+ let nameParts = FormAutofillNameUtils.splitName(normalized.name);
+ normalized["given-name"] = nameParts.given;
+ normalized["additional-name"] = nameParts.middle;
+ normalized["family-name"] = nameParts.family;
+ delete normalized.name;
+ }
+ return normalized;
+}
+
+async function verifyConfirmationHint(
+ browser,
+ forceClose,
+ anchorID = "identity-icon-box"
+) {
+ let hintElem = browser.ownerGlobal.ConfirmationHint._panel;
+ await BrowserTestUtils.waitForPopupEvent(hintElem, "shown");
+ try {
+ Assert.equal(hintElem.state, "open", "hint popup is open");
+ Assert.ok(
+ BrowserTestUtils.isVisible(hintElem.anchorNode),
+ "hint anchorNode is visible"
+ );
+ Assert.equal(
+ hintElem.anchorNode.id,
+ anchorID,
+ "Hint should be anchored on the expected notification icon"
+ );
+ info("verifyConfirmationHint, hint is shown and has its anchorNode");
+ if (forceClose) {
+ await closePopup(hintElem);
+ } else {
+ info("verifyConfirmationHint, assertion ok, wait for poopuphidden");
+ await BrowserTestUtils.waitForPopupEvent(hintElem, "hidden");
+ info("verifyConfirmationHint, hintElem popup is hidden");
+ }
+ } catch (ex) {
+ Assert.ok(false, "Confirmation hint not shown: " + ex.message);
+ } finally {
+ info("verifyConfirmationHint promise finalized");
+ }
+}
+
+async function showAddressDoorhanger(browser, values = null) {
+ const defaultValues = {
+ "#given-name": "John",
+ "#family-name": "Doe",
+ "#organization": "Mozilla",
+ "#street-address": "123 Sesame Street",
+ };
+
+ const onPopupShown = waitForPopupShown();
+ const promise = BrowserTestUtils.browserLoaded(browser);
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#given-name",
+ newValues: values ?? defaultValues,
+ });
+ await promise;
+ await onPopupShown;
+}
+
+add_setup(function () {
+ OSKeyStoreTestUtils.setup();
+});
+
+registerCleanupFunction(async () => {
+ await removeAllRecords();
+ await OSKeyStoreTestUtils.cleanup();
+});
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser.toml b/browser/extensions/formautofill/test/browser/heuristics/browser.toml
new file mode 100644
index 0000000000..e7bbfa0283
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser.toml
@@ -0,0 +1,39 @@
+[DEFAULT]
+skip-if = ["os == 'android'"] # bug 1730213
+support-files = [
+ "../head.js",
+ "../../fixtures/**",
+]
+
+["browser_autocomplete_off_on_form.js"]
+
+["browser_autocomplete_off_on_inputs.js"]
+
+["browser_basic.js"]
+
+["browser_capture_name.js"]
+skip-if = ["apple_silicon && !debug"]
+
+["browser_cc_exp.js"]
+
+["browser_de_fields.js"]
+
+["browser_fr_fields.js"]
+
+["browser_ignore_unfocusable_fields.js"]
+
+["browser_label_rules.js"]
+
+["browser_multiple_section.js"]
+
+["browser_parse_address_fields.js"]
+
+["browser_parse_creditcard_expiry_fields.js"]
+
+["browser_parse_name_fields.js"]
+
+["browser_parse_street_address_fields.js"]
+
+["browser_section_validation_address.js"]
+
+["browser_sections_by_name.js"]
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_form.js b/browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_form.js
new file mode 100644
index 0000000000..ccc93bc539
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_form.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* global add_heuristic_tests */
+
+// Ensures that fields are identified correctly even when the containing form
+// has its autocomplete attribute set to off.
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "autocomplete_off_on_form.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "organization" },
+ { fieldName: "street-address" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "organization" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2", reason: "update-heuristic" },
+ { fieldName: "address-line3", reason: "update-heuristic" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "cc-number" },
+ { fieldName: "cc-name" },
+ { fieldName: "cc-exp-month" },
+ { fieldName: "cc-exp-year" },
+ ],
+ },
+ {
+ invalid: true,
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "address-line1" },
+ { fieldName: "address-level2" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "address-line1", reason: "update-heuristic" },
+ { fieldName: "organization", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [{ fieldName: "address-line1", reason: "update-heuristic" }],
+ },
+ ],
+ },
+ ],
+ "fixtures/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_inputs.js b/browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_inputs.js
new file mode 100644
index 0000000000..d57ef8dc82
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_autocomplete_off_on_inputs.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* global add_heuristic_tests */
+
+// Ensures that fields are identified correctly even when the inputs
+// have their autocomplete attribute set to off.
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "autocomplete_off_on_inputs.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "organization" },
+ { fieldName: "street-address" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "organization" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2", reason: "update-heuristic" },
+ { fieldName: "address-line3", reason: "update-heuristic" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "cc-number", reason: "fathom" },
+ { fieldName: "cc-name", reason: "fathom" },
+ { fieldName: "cc-exp-month" },
+ { fieldName: "cc-exp-year" },
+ ],
+ },
+ {
+ invalid: true,
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "address-line1" },
+ { fieldName: "address-level2" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "address-line1", reason: "update-heuristic" },
+ { fieldName: "organization", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [{ fieldName: "address-line1", reason: "update-heuristic" }],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "organization" },
+ { fieldName: "address-line1", reason: "regex-heuristic" },
+ { fieldName: "address-line2", reason: "update-heuristic" },
+ { fieldName: "address-line3", reason: "update-heuristic" },
+ { fieldName: "address-level2", reason: "regex-heuristic" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code", reason: "regex-heuristic" },
+ { fieldName: "country", reason: "regex-heuristic" },
+ { fieldName: "tel" },
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "cc-number", reason: "fathom" },
+ { fieldName: "cc-name" },
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_basic.js b/browser/extensions/formautofill/test/browser/heuristics/browser_basic.js
new file mode 100644
index 0000000000..59447ac177
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_basic.js
@@ -0,0 +1,78 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "autocomplete_basic.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "organization" },
+ { fieldName: "street-address" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "organization" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2", reason: "update-heuristic" },
+ { fieldName: "address-line3", reason: "update-heuristic" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "cc-number" },
+ { fieldName: "cc-name" },
+ { fieldName: "cc-exp-month" },
+ { fieldName: "cc-exp-year" },
+ ],
+ },
+ {
+ invalid: true,
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "address-line1" },
+ { fieldName: "address-level2" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "address-line1", reason: "update-heuristic" },
+ { fieldName: "organization", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [{ fieldName: "address-line1", reason: "update-heuristic" }],
+ },
+ ],
+ },
+ ],
+ "fixtures/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_capture_name.js b/browser/extensions/formautofill/test/browser/heuristics/browser_capture_name.js
new file mode 100644
index 0000000000..2838d2d25e
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_capture_name.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from ../head.js */
+
+"use strict";
+
+const TESTCASES = [
+ {
+ description: `cc fields + first name with autocomplete attribute`,
+ document: `<form id="form">
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-exp" autocomplete="cc-exp">
+ <input id="name" autocomplete="given-name">
+ <input type="submit"/>
+ </form>`,
+ fillValues: {
+ "#cc-number": "4111111111111111",
+ "#cc-exp": "12/24",
+ "#name": "John Doe",
+ },
+ expected: undefined,
+ },
+ {
+ description: `cc fields + first name without autocomplete attribute`,
+ document: `<form id="form">
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-exp" autocomplete="cc-exp">
+ <input id="name" placeholder="given-name">
+ <input type="submit"/>
+ </form>`,
+ fillValues: {
+ "#cc-number": "4111111111111111",
+ "#cc-exp": "12/24",
+ "#name": "John Doe",
+ },
+ expected: "John Doe",
+ },
+ {
+ description: `cc fields + first and last name with autocomplete attribute`,
+ document: `<form id="form">
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-exp" autocomplete="cc-exp">
+ <input id="given" autocomplete="given-name">
+ <input id="family" autocomplete="family-name">
+ <input type="submit"/>
+ </form>`,
+ fillValues: {
+ "#cc-number": "4111111111111111",
+ "#cc-exp": "12/24",
+ "#given": "John",
+ "#family": "Doe",
+ },
+ expected: undefined,
+ },
+ {
+ description: `cc fields + first and last name without autocomplete attribute`,
+ document: `<form id="form">
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-exp" autocomplete="cc-exp">
+ <input id="given" placeholder="given-name">
+ <input id="family" placeholder="family-name">
+ <input type="submit"/>
+ </form>`,
+ fillValues: {
+ "#cc-number": "4111111111111111",
+ "#cc-exp": "12/24",
+ "#given": "John",
+ "#family": "Doe",
+ },
+ expected: "John Doe",
+ },
+ {
+ description: `cc fields + cc-name + first and last name`,
+ document: `<form id="form">
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-exp" autocomplete="cc-exp">
+ <input id="given" placeholder="given-name">
+ <input id="family" placeholder="family-name">
+ <input type="submit"/>
+ </form>`,
+ fillValues: {
+ "#cc-number": "4111111111111111",
+ "#cc-name": "Jane Poe",
+ "#cc-exp": "12/24",
+ "#given": "John",
+ "#family": "Doe",
+ },
+ expected: "Jane Poe",
+ },
+ {
+ description: `first and last name + cc fields`,
+ document: `<form id="form">
+ <input id="given" placeholder="given-name">
+ <input id="family" placeholder="family-name">
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-exp" autocomplete="cc-exp">
+ <input type="submit"/>
+ </form>`,
+ fillValues: {
+ "#cc-number": "4111111111111111",
+ "#cc-exp": "12/24",
+ "#given": "John",
+ "#family": "Doe",
+ },
+ expected: undefined,
+ },
+];
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.creditCards.supported", "on"],
+ ["extensions.formautofill.creditCards.enabled", true],
+ ],
+ });
+});
+
+add_task(async function test_save_doorhanger_click_save() {
+ for (const TEST of TESTCASES) {
+ info(`Test ${TEST.description}`);
+ let onChanged = waitForStorageChangedEvents("add");
+ await BrowserTestUtils.withNewTab(EMPTY_URL, async function (browser) {
+ await SpecialPowers.spawn(browser, [TEST.document], doc => {
+ content.document.body.innerHTML = doc;
+ });
+
+ await SimpleTest.promiseFocus(browser);
+
+ let onPopupShown = waitForPopupShown();
+
+ await focusUpdateSubmitForm(browser, {
+ focusSelector: "#cc-number",
+ newValues: TEST.fillValues,
+ });
+
+ await onPopupShown;
+ await clickDoorhangerButton(MAIN_BUTTON);
+ });
+
+ await onChanged;
+
+ let creditCards = await getCreditCards();
+ is(creditCards.length, 1, "1 credit card in storage");
+ is(creditCards[0]["cc-name"], TEST.expected, "Verify the name field");
+ await removeAllRecords();
+ }
+});
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_cc_exp.js b/browser/extensions/formautofill/test/browser/heuristics/browser_cc_exp.js
new file mode 100644
index 0000000000..fefeaac606
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_cc_exp.js
@@ -0,0 +1,56 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "heuristics_cc_exp.html",
+ expectedResult: [
+ {
+ description: "form1",
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "cc-name" },
+ { fieldName: "cc-number" },
+ { fieldName: "cc-exp-month" },
+ { fieldName: "cc-exp-year" },
+ ],
+ },
+ {
+ description: "form2",
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [{ fieldName: "cc-number" }, { fieldName: "cc-exp" }],
+ },
+ {
+ description: "form3",
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-exp", reason: "update-heuristic" },
+ ],
+ },
+ {
+ description: "form4",
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ description: "form5",
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "update-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_de_fields.js b/browser/extensions/formautofill/test/browser/heuristics/browser_de_fields.js
new file mode 100644
index 0000000000..0d47cb6935
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_de_fields.js
@@ -0,0 +1,32 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "heuristics_de_fields.html",
+ expectedResult: [
+ {
+ fields: [
+ { fieldName: "cc-name", reason: "fathom" },
+ { fieldName: "cc-type", reason: "regex-heuristic" },
+ { fieldName: "cc-number", reason: "fathom" },
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ fields: [
+ { fieldName: "cc-name", reason: "fathom" },
+ { fieldName: "cc-type", reason: "regex-heuristic" },
+ { fieldName: "cc-number", reason: "fathom" },
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_fr_fields.js b/browser/extensions/formautofill/test/browser/heuristics/browser_fr_fields.js
new file mode 100644
index 0000000000..b43d1b1e2a
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_fr_fields.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "heuristics_fr_fields.html",
+ expectedResult: [
+ {
+ fields: [
+ { fieldName: "cc-number", reason: "fathom" },
+ { fieldName: "cc-exp", reason: "update-heuristic" },
+ { fieldName: "cc-name", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_ignore_unfocusable_fields.js b/browser/extensions/formautofill/test/browser/heuristics/browser_ignore_unfocusable_fields.js
new file mode 100644
index 0000000000..56f53a1e76
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_ignore_unfocusable_fields.js
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests([
+ {
+ description: "All visual fields are considered focusable.",
+
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="name" autocomplete="name" />
+ <input type="text" id="tel" autocomplete="tel" />
+ <input type="text" id="email" autocomplete="email"/>
+ <select id="country" autocomplete="country">
+ <option value="United States">United States</option>
+ </select>
+ <input type="text" id="postal-code" autocomplete="postal-code"/>
+ <input type="text" id="address-line1" autocomplete="address-line1" />
+ <div>
+ <input type="text" id="address-line2" autocomplete="address-line2" />
+ </div>
+ </form>
+ </form>
+ </body>
+ </html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "name" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ { fieldName: "country" },
+ { fieldName: "postal-code" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ ],
+ },
+ ],
+ },
+ {
+ // ignore opacity (see Bug 1835852),
+ description:
+ "Invisible fields with style.opacity=0 set are considered focusable.",
+
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="name" autocomplete="name" style="opacity:0" />
+ <input type="text" id="tel" autocomplete="tel" />
+ <input type="text" id="email" autocomplete="email" style="opacity:0"/>
+ <select id="country" autocomplete="country">
+ <option value="United States">United States</option>
+ </select>
+ <input type="text" id="postal-code" autocomplete="postal-code" />
+ <input type="text" id="address-line1" autocomplete="address-line1" />
+ <div>
+ <input type="text" id="address-line2" autocomplete="address-line2" />
+ </div>
+ </form>
+ </form>
+ </body>
+ </html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "name" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ { fieldName: "country" },
+ { fieldName: "postal-code" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ ],
+ },
+ ],
+ },
+ {
+ description:
+ "Some fields are considered unfocusable due to their invisibility.",
+
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="name" autocomplete="name" />
+ <input type="text" id="tel" autocomplete="tel" />
+ <input type="text" id="email" autocomplete="email" />
+ <input type="text" id="country" autocomplete="country" />
+ <input type="text" id="postal-code" autocomplete="postal-code" hidden />
+ <input type="text" id="address-line1" autocomplete="address-line1" style="display:none" />
+ <div style="visibility: hidden">
+ <input type="text" id="address-line2" autocomplete="address-line2" />
+ </div>
+ </form>
+ </body>
+ </html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "name" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ { fieldName: "country" },
+ ],
+ },
+ ],
+ },
+ {
+ description: `Disabled field and field with tabindex="-1" is considered unfocusable`,
+
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="name" autocomplete="name" />
+ <input type="text" id="tel" autocomplete="tel" />
+ <input type="text" id="email" autocomplete="email" />
+ <input type="text" id="country" autocomplete="country" disabled/>
+ <input type="text" id="postal-code" autocomplete="postal-code" tabindex="-1"/>
+ <input type="text" id="address-line1" autocomplete="address-line1" />
+ <input type="text" id="address-line2" autocomplete="address-line2" />
+ </form>
+ </body>
+ </html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "name" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ ],
+ },
+ ],
+ },
+]);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_label_rules.js b/browser/extensions/formautofill/test/browser/heuristics/browser_label_rules.js
new file mode 100644
index 0000000000..16b5e8a56b
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_label_rules.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests([
+ {
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="name" autocomplete="name"/>
+ <input type="text" id="country" autocomplete="country"/>
+ <label for="test1">sender-address</label>
+ <input type="text" id="test1"/>
+ <input type="text" id="test2" name="sender-address"/>
+ </form>
+ </body>
+ </html>`,
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ description: `Only "sender-address" keywords in labels"`,
+ fields: [
+ { fieldName: "name", reason: "autocomplete" },
+ { fieldName: "country", reason: "autocomplete" },
+ { fieldName: "address-line1" },
+ ],
+ },
+ ],
+ },
+ {
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="name" autocomplete="name"/>
+ <input type="text" id="country" autocomplete="country"/>
+ <input type="text" id="test" aria-label="street-address"/>
+ </form>
+ </body>
+ </html>`,
+ expectedResult: [
+ {
+ description: `keywords are in aria-label`,
+ fields: [
+ { fieldName: "name", reason: "autocomplete" },
+ { fieldName: "country", reason: "autocomplete" },
+ { fieldName: "street-address", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+]);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_multiple_section.js b/browser/extensions/formautofill/test/browser/heuristics/browser_multiple_section.js
new file mode 100644
index 0000000000..078f2240db
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_multiple_section.js
@@ -0,0 +1,118 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "multiple_section.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ addressType: "shipping",
+ },
+ fields: [
+ { fieldName: "name", addressType: "" },
+ { fieldName: "organization", addressType: "" },
+ { fieldName: "street-address" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ addressType: "billing",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ section: "section-my",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ { fieldName: "tel", section: "", contactType: "work" },
+ { fieldName: "email", section: "", contactType: "work" },
+ ],
+ },
+ {
+ invalid: true,
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ // Even the `contactType` of these two fields are different with the
+ // above two, we still consider they are identical until supporting
+ // multiple phone number and email in one profile.
+ { fieldName: "tel", contactType: "home" },
+ { fieldName: "email", contactType: "home" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "name" },
+ { fieldName: "organization" },
+ { fieldName: "street-address" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ { fieldName: "tel", contactType: "work" },
+ { fieldName: "email", contactType: "work" },
+ ],
+ },
+ {
+ invalid: true,
+ default: {
+ reason: "autocomplete",
+ contactType: "home",
+ },
+ fields: [{ fieldName: "tel" }, { fieldName: "email" }],
+ },
+ ],
+ },
+ ],
+ "fixtures/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_parse_address_fields.js b/browser/extensions/formautofill/test/browser/heuristics/browser_parse_address_fields.js
new file mode 100644
index 0000000000..e602173ad5
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_parse_address_fields.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests([
+ {
+ description:
+ "When a <select> is next to address-level2, we assume the select field is address-level1",
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="name" autocomplete="name" />
+ <input type="text" id="country" autocomplete="country"/>
+ <select><option value="test">test</option></select>
+ <input type="text" id="address-level2" autocomplete="address-level2" />
+ </form>
+ <form>
+ <input type="text" id="name" autocomplete="name" />
+ <input type="text" id="country" autocomplete="country"/>
+ <input type="text" id="address-level2" autocomplete="address-level2" />
+ <select><option value="test">test</option></select>
+ </form>
+ </body>
+ </html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "name" },
+ { fieldName: "country" },
+ { fieldName: "address-level1", reason: "update-heuristic" },
+ { fieldName: "address-level2" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "name" },
+ { fieldName: "country" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1", reason: "update-heuristic" },
+ ],
+ },
+ ],
+ },
+]);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_parse_creditcard_expiry_fields.js b/browser/extensions/formautofill/test/browser/heuristics/browser_parse_creditcard_expiry_fields.js
new file mode 100644
index 0000000000..aa61633f17
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_parse_creditcard_expiry_fields.js
@@ -0,0 +1,198 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests([
+ {
+ description:
+ "Apply credit card expiry heuristic only when there is only 1 credit card expiry field",
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" autocomplete="cc-number"/>
+ <input type="text" placeholder="mm-yy"/>
+ </form>
+ <form>
+ <input type="text" autocomplete="cc-number"/>
+ <input type="text" id="month"/>
+ </form>
+ <form>
+ <input type="text" autocomplete="cc-number"/>
+ <input type="text" id="year"/>
+ </form>
+ </body>
+ </html>
+ `,
+ expectedResult: [
+ {
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-exp", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-exp", reason: "update-heuristic" },
+ ],
+ },
+ {
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-exp", reason: "update-heuristic" },
+ ],
+ },
+ ],
+ },
+ {
+ description:
+ "Do not apply credit card expiry heuristic only when the previous field is not a cc field",
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" placeholder="mm-yy"/>
+ <input type="text" autocomplete="cc-number"/>
+ </form>
+ <form>
+ <input type="text" id="month"/>
+ <input type="text" autocomplete="cc-number"/>
+ </form>
+ <form>
+ <input type="text" id="year"/>
+ <input type="text" autocomplete="cc-number"/>
+ </form>
+ </body>
+ </html>
+ `,
+ expectedResult: [
+ {
+ fields: [
+ { fieldName: "cc-exp", reason: "regex-heuristic" },
+ { fieldName: "cc-number", reason: "autocomplete" },
+ ],
+ },
+ {
+ fields: [{ fieldName: "cc-number", reason: "autocomplete" }],
+ },
+ {
+ fields: [{ fieldName: "cc-number", reason: "autocomplete" }],
+ },
+ ],
+ },
+ {
+ description:
+ "Apply credit card expiry heuristic only when the previous fields has a credit card number field",
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" autocomplete="cc-number"/>
+ <input type="text" autocomplete="cc-name"/>
+ <input type="text" placeholder="month"/>
+ </form>
+ <form>
+ <input type="text" autocomplete="cc-number"/>
+ <input type="text" autocomplete="cc-family-name"/>
+ <input type="text" autocomplete="cc-given-name"/>
+ <input type="text" placeholder="month"/>
+ </form>
+ </body>
+ </html>
+ `,
+ expectedResult: [
+ {
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-name", reason: "autocomplete" },
+ { fieldName: "cc-exp", reason: "update-heuristic" },
+ ],
+ },
+ {
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-family-name", reason: "autocomplete" },
+ { fieldName: "cc-given-name", reason: "autocomplete" },
+ { fieldName: "cc-exp", reason: "update-heuristic" },
+ ],
+ },
+ ],
+ },
+ {
+ description:
+ "Apply credit card expiry heuristic only when there are credit card expiry fields",
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" autocomplete="cc-number"/>
+ <input type="text" placeholder="month"/>
+ <input type="text" placeholder="year"/>
+ </form>
+ <form>
+ <input type="text" autocomplete="cc-number"/>
+ <input type="text" placeholder="month"/>
+ <input type="text" placeholder="month"/>
+ </form>
+ <form>
+ <input type="text" autocomplete="cc-number"/>
+ <input type="text" placeholder="year"/>
+ <input type="text" placeholder="year"/>
+ </form>
+ </body>
+ </html>
+ `,
+ expectedResult: [
+ {
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "update-heuristic" },
+ ],
+ },
+ {
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-exp-month", reason: "update-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ {
+ description: "Honor autocomplete attribute",
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" autocomplete="cc-number"/>
+ <input type="text" placeholder="month" autocomplete="cc-exp"/>
+ <input type="text" placeholder="year" autocomplete="cc-exp"/>
+ </form>
+ </body>
+ </html>
+ `,
+ expectedResult: [
+ {
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-exp", reason: "autocomplete" },
+ ],
+ },
+ {
+ fields: [{ fieldName: "cc-exp", reason: "autocomplete" }],
+ },
+ ],
+ },
+]);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_parse_name_fields.js b/browser/extensions/formautofill/test/browser/heuristics/browser_parse_name_fields.js
new file mode 100644
index 0000000000..6433a4a9ea
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_parse_name_fields.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests([
+ {
+ description:
+ "Update name to cc-name when the previous section is a credit card section",
+ fixtureData: `
+ <html><body><form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="given" placeholder="given-name">
+ <input id="family" placeholder="family-name">
+ </form></body></html>`,
+ expectedResult: [
+ {
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-given-name", reason: "update-heuristic" },
+ { fieldName: "cc-family-name", reason: "update-heuristic" },
+ ],
+ },
+ ],
+ },
+ {
+ description:
+ "Do not update name to cc-name when the previous credit card section already contains cc-name",
+ fixtureData: `
+ <html><body><form>
+ <input id="cc-name" autocomplete="cc-name">
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="given" placeholder="given-name">
+ <input id="family" placeholder="family-name">
+ <input id="address" autocomplete="street-address">
+ <input id="country" autocomplete="country">
+ </form></body></html>`,
+ expectedResult: [
+ {
+ fields: [
+ { fieldName: "cc-name", reason: "autocomplete" },
+ { fieldName: "cc-number", reason: "autocomplete" },
+ ],
+ },
+ {
+ fields: [
+ { fieldName: "given-name", reason: "regex-heuristic" },
+ { fieldName: "family-name", reason: "regex-heuristic" },
+ { fieldName: "street-address", reason: "autocomplete" },
+ { fieldName: "country", reason: "autocomplete" },
+ ],
+ },
+ ],
+ },
+ {
+ description:
+ "Do not update name to cc-name when the previous credit card section contains cc-csc",
+ fixtureData: `
+ <html><body><form>
+ <input id="cc-number" autocomplete="cc-number">
+ <input id="cc-csc" autocomplete="cc-csc">
+ <input id="given" placeholder="given-name">
+ <input id="family" placeholder="family-name">
+ <input id="address" autocomplete="street-address">
+ <input id="country" autocomplete="country">
+ </form></body></html>`,
+ expectedResult: [
+ {
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ //{ fieldName: "cc-csc", reason: "autocomplete" },
+ ],
+ },
+ {
+ fields: [
+ { fieldName: "given-name", reason: "regex-heuristic" },
+ { fieldName: "family-name", reason: "regex-heuristic" },
+ { fieldName: "street-address", reason: "autocomplete" },
+ { fieldName: "country", reason: "autocomplete" },
+ ],
+ },
+ ],
+ },
+]);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_parse_street_address_fields.js b/browser/extensions/formautofill/test/browser/heuristics/browser_parse_street_address_fields.js
new file mode 100644
index 0000000000..699ddf086f
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_parse_street_address_fields.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests([
+ {
+ description: "Apply heuristic when we only see one street-address fields",
+ fixtureData: `
+ <html><body>
+ <form><input type="text" id="street-address"/></form>
+ <form><input type="text" id="addr-1"/></form>
+ <form><input type="text" id="addr-2"/></form>
+ <form><input type="text" id="addr-3"/></form>
+ </body></html>`,
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [{ fieldName: "street-address", reason: "regex-heuristic" }],
+ },
+ {
+ invalid: true,
+ fields: [{ fieldName: "address-line1", reason: "regex-heuristic" }],
+ },
+ {
+ invalid: true,
+ fields: [{ fieldName: "address-line1", reason: "update-heuristic" }],
+ },
+ {
+ invalid: true,
+ fields: [{ fieldName: "address-line1", reason: "update-heuristic" }],
+ },
+ ],
+ },
+ {
+ // Bug 1833613
+ description:
+ "street-address field is treated as address-line1 when address-line2 is present while adddress-line1 is not",
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="street-address" autocomplete="street-address"/>
+ <input type="text" id="address-line2" autocomplete="address-line2"/>
+ <input type="text" id="email" autocomplete="email"/>
+ </form>
+ </body>
+ </html>`,
+ expectedResult: [
+ {
+ fields: [
+ { fieldName: "address-line1", reason: "update-heuristic" },
+ { fieldName: "address-line2", reason: "autocomplete" },
+ { fieldName: "email", reason: "autocomplete" },
+ ],
+ },
+ ],
+ },
+ {
+ // Bug 1833613
+ description:
+ "street-address field should not be treated as address-line1 when address-line2 is not present",
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="street-address" autocomplete="street-address"/>
+ <input type="text" id="address-line3" autocomplete="address-line3"/>
+ <input type="text" id="email" autocomplete="email"/>
+ </form>
+ </body>
+ </html>`,
+ expectedResult: [
+ {
+ fields: [
+ { fieldName: "street-address", reason: "autocomplete" },
+ { fieldName: "address-line3", reason: "autocomplete" },
+ { fieldName: "email", reason: "autocomplete" },
+ ],
+ },
+ ],
+ },
+ {
+ // Bug 1833613
+ description:
+ "street-address field should not be treated as address-line1 when address-line1 is present",
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="street-address" autocomplete="street-address"/>
+ <input type="text" id="address-line1" autocomplete="address-line1"/>
+ <input type="text" id="email" autocomplete="email"/>
+ </form>
+ </body>
+ </html>`,
+ expectedResult: [
+ {
+ fields: [
+ { fieldName: "street-address", reason: "autocomplete" },
+ { fieldName: "address-line1", reason: "autocomplete" },
+ { fieldName: "email", reason: "autocomplete" },
+ ],
+ },
+ ],
+ },
+ {
+ description:
+ "street-address field is treated as address-line1 when address-line2 is present while adddress-line1 is not",
+ fixtureData: `
+ <html>
+ <body>
+ <form>
+ <input type="text" id="addr-3"/>
+ <input type="text" id="addr-2"/>
+ <input type="text" id="addr-1"/>
+ </form>
+ <form>
+ <input type="text" id="addr-3" autocomplete="address-line3"/>
+ <input type="text" id="addr-2" autocomplete="address-line2"/>
+ <input type="text" id="addr-1" autocomplete="address-line1"/>
+ </form>
+ </body>
+ </html>`,
+ expectedResult: [
+ {
+ description:
+ "Apply heuristic when we see 3 street-address fields occur in a row",
+ fields: [
+ { fieldName: "address-line1", reason: "update-heuristic" },
+ { fieldName: "address-line2", reason: "regex-heuristic" },
+ { fieldName: "address-line3", reason: "update-heuristic" },
+ ],
+ },
+ {
+ description:
+ "Do not apply heuristic when we see 3 street-address fields occur in a row but autocomplete attribute is present",
+ fields: [
+ { fieldName: "address-line3", reason: "autocomplete" },
+ { fieldName: "address-line2", reason: "autocomplete" },
+ { fieldName: "address-line1", reason: "autocomplete" },
+ ],
+ },
+ ],
+ },
+]);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_section_validation_address.js b/browser/extensions/formautofill/test/browser/heuristics/browser_section_validation_address.js
new file mode 100644
index 0000000000..2e9cf42ab0
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_section_validation_address.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests([
+ {
+ description: `An address section is valid when it only contains more than three fields`,
+ fixtureData: `
+ <html><body>
+ <input id="street-address" autocomplete="street-address">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="email" autocomplete="email">
+ </body></html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "email" },
+ ],
+ },
+ ],
+ },
+ {
+ description: `An address section is invalid when it contains less than threee fields`,
+ fixtureData: `
+ <html><body>
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="email" autocomplete="email">
+
+ <input id="postal-code" autocomplete="postal-code">
+ </body></html>
+ `,
+ expectedResult: [
+ {
+ description: "A section with two fields",
+ invalid: true,
+ fields: [
+ { fieldName: "postal-code", reason: "autocomplete" },
+ { fieldName: "email", reason: "autocomplete" },
+ ],
+ },
+ {
+ description: "A section with one field",
+ invalid: true,
+ fields: [{ fieldName: "postal-code", reason: "autocomplete" }],
+ },
+ ],
+ },
+ {
+ description: `Address section validation only counts the number of different address field name in the section`,
+ fixtureData: `
+ <html><body>
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="email" autocomplete="email">
+ <input id="email" autocomplete="email">
+ </body></html>
+ `,
+ expectedResult: [
+ {
+ description:
+ "A section with three fields but has duplicated email fields",
+ invalid: true,
+ fields: [
+ { fieldName: "postal-code", reason: "autocomplete" },
+ { fieldName: "email", reason: "autocomplete" },
+ { fieldName: "email", reason: "autocomplete" },
+ ],
+ },
+ ],
+ },
+]);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/browser_sections_by_name.js b/browser/extensions/formautofill/test/browser/heuristics/browser_sections_by_name.js
new file mode 100644
index 0000000000..c6c8ea5759
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/browser_sections_by_name.js
@@ -0,0 +1,318 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global add_heuristic_tests */
+
+"use strict";
+
+// The following are included in this test
+// - One named billing section
+// - One named billing section and one named shipping section
+// - One named billing section and one section without name
+// - Fields without section name are merged to a section with section name
+// - Two sections without name
+
+add_heuristic_tests([
+ {
+ description: `One named billing section`,
+ fixtureData: `
+ <html><body>
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="country" autocomplete="billing country">
+ </body></html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ addressType: "billing",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ ],
+ },
+ {
+ description: `One billing section and one shipping section`,
+ fixtureData: `
+ <html><body>
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="country" autocomplete="billing country">
+ <input id="street-address" autocomplete="shipping street-address">
+ <input id="postal-code" autocomplete="shipping postal-code">
+ <input id="country" autocomplete="shipping country">
+ </body></html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ addressType: "billing",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ addressType: "shipping",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ ],
+ },
+ {
+ description: `One billing section, one shipping section, and then billing section`,
+ fixtureData: `
+ <html><body>
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="street-address" autocomplete="shipping street-address">
+ <input id="postal-code" autocomplete="shipping postal-code">
+ <input id="country" autocomplete="shipping country">
+ <input id="country" autocomplete="billing country">
+ </body></html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ addressType: "billing",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ addressType: "shipping",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ ],
+ },
+ {
+ description: `one section without a name and one billing section`,
+ fixtureData: `
+ <html><body>
+ <input id="street-address" autocomplete="street-address">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="country" autocomplete="country">
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="country" autocomplete="billing country">
+ </body></html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ addressType: "billing",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ ],
+ },
+ {
+ description: `One billing section and one section without a name`,
+ fixtureData: `
+ <html><body>
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="country" autocomplete="billing country">
+ <input id="street-address" autocomplete="street-address">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="country" autocomplete="country">
+ </body></html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ addressType: "billing",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ ],
+ },
+ {
+ description: `Fields without section name are merged (test both before and after the section with a name)`,
+ fixtureData: `
+ <html><body>
+ <input id="name" autocomplete="name">
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="country" autocomplete="billing country">
+ <input id="street-address" autocomplete="shipping street-address">
+ <input id="postal-code" autocomplete="shipping postal-code">
+ <input id="country" autocomplete="shipping country">
+ <input id="name" autocomplete="name">
+ </body></html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ addressType: "billing",
+ },
+ fields: [
+ { fieldName: "name", addressType: "" },
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ addressType: "shipping",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ { fieldName: "name", addressType: "" },
+ ],
+ },
+ ],
+ },
+ {
+ description: `Fields without section name are merged, but do not merge if the field already exists`,
+ fixtureData: `
+ <html><body>
+ <input id="name" autocomplete="name">
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="country" autocomplete="billing country">
+ <input id="name" autocomplete="name">
+ </body></html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ addressType: "billing",
+ },
+ fields: [
+ { fieldName: "name", addressType: "" },
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [{ fieldName: "name", reason: "autocomplete" }],
+ },
+ ],
+ },
+ {
+ description: `Fields without section name are merged (multi-fields)`,
+ fixtureData: `
+ <html><body>
+ <input id="street-address" autocomplete="billing street-address">
+ <input id="postal-code" autocomplete="billing postal-code">
+ <input id="country" autocomplete="billing country">
+ <input id="email" autocomplete="email">
+ <input id="email" autocomplete="email">
+ </body></html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ addressType: "billing",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ { fieldName: "email", addressType: "" },
+ { fieldName: "email", addressType: "" },
+ ],
+ },
+ ],
+ },
+ {
+ description: `Two sections without name`,
+ fixtureData: `
+ <html><body>
+ <input id="street-address" autocomplete="street-address">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="country" autocomplete="country">
+ <input id="street-address" autocomplete="street-address">
+ <input id="postal-code" autocomplete="postal-code">
+ <input id="country" autocomplete="country">
+ </body></html>
+ `,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ { fieldName: "country" },
+ ],
+ },
+ ],
+ },
+]);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser.toml b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser.toml
new file mode 100644
index 0000000000..aba28e6597
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser.toml
@@ -0,0 +1,40 @@
+[DEFAULT]
+skip-if = ["os == 'android'"] # bug 1730213
+support-files = [
+ "../../head.js",
+ "../../../fixtures/**",
+]
+
+["browser_BestBuy.js"]
+
+["browser_CDW.js"]
+
+["browser_CostCo.js"]
+
+["browser_DirectAsda.js"]
+
+["browser_Ebay.js"]
+
+["browser_Euronics.js"]
+
+["browser_GlobalDirectAsda.js"]
+
+["browser_HomeDepot.js"]
+
+["browser_Lufthansa.js"]
+
+["browser_Lush.js"]
+
+["browser_Macys.js"]
+
+["browser_NewEgg.js"]
+
+["browser_OfficeDepot.js"]
+
+["browser_QVC.js"]
+
+["browser_Sears.js"]
+
+["browser_Staples.js"]
+
+["browser_Walmart.js"]
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_BestBuy.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_BestBuy.js
new file mode 100644
index 0000000000..e91c19de45
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_BestBuy.js
@@ -0,0 +1,82 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "Checkout_ShippingAddress.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "street-address" },
+ { fieldName: "address-level2" }, // city
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" }, // sign-up
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ { fieldName: "tel", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "Checkout_Payment.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "street-address" },
+ { fieldName: "address-level2" }, // city
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ { fieldName: "tel", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "SignIn.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" }, // sign-in
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/BestBuy/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CDW.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CDW.js
new file mode 100644
index 0000000000..020f26ff42
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CDW.js
@@ -0,0 +1,71 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "Checkout_ShippingInfo.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "organization" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "address-level2" }, // city
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ { fieldName: "postal-code" }, // EXt
+ { fieldName: "email" },
+ { fieldName: "tel" },
+ { fieldName: "tel-extension", reason: "update-heuristic" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "Checkout_BillingPaymentInfo.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "organization" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "address-level2" }, // city
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ { fieldName: "postal-code" }, // Ext
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "cc-type" }, // ac-off
+ { fieldName: "cc-number", reason: "fathom" }, // ac-off
+ { fieldName: "cc-exp-month" },
+ { fieldName: "cc-exp-year" },
+ // {fieldName: "cc-csc"},
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "Checkout_Logon.html",
+ expectedResult: [
+ ],
+ },
+ ],
+ "fixtures/third_party/CDW/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CostCo.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CostCo.js
new file mode 100644
index 0000000000..510656a773
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_CostCo.js
@@ -0,0 +1,194 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "ShippingAddress.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "additional-name" }, // middle-name initial
+ { fieldName: "family-name" },
+ { fieldName: "organization" },
+ { fieldName: "country" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2", reason:"update-heuristic" },
+ { fieldName: "address-level2" }, // city
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "address-line1", reason:"regex-heuristic" },
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "additional-name" }, // middle-name initial
+ { fieldName: "family-name" },
+ { fieldName: "organization" },
+ { fieldName: "country" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2", reason:"update-heuristic" },
+ { fieldName: "address-level2" }, // city
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "address-line1", reason:"regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "Payment.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "cc-type" }, // ac-off
+ { fieldName: "cc-number", reason: "fathom" }, // ac-off
+ { fieldName: "cc-exp-month" },
+ { fieldName: "cc-exp-year" },
+ // { fieldName: "cc-csc"}, // ac-off
+ { fieldName: "cc-name", reason: "fathom" }, // ac-off
+ ],
+ },
+ {
+ invalid: true, // confidence is not high enough
+ fields: [
+ { fieldName: "cc-number", reason: "fathom" }, // ac-off
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "additional-name" }, // middle-name initial
+ { fieldName: "family-name" },
+ { fieldName: "organization" },
+ { fieldName: "country" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2", reason:"update-heuristic" },
+ { fieldName: "address-level2" }, // city
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "address-line1", reason:"regex-heuristic" },
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "additional-name" }, // middle-name initial
+ { fieldName: "family-name" },
+ { fieldName: "organization" },
+ { fieldName: "country" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2", reason:"update-heuristic" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "address-line1", reason:"regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "SignIn.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ // Forgot password
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ // {fieldName: "password"},
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ // Sign up
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ }
+ ],
+ },
+ ],
+ "fixtures/third_party/CostCo/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_DirectAsda.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_DirectAsda.js
new file mode 100644
index 0000000000..6cb7979173
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_DirectAsda.js
@@ -0,0 +1,25 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "Payment.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-number" },
+ { fieldName: "cc-name" },
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/DirectAsda/"
+)
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Ebay.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Ebay.js
new file mode 100644
index 0000000000..db6047718f
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Ebay.js
@@ -0,0 +1,25 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "Checkout_Payment_FR.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "cc-number" },
+ { fieldName: "cc-exp" },
+ { fieldName: "cc-given-name" },
+ { fieldName: "cc-family-name" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/Ebay/"
+)
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Euronics.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Euronics.js
new file mode 100644
index 0000000000..cd9757bd35
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Euronics.js
@@ -0,0 +1,54 @@
+/* global runHeuristicsTest */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "Payment.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "cc-number" },
+ //{ fieldName: "cc-cvc" },
+ { fieldName: "cc-exp-month" },
+ { fieldName: "cc-exp-year" },
+ ],
+ },
+ {
+ invalid: true,
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "cc-number" },
+ ],
+ },
+ {
+ invalid: true,
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "cc-number" },
+ { fieldName: "cc-type" },
+ ],
+ },
+ {
+ invalid: true,
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "country" },
+ { fieldName: "organization" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/Euronics/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_GlobalDirectAsda.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_GlobalDirectAsda.js
new file mode 100644
index 0000000000..7b82009032
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_GlobalDirectAsda.js
@@ -0,0 +1,24 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "Payment.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "cc-number", reason: "fathom" },
+ { fieldName: "cc-exp-month" },
+ { fieldName: "cc-exp-year" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/GlobalDirectAsda/"
+)
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_HomeDepot.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_HomeDepot.js
new file mode 100644
index 0000000000..46e5ecef3c
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_HomeDepot.js
@@ -0,0 +1,64 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "Checkout_ShippingPayment.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "email" },
+ { fieldName: "tel" },
+ { fieldName: "street-address" },
+ { fieldName: "postal-code" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ {
+ fieldName: "street-address",
+ reason: "autocomplete",
+ addressType: "billing",
+ },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ { fieldName: "cc-number", reason: "fathom" },
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "SignIn.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email",reason: "regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email",reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/HomeDepot/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lufthansa.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lufthansa.js
new file mode 100644
index 0000000000..801dbf66be
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lufthansa.js
@@ -0,0 +1,28 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "Checkout_Payment.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-type", reason: "regex-heuristic" },
+ { fieldName: "cc-number" },
+ { fieldName: "cc-number" },
+ { fieldName: "cc-number" },
+ { fieldName: "cc-number" },
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/Lufthansa/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lush.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lush.js
new file mode 100644
index 0000000000..a59fc966e5
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Lush.js
@@ -0,0 +1,31 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "index.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-name" },
+ { fieldName: "cc-number" },
+ ],
+ },
+ {
+ fields: [
+ { fieldName: "cc-number", reason: "autocomplete" },
+ { fieldName: "cc-name", reason: "fathom" },
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/Lush/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Macys.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Macys.js
new file mode 100644
index 0000000000..dab7a4cc70
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Macys.js
@@ -0,0 +1,40 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "SignIn.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ // Sign in
+ { fieldName: "email", reason: "regex-heuristic"},
+ // {fieldName: "password"},
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ // Forgot password
+ { fieldName: "email", reason: "regex-heuristic"},
+ ],
+ },
+ // Bug 1836256: The 'securityCode' field is being recognized as both an 'email'
+ // and 'csc' field. Given that we match 'csc' before the 'email' field, this
+ // field is currently recognized as a 'csc' field. We need to implement a heuristic
+ // to accurately determine the field type when a field aligns with multiple field types
+ // instead of depending on the order of we perform the matching
+ //{
+ //invalid: true,
+ //fields: [
+ //{ fieldName: "email", reason: "regex-heuristic"},
+ //],
+ //},
+ ],
+ },
+ ],
+ "fixtures/third_party/Macys/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_NewEgg.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_NewEgg.js
new file mode 100644
index 0000000000..d914f02890
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_NewEgg.js
@@ -0,0 +1,109 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "ShippingInfo.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "country" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ { fieldName: "tel" },
+ { fieldName: "email" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "BillingInfo.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-name" },
+ { fieldName: "cc-number" }, // ac-off
+ ],
+ },
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-name" },
+ { fieldName: "cc-number" }, // ac-off
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ // { fieldName: "cc-csc"},
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "country" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ { fieldName: "tel" },
+ ],
+ },
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-name" },
+ { fieldName: "cc-number" }, // ac-off
+ { fieldName: "cc-exp-month", reason: "regex-heuristic" },
+ { fieldName: "cc-exp-year", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-name" },
+ { fieldName: "cc-number" }, // ac-off
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "Login.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" }, // Email Address
+ { fieldName: "email", reason: "regex-heuristic" }, // Confirm Email Address
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/NewEgg/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_OfficeDepot.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_OfficeDepot.js
new file mode 100644
index 0000000000..8b56d50eca
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_OfficeDepot.js
@@ -0,0 +1,83 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "ShippingAddress.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "organization" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "postal-code" },
+ { fieldName: "address-level2" }, // City & State
+ { fieldName: "address-level2" }, // City
+ { fieldName: "address-level1" }, // State
+ { fieldName: "tel-area-code", reason: "update-heuristic" },
+ { fieldName: "tel-local-prefix", reason: "update-heuristic" },
+ { fieldName: "tel-local-suffix", reason: "update-heuristic" },
+ { fieldName: "tel-extension", reason: "update-heuristic" },
+ { fieldName: "email" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "Payment.html",
+ expectedResult: [
+ {
+ invalid: true, // because non of them is identified by fathom
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "cc-exp-month" },
+ { fieldName: "cc-exp-year" },
+ { fieldName: "cc-number" },
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "organization" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "postal-code" },
+ { fieldName: "address-level2" }, // City & State
+ { fieldName: "address-level2" }, // City
+ { fieldName: "address-level1" }, // state
+ { fieldName: "tel-area-code", reason: "update-heuristic" },
+ { fieldName: "tel-local-prefix", reason: "update-heuristic" },
+ { fieldName: "tel-local-suffix", reason: "update-heuristic" },
+ { fieldName: "tel-extension", reason: "update-heuristic" },
+ { fieldName: "email" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "SignIn.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/OfficeDepot/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_QVC.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_QVC.js
new file mode 100644
index 0000000000..00705293f7
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_QVC.js
@@ -0,0 +1,96 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "YourInformation.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "tel", reason: "regex-heuristic" },
+ { fieldName: "email", reason: "regex-heuristic" },
+ // { fieldName: "bday-month"}, // select
+ // { fieldName: "bday-day"}, // select
+ // { fieldName: "bday-year"},
+ ],
+ },
+ {
+ fields: [
+ // { fieldName: "cc-csc"},
+ { fieldName: "cc-type", reason: "regex-heuristic" },
+ { fieldName: "cc-number", reason: "fathom" },
+ { fieldName: "cc-exp", reason: "update-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "cc-number", reason: "regex-heuristic" }, // txtQvcGiftCardNumber
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "PaymentMethod.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "tel", reason: "regex-heuristic" },
+ { fieldName: "email", reason: "regex-heuristic" },
+ // { fieldName: "bday-month"}, // select
+ // { fieldName: "bday-day"}, // select
+ // { fieldName: "bday-year"}, // select
+ ],
+ },
+ {
+ default: {
+ reason: "fathom",
+ },
+ fields: [
+ { fieldName: "cc-type", reason: "regex-heuristic" }, // ac-off
+ { fieldName: "cc-number" }, // ac-off
+ { fieldName: "cc-exp", reason: "update-heuristic" },
+ // { fieldName: "cc-csc"},
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "cc-number", reason: "regex-heuristic" }, // txtQvcGiftCardNumbe, ac-off
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "SignIn.html",
+ expectedResult: [
+ {
+ // Sign in
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/QVC/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Sears.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Sears.js
new file mode 100644
index 0000000000..c2ee60f220
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Sears.js
@@ -0,0 +1,81 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "ShippingAddress.html",
+ expectedResult: [
+ {
+ invalid: true,
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "email" },
+ ]
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ // check-out, ac-off
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "tel" },
+ { fieldName: "tel-extension", reason: "update-heuristic" },
+ { fieldName: "email" },
+ { fieldName: "email" },
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ // ac-off
+ // { fieldName: "email"},
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2" },
+ { fieldName: "address-level2" },
+ { fieldName: "address-level1" },
+ { fieldName: "postal-code" },
+ { fieldName: "tel" },
+ { fieldName: "tel-extension", reason: "update-heuristic" },
+ // { fieldName: "new-password"},
+ ],
+ },
+ {
+ invalid: true,
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ // ac-off
+ { fieldName: "email" },
+ ]
+ },
+ {
+ invalid: true,
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ // ac-off
+ { fieldName: "email" },
+ ]
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/Sears/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Staples.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Staples.js
new file mode 100644
index 0000000000..7f116110cd
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Staples.js
@@ -0,0 +1,78 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "Basic.html",
+ expectedResult: [
+ {
+ // ac-off
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "address-line1" },
+ { fieldName: "email" },
+ { fieldName: "tel" },
+ { fieldName: "tel" }, // Extension
+ { fieldName: "organization" },
+ ]
+ },
+ ],
+ },
+ {
+ fixturePath: "Basic_ac_on.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "address-line1" },
+ { fieldName: "email" },
+ { fieldName: "tel" },
+ { fieldName: "tel" }, // Extension
+ { fieldName: "organization" },
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "PaymentBilling.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "cc-number", reason: "fathom" },
+ { fieldName: "cc-exp", reason:"update-heuristic" },
+ // {fieldName: "cc-csc"},
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "PaymentBilling_ac_on.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "cc-number", reason: "fathom" },
+ { fieldName: "cc-exp", reason:"update-heuristic" },
+ // { fieldName: "cc-csc"},
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/Staples/"
+);
diff --git a/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Walmart.js b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Walmart.js
new file mode 100644
index 0000000000..6260a6be0f
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/heuristics/third_party/browser_Walmart.js
@@ -0,0 +1,93 @@
+/* global add_heuristic_tests */
+
+"use strict";
+
+add_heuristic_tests(
+ [
+ {
+ fixturePath: "Checkout.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "postal-code", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "email", reason: "regex-heuristic" },
+ // { fieldName: "password"}, // ac-off
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "email" }, // ac-off
+ // { fieldName: "password"},
+ // { fieldName: "password"}, // ac-off
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "Payment.html",
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ section: "section-payment",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "tel" },
+ ],
+ },
+ {
+ default: {
+ reason: "autocomplete",
+ section: "section-payment",
+ },
+ fields: [
+ { fieldName: "cc-number" },
+ { fieldName: "cc-exp-month" },
+ { fieldName: "cc-exp-year" },
+ // { fieldName: "cc-csc"},
+ ],
+ },
+ ],
+ },
+ {
+ fixturePath: "Shipping.html",
+ expectedResult: [
+ {
+ invalid: true,
+ fields: [
+ { fieldName: "postal-code", reason: "regex-heuristic" },
+ ],
+ },
+ {
+ default: {
+ reason: "regex-heuristic",
+ },
+ fields: [
+ { fieldName: "given-name" },
+ { fieldName: "family-name" },
+ { fieldName: "tel" },
+ { fieldName: "address-line1" },
+ { fieldName: "address-line2", reason:"update-heuristic" },
+ { fieldName: "address-level2" }, // city
+ { fieldName: "address-level1" }, // state
+ { fieldName: "postal-code" },
+ ],
+ },
+ ],
+ },
+ ],
+ "fixtures/third_party/Walmart/"
+);