summaryrefslogtreecommitdiffstats
path: root/browser/extensions/formautofill/test/mochitest
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/formautofill/test/mochitest')
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/mochitest.ini26
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html251
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html205
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html211
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html96
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html174
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html110
-rw-r--r--browser/extensions/formautofill/test/mochitest/formautofill_common.js478
-rw-r--r--browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js304
-rw-r--r--browser/extensions/formautofill/test/mochitest/mochitest.ini23
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_address_level_1_submission.html102
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html116
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_autofocus_form.html69
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html220
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_form_changes.html128
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html121
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html273
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_multiple_forms.html67
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_on_address_submission.html121
19 files changed, 3095 insertions, 0 deletions
diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/mochitest.ini b/browser/extensions/formautofill/test/mochitest/creditCard/mochitest.ini
new file mode 100644
index 0000000000..d7a7ea2fd8
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/mochitest.ini
@@ -0,0 +1,26 @@
+[DEFAULT]
+prefs =
+ extensions.formautofill.creditCards.supported=on
+ extensions.formautofill.creditCards.enabled=true
+ extensions.formautofill.reauth.enabled=true
+support-files =
+ !/toolkit/components/satchel/test/satchel_common.js
+ ../../../../../../toolkit/components/satchel/test/parent_utils.js
+ !/toolkit/components/satchel/test/parent_utils.js
+ !/browser/extensions/formautofill/test/mochitest/formautofill_common.js
+ !/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
+skip-if = xorigin
+ toolkit == 'android' # bug 1730213
+
+[test_basic_creditcard_autocomplete_form.html]
+scheme=https
+[test_clear_form.html]
+scheme=https
+[test_clear_form_expiry_select_elements.html]
+scheme=https
+[test_creditcard_autocomplete_off.html]
+scheme=https
+[test_preview_highlight_with_multiple_cc_number_fields.html]
+scheme=https
+[test_preview_highlight_with_site_prefill.html]
+scheme=https
diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html
new file mode 100644
index 0000000000..0764bef0eb
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html
@@ -0,0 +1,251 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../formautofill_common.js"></script>
+ <script type="text/javascript" src="../../../../../..//toolkit/components/satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: simple form credit card autofill
+
+<script>
+"use strict";
+
+const MOCK_STORAGE = [{
+ "cc-name": "John Doe",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": 12,
+ "cc-exp-year": 2022,
+}];
+
+const reducedMockRecord = {
+ "cc-name": "John Doe",
+ "cc-number": "4929001587121045",
+};
+
+async function setupCreditCardStorage() {
+ await addCreditCard(MOCK_STORAGE[0]);
+ await addCreditCard(MOCK_STORAGE[1]);
+}
+
+async function setupFormHistory() {
+ await updateFormHistory([
+ {op: "add", fieldname: "cc-name", value: "John Smith"},
+ {op: "add", fieldname: "cc-exp-year", value: 2023},
+ ]);
+}
+
+initPopupListener();
+
+// Form with history only.
+add_task(async function history_only_menu_checking() {
+ // TODO: eliminate the timeout when we're able to indicate the right
+ // timing to start.
+ //
+ // After test process was re-spawning to https scheme. Wait 2 secs
+ // to ensure the environment is ready to do storage setup.
+ await sleep(2000);
+ await setupFormHistory();
+
+ await setInput("#cc-exp-year", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["2023"], false);
+});
+
+// Display credit card result even if the number of fillable fields is less than the threshold.
+add_task(async function all_saved_fields_less_than_threshold() {
+ await addCreditCard(reducedMockRecord);
+
+ await setInput("#cc-name", "");
+ await expectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ checkMenuEntries([reducedMockRecord].map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-name"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `Visa ${cc["cc-name"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+
+ await cleanUpCreditCards();
+});
+
+// Form with both history and credit card storage.
+add_task(async function check_menu_when_both_existed() {
+ await setupCreditCardStorage();
+
+ await setInput("#cc-number", "");
+ await expectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primaryAffix: cc.ccNumberFmt.affix,
+ primary: cc.ccNumberFmt.label,
+ secondary: cc["cc-name"],
+ ariaLabel: `${getCCTypeName(cc)} ${cc.ccNumberFmt.affix} ${cc.ccNumberFmt.label} ${cc["cc-name"]}`,
+ })));
+
+ await setInput("#cc-name", "");
+ await expectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-name"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `${getCCTypeName(cc)} ${cc["cc-name"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+
+ await setInput("#cc-exp-year", "");
+ await expectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-exp-year"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `${getCCTypeName(cc)} ${cc["cc-exp-year"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+
+ await setInput("#cc-exp-month", "");
+ await expectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-exp-month"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `${getCCTypeName(cc)} ${cc["cc-exp-month"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+
+ await cleanUpCreditCards();
+});
+
+// Display history search result if no matched data in credit card.
+add_task(async function check_fallback_for_mismatched_field() {
+ await addCreditCard(reducedMockRecord);
+
+ await setInput("#cc-exp-year", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["2023"], false);
+
+ await cleanUpCreditCards();
+});
+
+// Display history search result if credit card autofill is disabled.
+add_task(async function check_search_result_for_pref_off() {
+ await setupCreditCardStorage();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.formautofill.creditCards.enabled", false]],
+ });
+
+ await setInput("#cc-name", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["John Smith"], false);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+let canTest;
+
+// Autofill the credit card from dropdown menu.
+add_task(async function check_fields_after_form_autofill() {
+ canTest = await canTestOSKeyStoreLogin();
+ if (!canTest) {
+ todo(canTest, "Cannot test OS key store login on official builds.");
+ return;
+ }
+
+ await setInput("#cc-exp-year", 202);
+
+ synthesizeKey("KEY_ArrowDown");
+ // The popup doesn't auto-show on focus because the field isn't empty
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.slice(1).map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-exp-year"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `${getCCTypeName(cc)} ${cc["cc-exp-year"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+
+ synthesizeKey("KEY_ArrowDown");
+ let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ await new Promise(resolve => SimpleTest.executeSoon(resolve));
+ await triggerAutofillAndCheckProfile(MOCK_STORAGE[1]);
+ await osKeyStoreLoginShown;
+});
+
+// Fallback to history search after autofill values (for non-empty fields).
+add_task(async function check_fallback_after_form_autofill() {
+ if (!canTest) {
+ return;
+ }
+
+ await setInput("#cc-name", "J", true);
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["John Smith"], false);
+});
+
+// Present credit card popup immediately when user blanks a field
+add_task(async function check_cc_popup_on_field_blank() {
+ if (!canTest) {
+ return;
+ }
+
+ await setInput("#cc-name", "", true);
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-name"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `${getCCTypeName(cc)} ${cc["cc-name"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+});
+
+// Resume form autofill once all the autofilled fileds are changed.
+add_task(async function check_form_autofill_resume() {
+ if (!canTest) {
+ return;
+ }
+
+ document.querySelector("#cc-name").blur();
+ document.querySelector("#form1").reset();
+
+ await setInput("#cc-name", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-name"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `${getCCTypeName(cc)} ${cc["cc-name"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic form.</p>
+ <p><label>Name: <input id="cc-name" autocomplete="cc-name"></label></p>
+ <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
+ <p><label>Expiration month: <input id="cc-exp-month" autocomplete="cc-exp-month"></label></p>
+ <p><label>Expiration year: <input id="cc-exp-year" autocomplete="cc-exp-year"></label></p>
+ <p><label>Card Type: <select id="cc-type" autocomplete="cc-type">
+ <option value="discover">Discover</option>
+ <option value="jcb">JCB</option>
+ <option value="visa">Visa</option>
+ <option value="mastercard">MasterCard</option>
+ </select></label></p>
+ <p><label>CSC: <input id="cc-csc" autocomplete="cc-csc"></label></p>
+ </form>
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html
new file mode 100644
index 0000000000..3d8049f053
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html
@@ -0,0 +1,205 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form autofill - clear form button</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../formautofill_common.js"></script>
+ <script type="text/javascript" src="../../../../../../toolkit/components/satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: clear form button
+
+<script>
+"use strict";
+
+const MOCK_ADDR_STORAGE = [{
+ organization: "Sesame Street",
+ "street-address": "2 Harrison St\nline2\nline3",
+ tel: "+13453453456",
+}, {
+ organization: "Mozilla",
+ "street-address": "331 E. Evelyn Avenue",
+}, {
+ organization: "Tel org",
+ tel: "+12223334444",
+}];
+const MOCK_CC_STORAGE = [{
+ "cc-name": "John Doe",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": 12,
+ "cc-exp-year": 2022,
+}];
+
+const MOCK_CC_STORAGE_EXPECTED_FILL = [{
+ "cc-name": "John Doe",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": "04",
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": "12",
+ "cc-exp-year": 2022,
+}];
+
+initPopupListener();
+
+add_task(async function setup_storage() {
+ await addAddress(MOCK_ADDR_STORAGE[0]);
+ await addAddress(MOCK_ADDR_STORAGE[1]);
+ await addAddress(MOCK_ADDR_STORAGE[2]);
+
+ await addCreditCard(MOCK_CC_STORAGE[0]);
+ await addCreditCard(MOCK_CC_STORAGE[1]);
+});
+
+
+async function checkIsFormCleared(patch = {}) {
+ const form = document.getElementById("form1");
+
+ for (const elem of form.elements) {
+ const expectedValue = patch[elem.id] || "";
+ checkFieldValue(elem, expectedValue);
+ await checkFieldHighlighted(elem, false);
+ await checkFieldPreview(elem, "");
+ }
+}
+
+async function confirmClear(selector) {
+ info("Await for clearing input");
+ let promise = new Promise(resolve => {
+ let beforeInputFired = false;
+ let element = document.querySelector(selector);
+ element.addEventListener("beforeinput", (event) => {
+ beforeInputFired = true;
+ ok(event instanceof InputEvent,
+ '"beforeinput" event should be dispatched with InputEvent interface');
+ is(event.cancelable, SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input"),
+ `"beforeinput" event should be cancelable unless it's disabled by the pref`);
+ is(event.bubbles, true,
+ '"beforeinput" event should always bubble');
+ is(event.inputType, "insertReplacementText",
+ 'inputType value of "beforeinput" should be "insertReplacementText"');
+ is(event.data, "",
+ 'data value of "beforeinput" should be empty string');
+ is(event.dataTransfer, null,
+ 'dataTransfer value of "beforeinput" should be null');
+ is(event.getTargetRanges().length, 0,
+ 'getTargetRanges() of "beforeinput" event should return empty array');
+ }, {once: true});
+ element.addEventListener("input", (event) => {
+ ok(beforeInputFired, `"beforeinput" event should've been fired before "input" on <${element.tagName} type="${element.type}">`);
+ ok(event instanceof InputEvent,
+ '"input" event should be dispatched with InputEvent interface');
+ is(event.cancelable, false,
+ '"input" event should be never cancelable');
+ is(event.bubbles, true,
+ '"input" event should always bubble');
+ is(event.inputType, "insertReplacementText",
+ 'inputType value of "input" should be "insertReplacementText"');
+ is(event.data, "",
+ 'data value of "input" should be empty string');
+ is(event.dataTransfer, null,
+ 'dataTransfer value of "input" should be null');
+ is(event.getTargetRanges().length, 0,
+ 'getTargetRanges() of "input" should return empty array');
+ resolve();
+ }, {once: true})
+ });
+ synthesizeKey("KEY_Enter");
+ await promise;
+}
+
+add_task(async function simple_clear() {
+ await triggerPopupAndHoverItem("#organization", 0);
+ await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
+
+ await triggerPopupAndHoverItem("#tel", 0);
+ await confirmClear("#tel");
+ await checkIsFormCleared();
+});
+
+add_task(async function clear_adapted_record() {
+ await triggerPopupAndHoverItem("#street-address", 0);
+ await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
+
+ await triggerPopupAndHoverItem("#street-address", 0);
+ await confirmClear("#street-address");
+ await checkIsFormCleared();
+});
+
+add_task(async function clear_modified_form() {
+ await triggerPopupAndHoverItem("#organization", 0);
+ await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
+
+ await setInput("#tel", "+1111111111", true);
+
+ await triggerPopupAndHoverItem("#street-address", 0);
+ await confirmClear("#street-address");
+ await checkIsFormCleared({tel: "+1111111111"});
+});
+
+add_task(async function clear_distinct_section() {
+ if (!(await canTestOSKeyStoreLogin())) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+
+ document.getElementById("form1").reset();
+ await triggerPopupAndHoverItem("#cc-name", 0);
+ let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ await triggerAutofillAndCheckProfile(MOCK_CC_STORAGE_EXPECTED_FILL[0]);
+ await osKeyStoreLoginShown;
+
+ await triggerPopupAndHoverItem("#organization", 0);
+ await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
+ await triggerPopupAndHoverItem("#street-address", 0);
+ await confirmClear("#street-address");
+
+ for (const [id, val] of Object.entries(MOCK_CC_STORAGE_EXPECTED_FILL[0])) {
+ const element = document.getElementById(id);
+ if (!element) {
+ return;
+ }
+ checkFieldValue(element, val);
+ await checkFieldHighlighted(element, true);
+ }
+
+ await triggerPopupAndHoverItem("#cc-name", 0);
+ await confirmClear("#cc-name");
+ await checkIsFormCleared();
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic form.</p>
+ <p><label>organization: <input id="organization" autocomplete="organization"></label></p>
+ <p><label>streetAddress: <input id="street-address" autocomplete="street-address"></label></p>
+ <p><label>tel: <input id="tel" autocomplete="tel"></label></p>
+ <p><label>country: <input id="country" autocomplete="country"></label></p>
+
+ <p><label>Name: <input id="cc-name" autocomplete="cc-name"></label></p>
+ <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
+ <p><label>Expiration month: <input id="cc-exp-month" autocomplete="cc-exp-month"></label></p>
+ <p><label>Expiration year: <input id="cc-exp-year" autocomplete="cc-exp-year"></label></p>
+ <p><label>CSC: <input id="cc-csc" autocomplete="cc-csc"></label></p>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html
new file mode 100644
index 0000000000..4fc989a36e
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html
@@ -0,0 +1,211 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form autofill - clear form button with select elements</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../formautofill_common.js"></script>
+ <script type="text/javascript" src="../../../../../../toolkit/components/satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: clear form button with select elements.
+
+<script>
+"use strict";
+const MOCK_ADDR_STORAGE = [{
+ organization: "Sesame Street",
+ "street-address": "2 Harrison St\nline2\nline3",
+ tel: "+13453453456",
+}, {
+ organization: "Mozilla",
+ "street-address": "331 E. Evelyn Avenue",
+}, {
+ organization: "Tel org",
+ tel: "+12223334444",
+}];
+
+const MOCK_CC_STORAGE = [{
+ "cc-name": "John Doe",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": 12,
+ "cc-exp-year": 2022,
+}];
+
+initPopupListener();
+
+add_task(async function setup_storage() {
+ await addAddress(MOCK_ADDR_STORAGE[0]);
+ await addCreditCard(MOCK_CC_STORAGE[0]);
+ await addCreditCard(MOCK_CC_STORAGE[1]);
+});
+
+
+async function checkIsFormCleared(patch = {}) {
+ const form = document.getElementById("form1");
+
+ for (const elem of form.elements) {
+ const expectedValue = patch[elem.id] || "";
+ checkFieldValue(elem, expectedValue);
+ await checkFieldHighlighted(elem, false);
+ await checkFieldPreview(elem, "");
+ }
+}
+
+async function confirmClear(selector) {
+ info("Await for clearing input");
+ let promise = new Promise(resolve => {
+ let beforeInputFired = false;
+ let element = document.querySelector(selector);
+ info(`Which element are we clearing? ${element.id}`);
+ element.addEventListener("beforeinput", (event) => {
+ beforeInputFired = true;
+ ok(event instanceof InputEvent,
+ '"beforeinput" event should be dispatched with InputEvent interface');
+ is(event.cancelable, SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input"),
+ `"beforeinput" event should be cancelable unless it's disabled by the pref`);
+ is(event.bubbles, true,
+ '"beforeinput" event should always bubble');
+ is(event.inputType, "insertReplacementText",
+ 'inputType value of "beforeinput" should be "insertReplacementText"');
+ is(event.data, "",
+ 'data value of "beforeinput" should be empty string');
+ is(event.dataTransfer, null,
+ 'dataTransfer value of "beforeinput" should be null');
+ is(event.getTargetRanges().length, 0,
+ 'getTargetRanges() of "beforeinput" event should return empty array');
+ }, {once: true});
+ element.addEventListener("input", (event) => {
+ ok(beforeInputFired, `"beforeinput" event should've been fired before "input" on <${element.tagName} type="${element.type}">`);
+ ok(event instanceof InputEvent,
+ '"input" event should be dispatched with InputEvent interface');
+ is(event.cancelable, false,
+ '"input" event should be never cancelable');
+ is(event.bubbles, true,
+ '"input" event should always bubble');
+ is(event.inputType, "insertReplacementText",
+ 'inputType value of "input" should be "insertReplacementText"');
+ is(event.data, "",
+ 'data value of "input" should be empty string');
+ is(event.dataTransfer, null,
+ 'dataTransfer value of "input" should be null');
+ is(event.getTargetRanges().length, 0,
+ 'getTargetRanges() of "input" should return empty array');
+ resolve();
+ }, {once: true})
+ });
+ synthesizeKey("KEY_Enter");
+ await promise;
+}
+
+// tgiles: We need this task due to timing issues between focusAndWaitForFieldsIdentified and popupShownListener.
+// There's a 300ms delay in focusAndWaitForFieldsIdentified that can cause triggerPopupAndHoverItem to get out of sync
+// and cause the popup to appear before the test expects a popup to appear.
+
+// Without this task we end up either getting a consistent timeout or getting the following exception:
+// 0:20.55 GECKO(31108) JavaScript error: , line 0: uncaught exception: Checking selected index - timed out after 50 tries.
+// This exception appears if you attempt to create the expectPopup promise earlier than it currently is in triggetPopupAndHoverItem
+add_task(async function a_dummy_task() {
+ await triggerPopupAndHoverItem("#organization", 0);
+ await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
+
+ await triggerPopupAndHoverItem("#tel", 0);
+ await confirmClear("#tel");
+ await checkIsFormCleared({
+ "cc-exp-month": "MM",
+ "cc-exp-year": "YY"
+ });
+});
+
+add_task(async function clear_distinct_section() {
+ if (!(await canTestOSKeyStoreLogin())) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ await triggerPopupAndHoverItem("#cc-name", 0);
+ await triggerAutofillAndCheckProfile(MOCK_CC_STORAGE[0]);
+ await osKeyStoreLoginShown;
+
+ for (const [id, val] of Object.entries(MOCK_CC_STORAGE[0])) {
+ const element = document.getElementById(id);
+ if (!element) {
+ return;
+ }
+ checkFieldValue(element, val);
+ await checkFieldHighlighted(element, true);
+ }
+
+ await triggerPopupAndHoverItem("#cc-name", 0);
+ await confirmClear("#cc-name");
+ await checkIsFormCleared({
+ "cc-exp-month": "MM",
+ "cc-exp-year": "YY"
+ });
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic form.</p>
+ <p><label>organization: <input id="organization" autocomplete="organization"></label></p>
+ <p><label>streetAddress: <input id="street-address" autocomplete="street-address"></label></p>
+ <p><label>tel: <input id="tel" autocomplete="tel"></label></p>
+ <p><label>country: <input id="country" autocomplete="country"></label></p>
+
+ <p><label>Name: <input id="cc-name" autocomplete="cc-name"></label></p>
+ <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
+ <!-- NOTE: If you're going to write a test like this,
+ ensure that the selected option doesn't match the data that you're trying to autofill,
+ otherwise your test will wait forever for an event that will never fire.
+ I.e, if your saved cc-exp-month is 01, make sure your selected option ISN'T 01.
+ -->
+ <p><label>Expiration month: <select id="cc-exp-month" autocomplete="cc-exp-month">
+ <option value="MM" selected>MM</option>
+ <option value="1">01</option>
+ <option value="2">02</option>
+ <option value="3">03</option>
+ <option value="4">04</option>
+ <option value="5">05</option>
+ <option value="6">06</option>
+ <option value="7">07</option>
+ <option value="8">08</option>
+ <option value="9">09</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ </select>
+ </label></p>
+ <!-- NOTE: If you're going to write a test like this,
+ ensure that the selected option doesn't match the data that you're trying to autofill,
+ otherwise your test will wait forever for an event that will never fire.
+ I.e, if your saved cc-exp-year is 2017, make sure your selected option ISN'T 2017.
+ -->
+ <p><label>Expiration year: <select id="cc-exp-year" autocomplete="cc-exp-year">
+ <option value="YY" selected>YY</option>
+ <option value="2017">2017</option>
+ <option value="2018">2018</option>
+ <option value="2019">2019</option>
+ <option value="2020">2020</option>
+ <option value="2021">2021</option>
+ <option value="2022">2022</option>
+ </select>
+ </label></p>
+ <p><label>CSC: <input id="cc-csc" autocomplete="cc-csc"></label></p>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html
new file mode 100644
index 0000000000..225d828ccb
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../formautofill_common.js"></script>
+ <script type="text/javascript" src="../../../../../../toolkit/components/satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: simple form credit card autofill
+
+<script>
+"use strict";
+
+const MOCK_STORAGE = [{
+ "cc-name": "John Doe",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": 12,
+ "cc-exp-year": 2022,
+}];
+
+async function setupCreditCardStorage() {
+ await addCreditCard(MOCK_STORAGE[0]);
+ await addCreditCard(MOCK_STORAGE[1]);
+}
+
+async function setupFormHistory() {
+ await updateFormHistory([
+ {op: "add", fieldname: "cc-name", value: "John Smith"},
+ {op: "add", fieldname: "cc-number", value: "6011029476355493"},
+ ]);
+}
+
+initPopupListener();
+
+// Show Form History popup for non-autocomplete="off" field only
+add_task(async function history_only_menu_checking() {
+ await setupFormHistory();
+
+ await setInput("#cc-number", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["6011029476355493"], false);
+
+ await setInput("#cc-name", "");
+ synthesizeKey("KEY_ArrowDown");
+ await notExpectPopup();
+});
+
+// Show Form Autofill popup for the credit card fields.
+add_task(async function check_menu_when_both_with_autocomplete_off() {
+ await setupCreditCardStorage();
+
+ await setInput("#cc-number", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primaryAffix: cc.ccNumberFmt.affix,
+ primary: cc.ccNumberFmt.label,
+ secondary: cc["cc-name"],
+ ariaLabel: `${getCCTypeName(cc)} ${cc.ccNumberFmt.affix} ${cc.ccNumberFmt.label} ${cc["cc-name"]}`,
+ })));
+
+ await setInput("#cc-name", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
+ primary: cc["cc-name"],
+ secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
+ ariaLabel: `${getCCTypeName(cc)} ${cc["cc-name"]} ${cc.ccNumberFmt.affix}${cc.ccNumberFmt.label}`,
+ })));
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <form id="form1">
+ <p>This is a Credit Card form with autocomplete="off" cc-name field.</p>
+ <p><label>Name: <input id="cc-name" autocomplete="off"></label></p>
+ <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
+ </form>
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html
new file mode 100644
index 0000000000..c39877d1b7
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html
@@ -0,0 +1,174 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form autofill - preview and highlight with multiple cc number fields</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../formautofill_common.js"></script>
+ <script type="text/javascript" src="../../../../../../toolkit/components/satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: preview and highlight multiple cc number fields
+
+<script>
+"use strict";
+
+const MOCK_STORAGE = [{
+ "cc-name": "Test Name",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": 12,
+ "cc-exp-year": 2022,
+}];
+
+const MOCK_STORAGE_EXPECTED_FILL = [{
+ "cc-name": "Test Name",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": "04",
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": "12",
+ "cc-exp-year": 2022,
+}]
+
+const MOCK_STORAGE_PREVIEW = [{
+ "cc-name": "Test Name",
+ "cc-number": "************1045",
+ "cc-exp-month": "04",
+ "cc-exp-year": "2017",
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "************7870",
+ "cc-exp-month": "12",
+ "cc-exp-year": "2022",
+}];
+
+
+/*
+ This function is similar to checkFormFieldsStyle in formautofill_common.js, but deals with the case
+ when one value is spread across multiple fields.
+
+ This function is needed because of the multiple cc-number filling behavior introduced in Bug 1688607.
+ Since the cc-number is stored as a whole value in the profile,
+ there has to be specific handling in the test to assert the correct fillable value.
+ Otherwise, we would try to grab a value out of profile["cc-number1"] which doesn't exist.
+*/
+async function checkMultipleCCNumberFormStyle(profile, isPreviewing = true) {
+ const elements = document.querySelectorAll("input, select");
+ for (const element of elements) {
+ let fillableValue;
+ if (element.id.includes("cc-number") && isPreviewing) {
+ fillableValue = profile["cc-number"].slice(-8);
+ } else if (element.id.includes("cc-number")) {
+ fillableValue = profile["cc-number"];
+ } else {
+ fillableValue = profile[element.id];
+ }
+ let previewValue = (isPreviewing && fillableValue) || "";
+ await checkFieldHighlighted(element, !!fillableValue);
+ await checkFieldPreview(element, previewValue);
+
+ }
+}
+
+/*
+ This function sets up 'change' event listeners so that we can safely
+ assert an element's value after autofilling has occurred.
+ This is essentially a barebones copy of triggerAutofillAndCheckProfile
+ that exists in formautofill_common.js.
+
+ We can't use triggerAutofillAndCheckProfile because "cc-number1" through "cc-number4"
+ do not exist in the profile.
+ Again, we store the whole cc-number in the profile, not its subsections.
+ So if we tried to grab the element by ID using "cc-number", this element would not exist in the doc,
+ causing triggerAutofillAndCheckProfile to throw an exception.
+*/
+async function setupListeners(elements, profile) {
+ for (const element of elements) {
+ let id = element.id;
+ element.addEventListener("change", () => {
+ let filledValue;
+ if (id == "cc-number1") {
+ filledValue = profile["cc-number"].slice(0, 4);
+ } else if (id == "cc-number2") {
+ filledValue = profile["cc-number"].slice(4, 8);
+ } else if (id == "cc-number3") {
+ filledValue = profile["cc-number"].slice(8, 12);
+ } else if (id == "cc-number4") {
+ filledValue = profile["cc-number"].slice(12, 16);
+ } else {
+ filledValue = profile[element.id];
+ }
+ checkFieldValue(element, filledValue);
+ }, {once: true})
+ }
+
+}
+
+initPopupListener();
+
+add_task(async function setup_storage() {
+ await addCreditCard(MOCK_STORAGE[0]);
+ await addCreditCard(MOCK_STORAGE[1]);
+});
+
+add_task(async function check_preview() {
+ let canTest = await canTestOSKeyStoreLogin();
+ if (!canTest) {
+ todo(canTest, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ let popup = expectPopup();
+ const focusedInput = await setInput("#cc-name", "");
+ await popup;
+ for (let i = 0; i < MOCK_STORAGE_PREVIEW.length; i++) {
+ synthesizeKey("KEY_ArrowDown");
+ await notifySelectedIndex(i);
+ await checkMultipleCCNumberFormStyle(MOCK_STORAGE_PREVIEW[i]);
+ }
+
+ focusedInput.blur();
+});
+
+add_task(async function check_filled_highlight() {
+ let canTest = await canTestOSKeyStoreLogin();
+ if (!canTest) {
+ todo(canTest, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ await triggerPopupAndHoverItem("#cc-name", 0);
+ let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ // filled 1st credit card option
+ synthesizeKey("KEY_Enter");
+ await osKeyStoreLoginShown;
+ let elements = document.querySelectorAll("input, select");
+ let profile = MOCK_STORAGE_EXPECTED_FILL[0];
+ await setupListeners(elements, profile);
+ await checkMultipleCCNumberFormStyle(profile, false);
+});
+</script>
+<p id="display"></p>
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic credit card form.</p>
+ <p>card number subsection 1: <input id="cc-number1" maxlength="4"></p>
+ <p>card number subsection 2: <input id="cc-number2" maxlength="4"></p>
+ <p>card number subsection 3: <input id="cc-number3" maxlength="4"></p>
+ <p>card number subsection 4: <input id="cc-number4" maxlength="4"></p>
+ <p>cardholder name: <input id="cc-name" autocomplete="cc-name"></p>
+ <p>expiration month: <input id="cc-exp-month" autocomplete="cc-exp-month"></p>
+ <p>expiration year: <input id="cc-exp-year" autocomplete="cc-exp-year"></p>
+ </form>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html
new file mode 100644
index 0000000000..090eb9290e
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form autofill - preview and highlight with site prefill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../formautofill_common.js"></script>
+ <script type="text/javascript" src="../../../../../../toolkit/components/satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: preview and highlight field that has been filled by site
+
+<script>
+"use strict";
+
+const MOCK_STORAGE = [{
+ "cc-name": "Test Name",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": 12,
+ "cc-exp-year": 2022,
+}];
+
+const MOCK_STORAGE_PREVIEW = [{
+ "cc-name": "Test Name",
+ "cc-number": "************1045",
+ "cc-exp-month": "04",
+ "cc-exp-year": "2017",
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "************7870",
+ "cc-exp-month": "12",
+ "cc-exp-year": "2022",
+}];
+
+const MOCK_STORAGE_EXPECTED_FILL = [{
+ "cc-name": "Test Name",
+ "cc-number": "4929001587121045",
+ "cc-exp-month": "04",
+ "cc-exp-year": 2017,
+}, {
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "5103059495477870",
+ "cc-exp-month": "12",
+ "cc-exp-year": 2022,
+}];
+
+initPopupListener();
+
+add_task(async function setup_storage() {
+ await addCreditCard(MOCK_STORAGE[0]);
+ await addCreditCard(MOCK_STORAGE[1]);
+});
+
+add_task(async function check_preview() {
+ let canTest = await canTestOSKeyStoreLogin();
+ if (!canTest) {
+ todo(canTest, "Cannot test OS key store login on official builds.");
+ return;
+ }
+
+ let cardholderName = document.querySelector("#cc-name");
+ let sitePrefillValue = cardholderName.value;
+ let popup = expectPopup();
+ const focusedInput = await setInput("#cc-number", "");
+ await popup;
+ for (let i = 0; i < MOCK_STORAGE_PREVIEW.length; i++) {
+ synthesizeKey("KEY_ArrowDown");
+ await notifySelectedIndex(i);
+ await checkFormFieldsStyle(MOCK_STORAGE_PREVIEW[i]);
+ }
+
+ focusedInput.blur();
+ is(cardholderName.value, sitePrefillValue, "value should not have changed because previous value was a site prefill");
+});
+
+add_task(async function check_filled_highlight() {
+ let canTest = await canTestOSKeyStoreLogin();
+ if (!canTest) {
+ todo(canTest, "Cannot test OS key store login on official builds.");
+ return;
+ }
+ await triggerPopupAndHoverItem("#cc-number", 0);
+ let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ // filled 1st credit card option
+ await triggerAutofillAndCheckProfile(MOCK_STORAGE_EXPECTED_FILL[0]);
+ await osKeyStoreLoginShown;
+ await checkFormFieldsStyle(MOCK_STORAGE_EXPECTED_FILL[0], false);
+});
+</script>
+<p id="display"></p>
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic credit card form.</p>
+ <p>card number: <input id="cc-number" autocomplete="cc-number"></p>
+ <p>cardholder name: <input id="cc-name" autocomplete="cc-name" value="JOHN DOE"></p>
+ <p>expiration month: <input id="cc-exp-month" autocomplete="cc-exp-month"></p>
+ <p>expiration year: <input id="cc-exp-year" autocomplete="cc-exp-year"></p>
+ </form>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/mochitest/formautofill_common.js b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
new file mode 100644
index 0000000000..d65f0beb49
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
@@ -0,0 +1,478 @@
+/* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/SimpleTest.js */
+/* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/EventUtils.js */
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+/* eslint-disable no-unused-vars */
+
+"use strict";
+
+let formFillChromeScript;
+let defaultTextColor;
+let defaultDisabledTextColor;
+let expectingPopup = null;
+
+const { FormAutofillUtils } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+);
+
+async function sleep(ms = 500, reason = "Intentionally wait for UI ready") {
+ SimpleTest.requestFlakyTimeout(reason);
+ await new Promise(resolve => setTimeout(resolve, ms));
+}
+
+async function focusAndWaitForFieldsIdentified(
+ input,
+ mustBeIdentified = false
+) {
+ info("expecting the target input being focused and indentified");
+ if (typeof input === "string") {
+ input = document.querySelector(input);
+ }
+ const rootElement = input.form || input.ownerDocument.documentElement;
+ const previouslyFocused = input != document.activeElement;
+
+ input.focus();
+
+ if (mustBeIdentified) {
+ rootElement.removeAttribute("test-formautofill-identified");
+ }
+ if (rootElement.hasAttribute("test-formautofill-identified")) {
+ return;
+ }
+ if (!previouslyFocused) {
+ await new Promise(resolve => {
+ formFillChromeScript.addMessageListener(
+ "FormAutofillTest:FieldsIdentified",
+ function onIdentified() {
+ formFillChromeScript.removeMessageListener(
+ "FormAutofillTest:FieldsIdentified",
+ onIdentified
+ );
+ resolve();
+ }
+ );
+ });
+ }
+ // In order to ensure that "markAsAutofillField" is fully executed, a short period
+ // of timeout is still required.
+ await sleep(300, "Guarantee asynchronous identifyAutofillFields is invoked");
+ rootElement.setAttribute("test-formautofill-identified", "true");
+}
+
+async function setInput(selector, value, userInput = false) {
+ const input = document.querySelector("input" + selector);
+ if (userInput) {
+ SpecialPowers.wrap(input).setUserInput(value);
+ } else {
+ input.value = value;
+ }
+ await focusAndWaitForFieldsIdentified(input);
+
+ return input;
+}
+
+function clickOnElement(selector) {
+ let element = document.querySelector(selector);
+
+ if (!element) {
+ throw new Error("Can not find the element");
+ }
+
+ SimpleTest.executeSoon(() => element.click());
+}
+
+// The equivalent helper function to getAdaptedProfiles in FormAutofillHandler.jsm that
+// transforms the given profile to expected filled profile.
+function _getAdaptedProfile(profile) {
+ const adaptedProfile = Object.assign({}, profile);
+
+ if (profile["street-address"]) {
+ adaptedProfile["street-address"] = FormAutofillUtils.toOneLineAddress(
+ profile["street-address"]
+ );
+ }
+
+ return adaptedProfile;
+}
+
+async function checkFieldHighlighted(elem, expectedValue) {
+ let isHighlightApplied;
+ await SimpleTest.promiseWaitForCondition(function checkHighlight() {
+ isHighlightApplied = elem.matches(":autofill");
+ return isHighlightApplied === expectedValue;
+ }, `Checking #${elem.id} highlight style`);
+
+ is(isHighlightApplied, expectedValue, `Checking #${elem.id} highlight style`);
+}
+
+async function checkFieldPreview(elem, expectedValue) {
+ is(
+ SpecialPowers.wrap(elem).previewValue,
+ expectedValue,
+ `Checking #${elem.id} previewValue`
+ );
+ let isTextColorApplied;
+ await SimpleTest.promiseWaitForCondition(function checkPreview() {
+ const computedStyle = window.getComputedStyle(elem);
+ const actualColor = computedStyle.getPropertyValue("color");
+ if (elem.disabled) {
+ isTextColorApplied = actualColor !== defaultDisabledTextColor;
+ } else {
+ isTextColorApplied = actualColor !== defaultTextColor;
+ }
+ return isTextColorApplied === !!expectedValue;
+ }, `Checking #${elem.id} preview style`);
+
+ is(isTextColorApplied, !!expectedValue, `Checking #${elem.id} preview style`);
+}
+
+async function checkFormFieldsStyle(profile, isPreviewing = true) {
+ const elems = document.querySelectorAll("input, select");
+
+ for (const elem of elems) {
+ let fillableValue;
+ let previewValue;
+ let isElementEligible =
+ FormAutofillUtils.isCreditCardOrAddressFieldType(elem) &&
+ FormAutofillUtils.isFieldAutofillable(elem);
+ if (!isElementEligible) {
+ fillableValue = "";
+ previewValue = "";
+ } else {
+ fillableValue = profile && profile[elem.id];
+ previewValue = (isPreviewing && fillableValue) || "";
+ }
+ await checkFieldHighlighted(elem, !!fillableValue);
+ await checkFieldPreview(elem, previewValue);
+ }
+}
+
+function checkFieldValue(elem, expectedValue) {
+ if (typeof elem === "string") {
+ elem = document.querySelector(elem);
+ }
+ is(elem.value, String(expectedValue), "Checking " + elem.id + " field");
+}
+
+async function triggerAutofillAndCheckProfile(profile) {
+ let adaptedProfile = _getAdaptedProfile(profile);
+ const promises = [];
+ for (const [fieldName, value] of Object.entries(adaptedProfile)) {
+ info(`triggerAutofillAndCheckProfile: ${fieldName}`);
+ const element = document.getElementById(fieldName);
+ const expectingEvent =
+ document.activeElement == element ? "input" : "change";
+ const checkFieldAutofilled = Promise.all([
+ new Promise(resolve => {
+ let beforeInputFired = false;
+ let hadEditor = SpecialPowers.wrap(element).hasEditor;
+ element.addEventListener(
+ "beforeinput",
+ event => {
+ beforeInputFired = true;
+ is(
+ event.inputType,
+ "insertReplacementText",
+ 'inputType value should be "insertReplacementText"'
+ );
+ is(
+ event.data,
+ String(value),
+ `data value of "beforeinput" should be "${value}"`
+ );
+ is(
+ event.dataTransfer,
+ null,
+ 'dataTransfer of "beforeinput" should be null'
+ );
+ is(
+ event.getTargetRanges().length,
+ 0,
+ 'getTargetRanges() of "beforeinput" should return empty array'
+ );
+ is(
+ event.cancelable,
+ SpecialPowers.getBoolPref(
+ "dom.input_event.allow_to_cancel_set_user_input"
+ ),
+ `"beforeinput" event should be cancelable on ${element.tagName} unless it's suppressed by the pref`
+ );
+ is(
+ event.bubbles,
+ true,
+ `"beforeinput" event should always bubble on ${element.tagName}`
+ );
+ resolve();
+ },
+ { once: true }
+ );
+ element.addEventListener(
+ "input",
+ event => {
+ if (element.tagName == "INPUT" && element.type == "text") {
+ if (hadEditor) {
+ ok(
+ beforeInputFired,
+ `"beforeinput" event should've been fired before "input" event on ${element.tagName}`
+ );
+ } else {
+ ok(
+ beforeInputFired,
+ `"beforeinput" event should've been fired before "input" event on ${element.tagName}`
+ );
+ }
+ ok(
+ event instanceof InputEvent,
+ `"input" event should be dispatched with InputEvent interface on ${element.tagName}`
+ );
+ is(
+ event.inputType,
+ "insertReplacementText",
+ 'inputType value should be "insertReplacementText"'
+ );
+ is(event.data, String(value), `data value should be "${value}"`);
+ is(event.dataTransfer, null, "dataTransfer should be null");
+ is(
+ event.getTargetRanges().length,
+ 0,
+ "getTargetRanges() should return empty array"
+ );
+ } else {
+ ok(
+ !beforeInputFired,
+ `"beforeinput" event shouldn't be fired on ${element.tagName}`
+ );
+ ok(
+ event instanceof Event && !(event instanceof UIEvent),
+ `"input" event should be dispatched with Event interface on ${element.tagName}`
+ );
+ }
+ is(
+ event.cancelable,
+ false,
+ `"input" event should be never cancelable on ${element.tagName}`
+ );
+ is(
+ event.bubbles,
+ true,
+ `"input" event should always bubble on ${element.tagName}`
+ );
+ resolve();
+ },
+ { once: true }
+ );
+ }),
+ new Promise(resolve =>
+ element.addEventListener(expectingEvent, resolve, { once: true })
+ ),
+ ]).then(() => checkFieldValue(element, value));
+
+ promises.push(checkFieldAutofilled);
+ }
+ // Press Enter key and trigger form autofill.
+ synthesizeKey("KEY_Enter");
+
+ return Promise.all(promises);
+}
+
+async function onStorageChanged(type) {
+ info(`expecting the storage changed: ${type}`);
+ return new Promise(resolve => {
+ formFillChromeScript.addMessageListener(
+ "formautofill-storage-changed",
+ function onChanged(data) {
+ formFillChromeScript.removeMessageListener(
+ "formautofill-storage-changed",
+ onChanged
+ );
+ is(data.data, type, `Receive ${type} storage changed event`);
+ resolve();
+ }
+ );
+ });
+}
+
+function checkMenuEntries(expectedValues, isFormAutofillResult = true) {
+ let actualValues = getMenuEntries();
+ // Expect one more item would appear at the bottom as the footer if the result is from form autofill.
+ let expectedLength = isFormAutofillResult
+ ? expectedValues.length + 1
+ : expectedValues.length;
+
+ is(actualValues.length, expectedLength, " Checking length of expected menu");
+ for (let i = 0; i < expectedValues.length; i++) {
+ is(actualValues[i], expectedValues[i], " Checking menu entry #" + i);
+ }
+}
+
+function invokeAsyncChromeTask(message, payload = {}) {
+ info(`expecting the chrome task finished: ${message}`);
+ return formFillChromeScript.sendQuery(message, payload);
+}
+
+async function addAddress(address) {
+ await invokeAsyncChromeTask("FormAutofillTest:AddAddress", { address });
+ await sleep();
+}
+
+async function removeAddress(guid) {
+ return invokeAsyncChromeTask("FormAutofillTest:RemoveAddress", { guid });
+}
+
+async function updateAddress(guid, address) {
+ return invokeAsyncChromeTask("FormAutofillTest:UpdateAddress", {
+ address,
+ guid,
+ });
+}
+
+async function checkAddresses(expectedAddresses) {
+ return invokeAsyncChromeTask("FormAutofillTest:CheckAddresses", {
+ expectedAddresses,
+ });
+}
+
+async function cleanUpAddresses() {
+ return invokeAsyncChromeTask("FormAutofillTest:CleanUpAddresses");
+}
+
+async function addCreditCard(creditcard) {
+ await invokeAsyncChromeTask("FormAutofillTest:AddCreditCard", { creditcard });
+ await sleep();
+}
+
+async function removeCreditCard(guid) {
+ return invokeAsyncChromeTask("FormAutofillTest:RemoveCreditCard", { guid });
+}
+
+async function checkCreditCards(expectedCreditCards) {
+ return invokeAsyncChromeTask("FormAutofillTest:CheckCreditCards", {
+ expectedCreditCards,
+ });
+}
+
+async function cleanUpCreditCards() {
+ return invokeAsyncChromeTask("FormAutofillTest:CleanUpCreditCards");
+}
+
+async function cleanUpStorage() {
+ await cleanUpAddresses();
+ await cleanUpCreditCards();
+}
+
+async function canTestOSKeyStoreLogin() {
+ let { canTest } = await invokeAsyncChromeTask(
+ "FormAutofillTest:CanTestOSKeyStoreLogin"
+ );
+ return canTest;
+}
+
+async function waitForOSKeyStoreLogin(login = false) {
+ await invokeAsyncChromeTask("FormAutofillTest:OSKeyStoreLogin", { login });
+}
+
+function patchRecordCCNumber(record) {
+ const number = record["cc-number"];
+ const ccNumberFmt = {
+ affix: "****",
+ label: number.substr(-4),
+ };
+
+ return Object.assign({}, record, { ccNumberFmt });
+}
+
+// Utils for registerPopupShownListener(in satchel_common.js) that handles dropdown popup
+// Please call "initPopupListener()" in your test and "await expectPopup()"
+// if you want to wait for dropdown menu displayed.
+function expectPopup() {
+ info("expecting a popup");
+ return new Promise(resolve => {
+ expectingPopup = resolve;
+ });
+}
+
+function notExpectPopup(ms = 500) {
+ info("not expecting a popup");
+ return new Promise((resolve, reject) => {
+ expectingPopup = reject.bind(this, "Unexpected Popup");
+ // TODO: We don't have an event to notify no popup showing, so wait for 500
+ // ms (in default) to predict any unexpected popup showing.
+ setTimeout(resolve, ms);
+ });
+}
+
+function popupShownListener() {
+ info("popup shown for test ");
+ if (expectingPopup) {
+ expectingPopup();
+ expectingPopup = null;
+ }
+}
+
+function initPopupListener() {
+ registerPopupShownListener(popupShownListener);
+}
+
+async function triggerPopupAndHoverItem(fieldSelector, selectIndex) {
+ await focusAndWaitForFieldsIdentified(fieldSelector);
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ for (let i = 0; i <= selectIndex; i++) {
+ synthesizeKey("KEY_ArrowDown");
+ }
+ await notifySelectedIndex(selectIndex);
+}
+
+function formAutoFillCommonSetup() {
+ // Remove the /creditCard path segement when referenced from the 'creditCard' subdirectory.
+ let chromeURL = SimpleTest.getTestFileURL(
+ "formautofill_parent_utils.js"
+ ).replace(/\/creditCard/, "");
+ formFillChromeScript = SpecialPowers.loadChromeScript(chromeURL);
+ formFillChromeScript.addMessageListener("onpopupshown", ({ results }) => {
+ gLastAutoCompleteResults = results;
+ if (gPopupShownListener) {
+ gPopupShownListener({ results });
+ }
+ });
+
+ add_setup(async () => {
+ info(`expecting the storage setup`);
+ await formFillChromeScript.sendQuery("setup");
+ });
+
+ SimpleTest.registerCleanupFunction(async () => {
+ info(`expecting the storage cleanup`);
+ await formFillChromeScript.sendQuery("cleanup");
+
+ formFillChromeScript.destroy();
+ expectingPopup = null;
+ });
+
+ document.addEventListener(
+ "DOMContentLoaded",
+ function () {
+ defaultTextColor = window
+ .getComputedStyle(document.querySelector("input"))
+ .getPropertyValue("color");
+
+ // This is needed for test_formautofill_preview_highlight.html to work properly
+ let disabledInput = document.querySelector(`input[disabled]`);
+ if (disabledInput) {
+ defaultDisabledTextColor = window
+ .getComputedStyle(disabledInput)
+ .getPropertyValue("color");
+ }
+ },
+ { once: true }
+ );
+}
+
+/*
+ * Extremely over-simplified detection of card type from card number just for
+ * our tests. This is needed to test the aria-label of credit card menu entries.
+ */
+function getCCTypeName(creditCard) {
+ return creditCard["cc-number"][0] == "4" ? "Visa" : "MasterCard";
+}
+
+formAutoFillCommonSetup();
diff --git a/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js b/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
new file mode 100644
index 0000000000..0d9af772b5
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
@@ -0,0 +1,304 @@
+/* eslint-env mozilla/chrome-script */
+
+"use strict";
+
+const { FormAutofill } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofill.sys.mjs"
+);
+const { FormAutofillUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+);
+const { OSKeyStoreTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/OSKeyStoreTestUtils.sys.mjs"
+);
+
+let { formAutofillStorage } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillStorage.sys.mjs"
+);
+
+const { ADDRESSES_COLLECTION_NAME, CREDITCARDS_COLLECTION_NAME } =
+ FormAutofillUtils;
+
+let destroyed = false;
+
+var ParentUtils = {
+ getFormAutofillActor() {
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ let selectedBrowser = win.gBrowser.selectedBrowser;
+ return selectedBrowser.browsingContext.currentWindowGlobal.getActor(
+ "FormAutofill"
+ );
+ },
+
+ _getRecords(collectionName) {
+ return this.getFormAutofillActor().receiveMessage({
+ name: "FormAutofill:GetRecords",
+ data: {
+ searchString: "",
+ collectionName,
+ },
+ });
+ },
+
+ async _storageChangeObserved({
+ topic = "formautofill-storage-changed",
+ type,
+ times = 1,
+ }) {
+ let count = times;
+
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(subject, obsTopic, data) {
+ if ((type && data != type) || !!--count) {
+ return;
+ }
+
+ // every notification type should have the collection name.
+ // We're not allowed to trigger assertions during mochitest
+ // cleanup functions.
+ if (!destroyed) {
+ let allowedNames = [
+ ADDRESSES_COLLECTION_NAME,
+ CREDITCARDS_COLLECTION_NAME,
+ ];
+ assert.ok(
+ allowedNames.includes(subject.wrappedJSObject.collectionName),
+ "should include the collection name"
+ );
+ // every notification except removeAll should have a guid.
+ if (data != "removeAll") {
+ assert.ok(subject.wrappedJSObject.guid, "should have a guid");
+ }
+ }
+ Services.obs.removeObserver(observer, obsTopic);
+ resolve();
+ }, topic);
+ });
+ },
+
+ async _operateRecord(collectionName, type, msgData) {
+ let msgName, times, topic;
+
+ if (collectionName == ADDRESSES_COLLECTION_NAME) {
+ switch (type) {
+ case "add": {
+ msgName = "FormAutofill:SaveAddress";
+ break;
+ }
+ case "update": {
+ msgName = "FormAutofill:SaveAddress";
+ break;
+ }
+ case "remove": {
+ msgName = "FormAutofill:RemoveAddresses";
+ times = msgData.guids.length;
+ break;
+ }
+ default:
+ return;
+ }
+ } else {
+ switch (type) {
+ case "add": {
+ msgData = Object.assign({}, msgData);
+ msgName = "FormAutofill:SaveCreditCard";
+ break;
+ }
+ case "remove": {
+ msgName = "FormAutofill:RemoveCreditCards";
+ times = msgData.guids.length;
+ break;
+ }
+ default:
+ return;
+ }
+ }
+
+ let storageChangePromise = this._storageChangeObserved({
+ type,
+ times,
+ topic,
+ });
+ this.getFormAutofillActor().receiveMessage({
+ name: msgName,
+ data: msgData,
+ });
+ await storageChangePromise;
+ },
+
+ async operateAddress(type, msgData) {
+ await this._operateRecord(ADDRESSES_COLLECTION_NAME, ...arguments);
+ },
+
+ async operateCreditCard(type, msgData) {
+ await this._operateRecord(CREDITCARDS_COLLECTION_NAME, ...arguments);
+ },
+
+ async cleanUpAddresses() {
+ const guids = (await this._getRecords(ADDRESSES_COLLECTION_NAME)).map(
+ record => record.guid
+ );
+
+ if (!guids.length) {
+ return;
+ }
+
+ await this.operateAddress(
+ "remove",
+ { guids },
+ "FormAutofillTest:AddressesCleanedUp"
+ );
+ },
+
+ async cleanUpCreditCards() {
+ if (!FormAutofill.isAutofillCreditCardsAvailable) {
+ return;
+ }
+ const guids = (await this._getRecords(CREDITCARDS_COLLECTION_NAME)).map(
+ record => record.guid
+ );
+
+ if (!guids.length) {
+ return;
+ }
+
+ await this.operateCreditCard(
+ "remove",
+ { guids },
+ "FormAutofillTest:CreditCardsCleanedUp"
+ );
+ },
+
+ setup() {
+ OSKeyStoreTestUtils.setup();
+ },
+
+ async cleanup() {
+ await this.cleanUpAddresses();
+ await this.cleanUpCreditCards();
+ await OSKeyStoreTestUtils.cleanup();
+
+ Services.obs.removeObserver(this, "formautofill-storage-changed");
+ },
+
+ _areRecordsMatching(recordA, recordB, collectionName) {
+ for (let field of formAutofillStorage[collectionName].VALID_FIELDS) {
+ if (recordA[field] !== recordB[field]) {
+ return false;
+ }
+ }
+ // Check the internal field if both addresses have valid value.
+ for (let field of formAutofillStorage.INTERNAL_FIELDS) {
+ if (
+ field in recordA &&
+ field in recordB &&
+ recordA[field] !== recordB[field]
+ ) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ async _checkRecords(collectionName, expectedRecords) {
+ const records = await this._getRecords(collectionName);
+
+ if (records.length !== expectedRecords.length) {
+ return false;
+ }
+
+ for (let record of records) {
+ let matching = expectedRecords.some(expectedRecord => {
+ return ParentUtils._areRecordsMatching(
+ record,
+ expectedRecord,
+ collectionName
+ );
+ });
+
+ if (!matching) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ async checkAddresses({ expectedAddresses }) {
+ return this._checkRecords(ADDRESSES_COLLECTION_NAME, expectedAddresses);
+ },
+
+ async checkCreditCards({ expectedCreditCards }) {
+ return this._checkRecords(CREDITCARDS_COLLECTION_NAME, expectedCreditCards);
+ },
+
+ observe(subject, topic, data) {
+ if (!destroyed) {
+ assert.ok(topic === "formautofill-storage-changed");
+ }
+ sendAsyncMessage("formautofill-storage-changed", {
+ subject: null,
+ topic,
+ data,
+ });
+ },
+};
+
+Services.obs.addObserver(ParentUtils, "formautofill-storage-changed");
+
+Services.mm.addMessageListener("FormAutofill:FieldsIdentified", () => {
+ return null;
+});
+
+addMessageListener("FormAutofillTest:AddAddress", msg => {
+ return ParentUtils.operateAddress("add", msg);
+});
+
+addMessageListener("FormAutofillTest:RemoveAddress", msg => {
+ return ParentUtils.operateAddress("remove", msg);
+});
+
+addMessageListener("FormAutofillTest:UpdateAddress", msg => {
+ return ParentUtils.operateAddress("update", msg);
+});
+
+addMessageListener("FormAutofillTest:CheckAddresses", msg => {
+ return ParentUtils.checkAddresses(msg);
+});
+
+addMessageListener("FormAutofillTest:CleanUpAddresses", msg => {
+ return ParentUtils.cleanUpAddresses();
+});
+
+addMessageListener("FormAutofillTest:AddCreditCard", msg => {
+ return ParentUtils.operateCreditCard("add", msg);
+});
+
+addMessageListener("FormAutofillTest:RemoveCreditCard", msg => {
+ return ParentUtils.operateCreditCard("remove", msg);
+});
+
+addMessageListener("FormAutofillTest:CheckCreditCards", msg => {
+ return ParentUtils.checkCreditCards(msg);
+});
+
+addMessageListener("FormAutofillTest:CleanUpCreditCards", msg => {
+ return ParentUtils.cleanUpCreditCards();
+});
+
+addMessageListener("FormAutofillTest:CanTestOSKeyStoreLogin", msg => {
+ return { canTest: OSKeyStoreTestUtils.canTestOSKeyStoreLogin() };
+});
+
+addMessageListener("FormAutofillTest:OSKeyStoreLogin", async msg => {
+ await OSKeyStoreTestUtils.waitForOSKeyStoreLogin(msg.login);
+});
+
+addMessageListener("setup", async () => {
+ ParentUtils.setup();
+});
+
+addMessageListener("cleanup", async () => {
+ destroyed = true;
+ await ParentUtils.cleanup();
+});
diff --git a/browser/extensions/formautofill/test/mochitest/mochitest.ini b/browser/extensions/formautofill/test/mochitest/mochitest.ini
new file mode 100644
index 0000000000..2964fd5b13
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/mochitest.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+prefs =
+ extensions.formautofill.creditCards.supported=on
+ extensions.formautofill.creditCards.enabled=true
+ extensions.formautofill.addresses.supported=on
+ extensions.formautofill.addresses.enabled=true
+skip-if = toolkit == 'android' # bug 1730213
+support-files =
+ ../../../../../toolkit/components/satchel/test/satchel_common.js
+ ../../../../../toolkit/components/satchel/test/parent_utils.js
+ formautofill_common.js
+ formautofill_parent_utils.js
+
+[test_address_level_1_submission.html]
+[test_autofill_and_ordinal_forms.html]
+[test_autofocus_form.html]
+[test_basic_autocomplete_form.html]
+[test_form_changes.html]
+[test_formautofill_preview_highlight.html]
+skip-if = verify
+[test_multi_locale_CA_address_form.html]
+[test_multiple_forms.html]
+[test_on_address_submission.html]
diff --git a/browser/extensions/formautofill/test/mochitest/test_address_level_1_submission.html b/browser/extensions/formautofill/test/mochitest/test_address_level_1_submission.html
new file mode 100644
index 0000000000..9d6ad1f7ea
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/test_address_level_1_submission.html
@@ -0,0 +1,102 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill submission for a country without address-level1</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: Test autofill submission for a country without address-level1
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+const TEST_ADDRESSES = [{
+ organization: "Mozilla",
+ "street-address": "123 Sesame Street",
+ "address-level1": "AL",
+ country: "DE",
+ timesUsed: 1,
+}];
+
+add_task(async function test_DE_is_valid_testcase() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.addresses.capture.enabled", true],
+ ["extensions.formautofill.addresses.supportedCountries", "US,CA,DE"],
+ ["extensions.formautofill.creditCards.supportedCountries", "US,CA,DE"],
+ ],
+ });
+ let chromeScript = SpecialPowers.loadChromeScript(function test_country_data() {
+ /* eslint-env mozilla/chrome-script */
+ const {AddressDataLoader} = ChromeUtils.importESModule("resource://gre/modules/shared/FormAutofillUtils.sys.mjs");
+ let data = AddressDataLoader.getData("DE");
+ addMessageListener("CheckSubKeys", () => {
+ return !data.defaultLocale.sub_keys;
+ });
+ });
+
+ SimpleTest.registerCleanupFunction(() => {
+ chromeScript.destroy();
+ });
+
+ let result = await chromeScript.sendQuery("CheckSubKeys");
+ ok(result, "Check that there are no sub_keys for the test country");
+});
+
+add_task(async function test_form_will_submit_without_sub_keys() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // This needs to match the country in the previous test and must have no sub_keys.
+ ["browser.search.region", "DE"],
+ // We already verified the first time use case in browser test
+ ["extensions.formautofill.firstTimeUse", false],
+ ["extensions.formautofill.addresses.capture.enabled", true],
+ ["extensions.formautofill.addresses.supportedCountries", "US,CA,DE"],
+ ["extensions.formautofill.addresses.supported", "detect"]
+ ],
+ });
+ // Click a field to get the form handler created
+ await focusAndWaitForFieldsIdentified("input[autocomplete='organization']");
+
+ let loadPromise = new Promise(resolve => {
+ /* eslint-disable-next-line mozilla/balanced-listeners */
+ document.getElementById("submit_frame").addEventListener("load", resolve);
+ });
+
+ clickOnElement("input[type=submit]");
+ await onStorageChanged("add");
+ // Check if timesUsed is set correctly
+ let matching = await checkAddresses(TEST_ADDRESSES);
+ ok(matching, "Address saved as expected");
+
+ await loadPromise;
+ isnot(window.submit_frame.location.href, "about:blank", "Check form submitted");
+});
+
+</script>
+
+<div>
+ <!-- Submit to the frame so that the test doesn't get replaced. We don't return
+ -- false in onsubmit since we're testing the submission succeeds. -->
+ <iframe id="submit_frame" name="submit_frame"></iframe>
+ <form action="/" target="submit_frame" method="POST">
+ <p><label>organization: <input autocomplete="organization" value="Mozilla"></label></p>
+ <p><label>streetAddress: <input autocomplete="street-address" value="123 Sesame Street"></label></p>
+ <p><label>address-level1: <select autocomplete="address-level1">
+ <option selected>AL</option>
+ <option>AK</option>
+ </select></label></p>
+ <p><label>country: <input autocomplete="country" value="DE"></label></p>
+ <p><input type="submit"></p>
+ </form>
+
+</div>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html b/browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html
new file mode 100644
index 0000000000..5c143c3f4a
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill submit</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+SpecialPowers.pushPrefEnv({"set": [["security.allow_eval_with_system_principal", true]]});
+
+let MOCK_STORAGE = [{
+ "given-name": "John",
+ "additional-name": "R",
+ "family-name": "Smith",
+ "organization": "Sesame Street",
+ "street-address": "123 Sesame Street.",
+ "tel": "+13453453456",
+ "country": "US",
+ "address-level1": "NY",
+}];
+
+initPopupListener();
+
+add_task(async function setupStorage() {
+ await addAddress(MOCK_STORAGE[0]);
+
+ await updateFormHistory([
+ {op: "add", fieldname: "username", value: "petya"},
+ {op: "add", fieldname: "current-password", value: "abrh#25_,K"},
+ ]);
+});
+
+add_task(async function check_switch_autofill_form_popup() {
+ await setInput("#tel", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(
+ [
+ `{"primary":"+13453453456","secondary":"123 Sesame Street."}`,
+ `{"primary":"","secondary":"","categories":["name","organization","address","tel"],"focusedCategory":"tel"}`,
+ ],
+ false
+ );
+
+ await testMenuEntry(0, "!(el instanceof MozElements.MozAutocompleteRichlistitem)");
+});
+
+add_task(async function check_switch_oridnal_form_popup() {
+ // We need an intentional wait here before switching form.
+ await sleep();
+ await setInput("#username", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["petya"], false);
+
+ await testMenuEntry(0, "el instanceof MozElements.MozAutocompleteRichlistitem");
+});
+
+add_task(async function check_switch_autofill_form_popup_back() {
+ // We need an intentional wait here before switching form.
+ await sleep();
+ await setInput("#tel", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(
+ [
+ `{"primary":"+13453453456","secondary":"123 Sesame Street."}`,
+ `{"primary":"","secondary":"","categories":["name","organization","address","tel"],"focusedCategory":"tel"}`,
+ ],
+ false
+ );
+
+ await testMenuEntry(0, "!(el instanceof MozElements.MozAutocompleteRichlistitem)");
+});
+
+</script>
+
+<div>
+
+ <h2>Address form</h2>
+ <form class="alignedLabels">
+ <label>given-name: <input autocomplete="given-name" autofocus></label>
+ <label>additional-name: <input id="additional-name" autocomplete="additional-name"></label>
+ <label>family-name: <input autocomplete="family-name"></label>
+ <label>organization: <input autocomplete="organization"></label>
+ <label>street-address: <input autocomplete="street-address"></label>
+ <label>address-level1: <input autocomplete="address-level1"></label>
+ <label>postal-code: <input autocomplete="postal-code"></label>
+ <label>country: <input autocomplete="country"></label>
+ <label>country-name: <input autocomplete="country-name"></label>
+ <label>tel: <input id="tel" autocomplete="tel"></label>
+ <p>
+ <input type="submit" value="Submit">
+ <button type="reset">Reset</button>
+ </p>
+ </form>
+
+ <h2>Ordinal form</h2>
+ <form class="alignedLabels">
+ <label>username: <input id="username" autocomplete="username"></label>
+ <p><input type="submit" value="Username"></p>
+ </form>
+
+</div>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/mochitest/test_autofocus_form.html b/browser/extensions/formautofill/test/mochitest/test_autofocus_form.html
new file mode 100644
index 0000000000..e2240474c8
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/test_autofocus_form.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: autocomplete on an autofocus form
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+let MOCK_STORAGE = [{
+ organization: "Sesame Street",
+ "street-address": "123 Sesame Street.",
+ tel: "1-345-345-3456",
+}, {
+ organization: "Mozilla",
+ "street-address": "331 E. Evelyn Avenue",
+ tel: "1-650-903-0800",
+}];
+
+initPopupListener();
+
+async function setupAddressStorage() {
+ await addAddress(MOCK_STORAGE[0]);
+ await addAddress(MOCK_STORAGE[1]);
+}
+
+add_task(async function check_autocomplete_on_autofocus_field() {
+ await setupAddressStorage();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({primary: address.organization, secondary: address["street-address"]})
+ ));
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic form.</p>
+ <p><label>organization: <input id="organization" name="organization" autocomplete="organization" type="text"></label></p>
+ <script>
+ "use strict";
+ // Focuses the input before DOMContentLoaded
+ document.getElementById("organization").focus();
+ </script>
+ <p><label>streetAddress: <input id="street-address" name="street-address" autocomplete="street-address" type="text"></label></p>
+ <p><label>tel: <input id="tel" name="tel" autocomplete="tel" type="text"></label></p>
+ <p><label>country: <input id="country" name="country" autocomplete="country" type="text"></label></p>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html b/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html
new file mode 100644
index 0000000000..a642b2abca
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html
@@ -0,0 +1,220 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: simple form address autofill
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+let MOCK_STORAGE = [{
+ organization: "Sesame Street",
+ "street-address": "123 Sesame Street.\n2-line\n3-line",
+ tel: "+13453453456",
+ country: "US",
+ "address-level1": "NY",
+}, {
+ organization: "Mozilla",
+ "street-address": "331 E. Evelyn Avenue\n2-line\n3-line",
+ tel: "+16509030800",
+ country: "US",
+ "address-level1": "CA",
+}];
+
+async function setupAddressStorage() {
+ await addAddress(MOCK_STORAGE[0]);
+ await addAddress(MOCK_STORAGE[1]);
+}
+
+async function setupFormHistory() {
+ await updateFormHistory([
+ {op: "add", fieldname: "tel", value: "+1234567890"},
+ {op: "add", fieldname: "email", value: "foo@mozilla.com"},
+ ]);
+}
+
+initPopupListener();
+
+// Form with history only.
+add_task(async function history_only_menu_checking() {
+ await setupFormHistory();
+
+ await setInput("#tel", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["+1234567890"], false);
+});
+
+// Display history search result if less than 3 inputs are covered by all saved
+// fields in the storage.
+add_task(async function all_saved_fields_less_than_threshold() {
+ await addAddress({
+ email: "test@test.com",
+ });
+
+ await setInput("#email", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["foo@mozilla.com"], false);
+
+ await cleanUpAddresses();
+});
+
+// Form with both history and address storage.
+add_task(async function check_menu_when_both_existed() {
+ await setupAddressStorage();
+
+ await setInput("#organization", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({
+ primary: address.organization,
+ secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ })
+ ));
+
+ await setInput("#street-address", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({
+ primary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ secondary: address.organization,
+ })
+ ));
+
+ await setInput("#tel", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({
+ primary: address.tel,
+ secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ })
+ ));
+
+ await setInput("#address-line1", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({
+ primary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ secondary: address.organization,
+ })
+ ));
+});
+
+// Display history search result if no matched data in addresses.
+add_task(async function check_fallback_for_mismatched_field() {
+ await setInput("#email", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["foo@mozilla.com"], false);
+});
+
+// Display history search result if address autofill is disabled.
+add_task(async function check_search_result_for_pref_off() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.formautofill.addresses.enabled", false]],
+ });
+
+ await setInput("#tel", "");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(["+1234567890"], false);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Autofill the address from dropdown menu.
+add_task(async function check_fields_after_form_autofill() {
+ const focusedInput = await setInput("#organization", "Moz");
+ await notExpectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({
+ primary: address.organization,
+ secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ })
+ ).slice(1));
+ synthesizeKey("KEY_ArrowDown");
+ await triggerAutofillAndCheckProfile(MOCK_STORAGE[1]);
+ synthesizeKey("KEY_Escape");
+ is(focusedInput.value, "Mozilla", "Filled field shouldn't be reverted by ESC key");
+});
+
+// Fallback to history search after autofill address.
+add_task(async function check_fallback_after_form_autofill() {
+ await setInput("#tel", "", true);
+ await triggerPopupAndHoverItem("#tel", 0);
+ checkMenuEntries(["+1234567890"], false);
+ await triggerAutofillAndCheckProfile({
+ tel: "+1234567890",
+ });
+});
+
+// Resume form autofill once all the autofilled fileds are changed.
+add_task(async function check_form_autofill_resume() {
+ document.querySelector("#tel").blur();
+ document.querySelector("#form1").reset();
+ await setInput("#tel", "");
+ await triggerPopupAndHoverItem("#tel", 0);
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({
+ primary: address.tel,
+ secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ })
+ ));
+ await triggerAutofillAndCheckProfile(MOCK_STORAGE[0]);
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic form.</p>
+ <p><label>organization: <input id="organization" name="organization" autocomplete="organization" type="text"></label></p>
+ <p><label>streetAddress: <input id="street-address" name="street-address" autocomplete="street-address" type="text"></label></p>
+ <p><label>address-line1: <input id="address-line1" name="address-line1" autocomplete="address-line1" type="text"></label></p>
+ <p><label>tel: <input id="tel" name="tel" autocomplete="tel" type="text"></label></p>
+ <p><label>email: <input id="email" name="email" autocomplete="email" type="text"></label></p>
+ <p><label>country: <select id="country" name="country" autocomplete="country">
+ <option/>
+ <option value="US">United States</option>
+ </select></label></p>
+ <p><label>states: <select id="address-level1" name="address-level1" autocomplete="address-level1">
+ <option/>
+ <option value="CA">California</option>
+ <option value="NY">New York</option>
+ <option value="WA">Washington</option>
+ </select></label></p>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/mochitest/test_form_changes.html b/browser/extensions/formautofill/test/mochitest/test_form_changes.html
new file mode 100644
index 0000000000..2eace91a53
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/test_form_changes.html
@@ -0,0 +1,128 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: autocomplete on an autofocus form
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+let MOCK_STORAGE = [{
+ name: "John Doe",
+ organization: "Sesame Street",
+ "address-level2": "Austin",
+ tel: "+13453453456",
+}, {
+ name: "Foo Bar",
+ organization: "Mozilla",
+ "address-level2": "San Francisco",
+ tel: "+16509030800",
+}];
+
+initPopupListener();
+
+async function setupAddressStorage() {
+ await addAddress(MOCK_STORAGE[0]);
+ await addAddress(MOCK_STORAGE[1]);
+}
+
+function addInputField(form, className) {
+ let newElem = document.createElement("input");
+ newElem.name = className;
+ newElem.autocomplete = className;
+ newElem.type = "text";
+ form.appendChild(newElem);
+}
+
+async function checkFieldsAutofilled(formId, profile) {
+ const elements = document.querySelectorAll(`#${formId} input`);
+ for (const element of elements) {
+ await SimpleTest.promiseWaitForCondition(() => {
+ return element.value == profile[element.name];
+ });
+ await checkFieldHighlighted(element, true);
+ }
+}
+
+async function checkFormChangeHappened(formId) {
+ info("expecting form changed");
+ await focusAndWaitForFieldsIdentified(`#${formId} input[name=tel]`);
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ synthesizeKey("KEY_ArrowDown");
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({primary: address.tel, secondary: address.name})
+ ));
+
+ // Click the first entry of the autocomplete popup and make sure all fields are autofilled
+ synthesizeKey("KEY_Enter");
+ await checkFieldsAutofilled(formId, MOCK_STORAGE[0]);
+ // This is for checking the changes of element count.
+ addInputField(document.querySelector(`#${formId}`), "address-level2");
+
+ await focusAndWaitForFieldsIdentified(`#${formId} input[name=name]`);
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+
+ // Click on an autofilled field would show an autocomplete popup with "clear form" entry
+ checkMenuEntries([
+ JSON.stringify({primary: "", secondary: ""}), // Clear Autofill Form
+ JSON.stringify({primary: "", secondary: ""}) // FormAutofill Preferemce
+ ], false);
+
+ // This is for checking the changes of element removed and added then.
+ document.querySelector(`#${formId} input[name=address-level2]`).remove();
+ addInputField(document.querySelector(`#${formId}`), "address-level2");
+
+ await focusAndWaitForFieldsIdentified(`#${formId} input[name=address-level2]`, true);
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({primary: address["address-level2"], secondary: address.name})
+ ));
+
+ // Make sure everything is autofilled in the end
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Enter");
+ await checkFieldsAutofilled(formId, MOCK_STORAGE[0]);
+}
+
+add_task(async function init_storage() {
+ await setupAddressStorage();
+});
+
+add_task(async function check_change_happened_in_form() {
+ await checkFormChangeHappened("form1");
+});
+
+add_task(async function check_change_happened_in_body() {
+ await checkFormChangeHappened("form2");
+});
+</script>
+
+<p id="display"></p>
+<div id="content">
+ <form id="form1">
+ <p><label>organization: <input name="organization" autocomplete="organization" type="text"></label></p>
+ <p><label>tel: <input name="tel" autocomplete="tel" type="text"></label></p>
+ <p><label>name: <input name="name" autocomplete="name" type="text"></label></p>
+ </form>
+ <div id="form2">
+ <p><label>organization: <input name="organization" autocomplete="organization" type="text"></label></p>
+ <p><label>tel: <input name="tel" autocomplete="tel" type="text"></label></p>
+ <p><label>name: <input name="name" autocomplete="name" type="text"></label></p>
+ </div>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html b/browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html
new file mode 100644
index 0000000000..b32b036c9c
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form autofill - preview and highlight</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: preview and highlight
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+const MOCK_STORAGE = [
+ {
+ organization: "Sesame Street",
+ "street-address": "123 Sesame Street.",
+ tel: "+13453453456",
+ },
+ {
+ organization: "Mozilla",
+ "street-address": "331 E. Evelyn Avenue",
+ },
+ {
+ organization: "Tel org",
+ tel: "+12223334444",
+ },
+ {
+ organization: "Random Org",
+ "address-level1": "First Admin Level",
+ tel: "+13453453456",
+ },
+ {
+ organization: "readonly Org",
+ "address-level1": "First Admin Level",
+ tel: "+13453453456",
+ name: "John Doe",
+ },
+ {
+ organization: "test org",
+ "address-level2": "Not a Town",
+ tel: "+13453453456",
+ name: "John Doe",
+ }
+];
+
+
+initPopupListener();
+
+add_task(async function setup_storage() {
+ for (const storage of MOCK_STORAGE) {
+ await addAddress(storage);
+ }
+});
+
+add_task(async function check_preview() {
+ const focusedInput = await setInput("#organization", "");
+
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ await checkFormFieldsStyle(null);
+
+ for (let i = 0; i < MOCK_STORAGE.length; i++) {
+ info(`Checking organization: ${MOCK_STORAGE[i].organization} preview`);
+ synthesizeKey("KEY_ArrowDown");
+ await notifySelectedIndex(i);
+ await checkFormFieldsStyle(MOCK_STORAGE[i]);
+ }
+
+ // Navigate to the footer
+ synthesizeKey("KEY_ArrowDown");
+ await notifySelectedIndex(MOCK_STORAGE.length);
+ await checkFormFieldsStyle(null);
+
+ synthesizeKey("KEY_ArrowDown");
+ await notifySelectedIndex(-1);
+ await checkFormFieldsStyle(null);
+
+ focusedInput.blur();
+});
+
+add_task(async function check_filled_highlight() {
+ await triggerPopupAndHoverItem("#organization", 0);
+ // filled 1st address
+ await triggerAutofillAndCheckProfile(MOCK_STORAGE[0]);
+ await checkFormFieldsStyle(MOCK_STORAGE[0], false);
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1">
+ <p>This is a basic form.</p>
+ <p><label>organization: <input id="organization" autocomplete="organization"></label></p>
+ <p><label>streetAddress: <input id="street-address" autocomplete="street-address"></label></p>
+ <p><label>tel: <input id="tel" autocomplete="tel"></label></p>
+ <p><label>country: <input id="country" autocomplete="country"></label></p>
+ <p><label>address-level1:
+ <select id="address-level1" autocomplete="address-level1">
+ <option>First Admin Level</option>
+ <option>Second Admin Level</option>
+ </select>
+ </label></p>
+ <p><label>full name: <input id="name" autocomplete="name" readonly value="UNCHANGED"></label></p>
+ <p><label>address-level2: <input id="address-level2" autocomplete="address-level2" disabled value="Town"></label></p>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html b/browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html
new file mode 100644
index 0000000000..48e0caa785
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html
@@ -0,0 +1,273 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: simple form address autofill
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+let MOCK_STORAGE = [{
+ organization: "Mozilla Vancouver",
+ "street-address": "163 W Hastings St.\n#209\n3-line",
+ tel: "+17787851540",
+ country: "CA",
+ "address-level1": "BC",
+}, {
+ organization: "Mozilla Toronto",
+ "street-address": "366 Adelaide St.\nW Suite 500\n3-line",
+ tel: "+14168483114",
+ country: "CA",
+ "address-level1": "ON",
+}, {
+ organization: "Prince of Wales Northern Heritage",
+ "street-address": "4750 48 St.\nYellowknife\n3-line",
+ tel: "+18677679347",
+ country: "CA",
+ "address-level1": "Northwest Territories",
+}, {
+ organization: "ExpoCité",
+ "street-address": "250 Boulevard Wilfrid-Hamel\nVille de Québec\n3-line",
+ tel: "+14186917110",
+ country: "CA",
+ "address-level1": "Québec",
+}];
+
+function checkElementFilled(element, expectedvalue) {
+ let focusFired = false;
+ let inputFired = false;
+ let changeFired = false;
+ return [
+ new Promise(resolve => {
+ element.addEventListener("focus", function onChange() {
+ ok(true, "Checking " + element.name + " field fires focus event");
+ focusFired = true;
+ resolve();
+ }, {once: true});
+ }),
+ new Promise(resolve => {
+ let beforeInputFired = false;
+ let oldValue = element.value;
+ element.addEventListener("beforeinput", function onBeforeInput(event) {
+ ok(true, "Checking " + element.name + " field fires beforeinput event");
+ ok(focusFired, "Focus fired before `beforeinput` event");
+ beforeInputFired = true;
+ ok(event instanceof InputEvent,
+ `"beforeinput" event should be dispatched with InputEvent interface on ${element.name}`);
+ is(event.inputType, "insertReplacementText",
+ 'inputType value of "beforeinput" event should be "insertReplacementText"');
+ is(event.data, expectedvalue,
+ 'data value of "beforeinput" event should be same as expected value');
+ is(event.dataTransfer, null,
+ 'dataTransfer value of "beforeinput" event should be null');
+ is(event.getTargetRanges().length, 0,
+ 'getTargetRanges() of "beforeinput" event should return empty array');
+ is(event.cancelable, SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input"),
+ `"beforeinput" event should be cancelable on ${element.name} unless it's suppressed by the pref`);
+ is(event.bubbles, true,
+ `"input" event should always bubble on ${element.name}`);
+ is(element.value, oldValue,
+ 'value of the element should not be modified at "beforeinput" event yet');
+ }, {once: true});
+ element.addEventListener("input", function onInput(event) {
+ ok(true, "Checking " + element.name + " field fires input event");
+ if (element.tagName == "INPUT" && element.type == "text") {
+ ok(beforeInputFired, `"beforeinput" event shoud've been fired on ${element.name} before "input" event`);
+ ok(event instanceof InputEvent,
+ `"input" event should be dispatched with InputEvent interface on ${element.name}`);
+ is(event.inputType, "insertReplacementText",
+ "inputType value should be \"insertReplacementText\"");
+ is(event.data, expectedvalue,
+ "data value should be same as expected value");
+ is(event.dataTransfer, null,
+ "dataTransfer value should be null");
+ is(event.getTargetRanges().length, 0,
+ 'getTargetRanges() should return empty array');
+ is(element.value, expectedvalue,
+ 'value of the element should be modified at "input" event');
+ } else {
+ ok(!beforeInputFired, `"beforeinput" event shoudn't be fired on ${element.name} before "input" event`);
+ ok(event instanceof Event && !(event instanceof UIEvent),
+ `"input" event should be dispatched with Event interface on ${element.name}`);
+ }
+ is(event.cancelable, false,
+ `"input" event should be never cancelable on ${element.name}`);
+ is(event.bubbles, true,
+ `"input" event should always bubble on ${element.name}`);
+ inputFired = true;
+ resolve();
+ }, {once: true});
+ }),
+ new Promise(resolve => {
+ element.addEventListener("change", function onChange() {
+ ok(true, "Checking " + element.name + " field fires change event");
+ is(element.value, expectedvalue, "Checking " + element.name + " field");
+ ok(focusFired, "Focus fired before `change` event");
+ changeFired = true;
+ resolve();
+ }, {once: true});
+ }),
+ new Promise(resolve => {
+ element.addEventListener("blur", function onChange() {
+ ok(true, "Checking " + element.name + " field fires blur event");
+ ok(changeFired, "Change fired before `blur` event");
+ ok(inputFired, "Input fired before `blur` event");
+ is(element.value, expectedvalue, "Checking " + element.name + " field");
+ resolve();
+ }, {once: true});
+ }),
+ ];
+}
+
+function checkAutoCompleteInputFilled(element, expectedvalue) {
+ return new Promise(resolve => {
+ element.addEventListener("input", function onInput() {
+ is(element.value, expectedvalue, "Checking " + element.name + " field");
+ resolve();
+ }, {once: true});
+ });
+}
+
+function checkFormFilled(selector, address) {
+ info("expecting form filled");
+ let promises = [];
+ let form = document.querySelector(selector);
+ for (let prop in address) {
+ let element = form.querySelector(`[name=${prop}]`);
+ if (document.activeElement == element) {
+ promises.push(checkAutoCompleteInputFilled(element, address[prop]));
+ } else {
+ let converted = address[prop];
+ if (prop == "street-address") {
+ converted = FormAutofillUtils.toOneLineAddress(converted);
+ }
+ promises.push(...checkElementFilled(element, converted));
+ }
+ }
+ synthesizeKey("KEY_Enter");
+ return Promise.all(promises);
+}
+
+async function setupAddressStorage() {
+ for (let address of MOCK_STORAGE) {
+ await addAddress(address);
+ }
+}
+
+initPopupListener();
+
+add_setup(async () => {
+ // This test relies on being able to fill a Canadian address which isn't possible
+ // without `supportedCountries` allowing Canada
+ await SpecialPowers.pushPrefEnv({"set": [["extensions.formautofill.supportedCountries", "US,CA"]]});
+
+ await setupAddressStorage();
+});
+
+// Autofill the address with address level 1 code.
+add_task(async function autofill_with_level1_code() {
+ await setInput("#organization-en", "Mozilla Toront");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+
+ synthesizeKey("KEY_ArrowDown");
+ // Replace address level 1 code with full name in English for test result
+ let result = Object.assign({}, MOCK_STORAGE[1], {"address-level1": "Ontario"});
+ await checkFormFilled("#form-en", result);
+
+ await setInput("#organization-fr", "Mozilla Vancouve");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+
+ synthesizeKey("KEY_ArrowDown");
+ // Replace address level 1 code with full name in French for test result
+ result = Object.assign({}, MOCK_STORAGE[0], {"address-level1": "Colombie-Britannique"});
+ await checkFormFilled("#form-fr", result);
+ document.querySelector("#form-en").reset();
+ document.querySelector("#form-fr").reset();
+});
+
+// Autofill the address with address level 1 full name.
+add_task(async function autofill_with_level1_full_name() {
+ await setInput("#organization-en", "ExpoCit");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+
+ synthesizeKey("KEY_ArrowDown");
+ // Replace address level 1 code with full name in French for test result
+ let result = Object.assign({}, MOCK_STORAGE[3], {"address-level1": "Quebec"});
+ await checkFormFilled("#form-en", result);
+
+ await setInput("#organization-fr", "Prince of Wales");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+
+ synthesizeKey("KEY_ArrowDown");
+ // Replace address level 1 code with full name in English for test result
+ result = Object.assign({}, MOCK_STORAGE[2], {"address-level1": "Territoires du Nord-Ouest"});
+ await checkFormFilled("#form-fr", result);
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form-en">
+ <p>This is a basic CA form with en address level 1 select.</p>
+ <p><label>organization: <input id="organization-en" name="organization" autocomplete="organization" type="text"></label></p>
+ <p><label>streetAddress: <input id="street-address-en" name="street-address" autocomplete="street-address" type="text"></label></p>
+ <p><label>address-line1: <input id="address-line1-en" name="address-line1" autocomplete="address-line1" type="text"></label></p>
+ <p><label>tel: <input id="tel-en" name="tel" autocomplete="tel" type="text"></label></p>
+ <p><label>email: <input id="email-en" name="email" autocomplete="email" type="text"></label></p>
+ <p><label>country: <select id="country-en" name="country" autocomplete="country">
+ <option/>
+ <option value="US">United States</option>
+ <option value="CA">Canada</option>
+ </select></label></p>
+ <p><label>states: <select id="address-level1-en" name="address-level1" autocomplete="address-level1">
+ <option/>
+ <option value="British Columbia">British Columbia</option>
+ <option value="Ontario">Ontario</option>
+ <option value="Northwest Territories">Northwest Territories</option>
+ <option value="Quebec">Quebec</option>
+ </select></label></p>
+ </form>
+
+ <form id="form-fr">
+ <p>This is a basic CA form with fr address level 1 select.</p>
+ <p><label>organization: <input id="organization-fr" name="organization" autocomplete="organization" type="text"></label></p>
+ <p><label>streetAddress: <input id="street-address-fr" name="street-address" autocomplete="street-address" type="text"></label></p>
+ <p><label>address-line1: <input id="address-line1-fr" name="address-line1" autocomplete="address-line1" type="text"></label></p>
+ <p><label>tel: <input id="tel-fr" name="tel" autocomplete="tel" type="text"></label></p>
+ <p><label>email: <input id="email-fr" name="email" autocomplete="email" type="text"></label></p>
+ <p><label>country: <select id="country-fr" name="country" autocomplete="country">
+ <option/>
+ <option value="US">United States</option>
+ <option value="CA">Canada</option>
+ </select></label></p>
+ <p><label>states: <select id="address-level1-fr" name="address-level1" autocomplete="address-level1">
+ <option/>
+ <option value="Colombie-Britannique">Colombie-Britannique</option>
+ <option value="Ontario">Ontario</option>
+ <option value="Territoires du Nord-Ouest">Territoires du Nord-Ouest</option>
+ <option value="Québec">Québec</option>
+ </select></label></p>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/mochitest/test_multiple_forms.html b/browser/extensions/formautofill/test/mochitest/test_multiple_forms.html
new file mode 100644
index 0000000000..feea55aae6
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/test_multiple_forms.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill submit</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+let MOCK_STORAGE = [{
+ "given-name": "John",
+ "additional-name": "R",
+ "family-name": "Smith",
+}];
+
+initPopupListener();
+
+add_task(async function setupStorage() {
+ await addAddress(MOCK_STORAGE[0]);
+});
+
+add_task(async function check_switch_form_popup() {
+ await setInput("#additional-name", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+
+ // We need an intentional wait here before switching form.
+ await sleep();
+ await setInput("#organization", "");
+ synthesizeKey("KEY_ArrowDown");
+ const {open: popupOpen} = await getPopupState();
+ is(popupOpen, false);
+
+ await sleep();
+ await setInput("#given-name", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+});
+
+</script>
+
+<div>
+
+ <form>
+ <label>Name:<input id="name" autocomplete="name"></label>
+ <label>Organization:<input id="organization" autocomplete="organization"></label>
+ <label>City:<input autocomplete="address-level2"></label>
+ </form>
+
+ <form>
+ <label>Given-Name: <input id="given-name" autocomplete="given-name"></label>
+ <label>Additional-Name/Middle: <input id="additional-name" autocomplete="additional-name"></label>
+ <label>FamilyName-LastName: <input id="family-name" autocomplete="family-name"></label>
+ </form>
+
+</div>
+</body>
+</html>
diff --git a/browser/extensions/formautofill/test/mochitest/test_on_address_submission.html b/browser/extensions/formautofill/test/mochitest/test_on_address_submission.html
new file mode 100644
index 0000000000..79da48c77b
--- /dev/null
+++ b/browser/extensions/formautofill/test/mochitest/test_on_address_submission.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill submit</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="formautofill_common.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form autofill test: check if address is saved/updated correctly
+
+<script>
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+"use strict";
+
+let TEST_ADDRESSES = [{
+ organization: "Sesame Street",
+ "street-address": "123 Sesame Street.",
+ tel: "+13453453456",
+}, {
+ organization: "Mozilla",
+ "street-address": "331 E. Evelyn Avenue",
+ tel: "+16509030800",
+}];
+
+add_task(async function setup_prefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.addresses.enabled", true],
+ ["extensions.formautofill.addresses.capture.enabled", true],
+ ],
+ });
+});
+
+initPopupListener();
+
+// Submit first address for saving.
+add_task(async function check_storage_after_form_submitted() {
+ // We already verified the first time use case in browser test
+ await SpecialPowers.pushPrefEnv({
+ "set": [["extensions.formautofill.firstTimeUse", false]],
+ });
+
+ for (let key in TEST_ADDRESSES[0]) {
+ await setInput("#" + key, TEST_ADDRESSES[0][key]);
+ }
+
+ clickOnElement("input[type=submit]");
+
+ let expectedAddresses = TEST_ADDRESSES.slice(0, 1);
+ await onStorageChanged("add");
+ // Check if timesUsed is set correctly
+ expectedAddresses[0].timesUsed = 1;
+ let matching = await checkAddresses(expectedAddresses);
+ ok(matching, "Address saved as expected");
+ delete expectedAddresses[0].timesUsed;
+});
+
+// Submit another new address.
+add_task(async function check_storage_after_another_address_submitted() {
+ await SpecialPowers.pushPrefEnv({"set": [["privacy.reduceTimerPrecision", false]]});
+
+ document.querySelector("form").reset();
+ for (let key in TEST_ADDRESSES[1]) {
+ await setInput("#" + key, TEST_ADDRESSES[1][key]);
+ }
+
+ clickOnElement("input[type=submit]");
+
+ // The 2nd test address should be on the top since it's the last used one.
+ let addressesInMenu = TEST_ADDRESSES.slice(1);
+ addressesInMenu.push(TEST_ADDRESSES[0]);
+
+ // let expectedAddresses = TEST_ADDRESSES.slice(0);
+ await onStorageChanged("add");
+ let matching = await checkAddresses(TEST_ADDRESSES);
+ ok(matching, "New address saved as expected");
+
+ await setInput("#organization", "");
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ checkMenuEntries(addressesInMenu.map(address =>
+ JSON.stringify({primary: address.organization, secondary: address["street-address"]})
+ ));
+});
+
+// Submit another new address that is mergeable.
+add_task(async function new_address_submitted_and_merged() {
+ // TODO: Bug Bug 1812294
+});
+
+// Submit an updated autofill address and merge.
+add_task(async function check_storage_after_form_submitted() {
+ // TODO: Bug Bug 1812294
+});
+
+// Submit a subset address manually.
+add_task(async function submit_subset_manually() {
+ // TODO: Bug Bug 1812294
+});
+
+</script>
+
+<div>
+
+ <form onsubmit="return false">
+ <p>This is a basic form for submitting test.</p>
+ <p><label>organization: <input id="organization" name="organization" autocomplete="organization" type="text"></label></p>
+ <p><label>streetAddress: <input id="street-address" name="street-address" autocomplete="street-address" type="text"></label></p>
+ <p><label>tel: <input id="tel" name="tel" autocomplete="tel" type="text"></label></p>
+ <p><label>country: <input id="country" name="country" autocomplete="country" type="text"></label></p>
+ <p><input type="submit"></p>
+ </form>
+
+</div>
+</body>
+</html>