summaryrefslogtreecommitdiffstats
path: root/browser/extensions/formautofill/test/browser/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/formautofill/test/browser/head.js')
-rw-r--r--browser/extensions/formautofill/test/browser/head.js1094
1 files changed, 1094 insertions, 0 deletions
diff --git a/browser/extensions/formautofill/test/browser/head.js b/browser/extensions/formautofill/test/browser/head.js
new file mode 100644
index 0000000000..2dd3d1451e
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/head.js
@@ -0,0 +1,1094 @@
+"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 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 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 EMPTY_URL = "https://example.org" + HTTP_TEST_PATH + "empty.html";
+
+const FTU_PREF = "extensions.formautofill.firstTimeUse";
+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";
+
+/**
+ * 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;
+
+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 found autocomplete items");
+}
+
+/**
+ * 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.
+ 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);
+}
+
+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} button The button type in popup notification.
+ * @param {number} index The action's index in menu list.
+ */
+async function clickDoorhangerButton(button, index) {
+ let popuphidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+
+ if (button == MAIN_BUTTON || button == SECONDARY_BUTTON) {
+ EventUtils.synthesizeMouseAtCenter(getNotification()[button], {});
+ } else if (button == 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"
+ );
+ await EventUtils.synthesizeMouseAtCenter(notification.menubutton, {});
+ info("expecting notification popup show up");
+ await dropdownPromise;
+
+ let actionMenuItem = notification.querySelectorAll("menuitem")[index];
+ await EventUtils.synthesizeMouseAtCenter(actionMenuItem, {});
+ }
+ info("expecting notification popup hidden");
+ await popuphidden;
+}
+
+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: "",
+ },
+ ...expectedSection.default,
+ ...expectedFieldDetail,
+ };
+
+ const keys = new Set([...Object.keys(field), ...Object.keys(expected)]);
+ ["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`
+ );
+ });
+}
+/**
+ * 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) {
+ 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 });
+}
+
+add_setup(function () {
+ OSKeyStoreTestUtils.setup();
+});
+
+registerCleanupFunction(async () => {
+ await removeAllRecords();
+ await OSKeyStoreTestUtils.cleanup();
+});