diff options
Diffstat (limited to '')
-rw-r--r-- | browser/extensions/formautofill/test/browser/head.js | 1094 |
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(); +}); |