Source code

Revision control

Copy as Markdown

Other Tools

/* 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 */
// Despite a use of `spawnChrome` and thus ChromeUtils, we can't use isInstance
// here as it gets used in plain mochitests which don't have the ChromeOnly
// APIs for it.
/* eslint-disable mozilla/use-isInstance */
"use strict";
let formFillChromeScript;
let defaultTextColor;
let defaultDisabledTextColor;
let expectingPopup = null;
const { FormAutofillUtils } = SpecialPowers.ChromeUtils.importESModule(
"resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
);
const { OSKeyStore } = SpecialPowers.ChromeUtils.importESModule(
"resource://gre/modules/OSKeyStore.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
// FormAutofillSection.sys.mjs 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 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?.toString().replaceAll("*", "•")) || "";
}
await checkFieldHighlighted(elem, !!fillableValue);
}
}
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 makeAddressComment({ primary, secondary, status }) {
return JSON.stringify({
primary,
secondary,
status,
ariaLabel: primary + " " + secondary + " " + status,
});
}
// Compare the labels on the autocomplete menu items to the expected labels.
function checkMenuEntries(expectedValues, extraRows = 1) {
let actualValues = getMenuEntries().labels;
let expectedLength = expectedValues.length + extraRows;
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);
}
}
// Compare the comment on the autocomplete menu items to the expected comment.
// The profile field is not compared.
function checkMenuEntriesComment(expectedValues, extraRows = 1) {
let actualValues = getMenuEntries().comments;
let expectedLength = expectedValues.length + extraRows;
is(actualValues.length, expectedLength, " Checking length of expected menu");
for (let i = 0; i < expectedValues.length; i++) {
const expectedValue = JSON.parse(expectedValues[i]);
const actualValue = JSON.parse(actualValues[i]);
for (const [key, value] of Object.entries(expectedValue)) {
is(
actualValue[key],
value,
` Checking menu entry #${i}, ${key} should be the same`
);
}
}
}
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;
}
/**
* This function should be used along with the `waitForOSKeyStoreLogin` API.
* See the comment in `waitForOSKeyStoreLogin` for more details.
*/
async function waitForOSKeyStoreLoginTestSetupComplete() {
if (
!(await SpecialPowers.spawnChrome([], () => {
// Need to re-import this because we're running in the parent.
// eslint-disable-next-line no-shadow
const { FormAutofillUtils } = ChromeUtils.importESModule(
"resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
);
return FormAutofillUtils.getOSAuthEnabled(
FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
);
}))
) {
return;
}
await SimpleTest.promiseWaitForCondition(async () => {
return await SpecialPowers.spawnChrome([], () => {
const { OSKeyStoreTestUtils } = ChromeUtils.importESModule(
);
return Services.prefs.getStringPref(
OSKeyStoreTestUtils.TEST_ONLY_REAUTH,
""
);
});
});
}
/**
* This API returns a promise that will be resolved when
* `waitForOSKeyStoreLogin` in `OSKeyStoreTestUtils.sys.mjs` completes.
* It is common to use it as follows:
* const promise = waitForOSKeyStoreLogin();
* triggerOSReauth(); // Code that triggers OS re-authentication
* await promise;
*
* However, the timing to switch to using test OS re-auth after calling this
* function is asynchronous, which means triggering OS re-auth right after
* this API may still activate the real OS re-auth popup. To avoid that, you
* need to call `await waitForOSKeyStoreLoginTestSetupComplete()` before
* triggering OS re-auth.
*/
async function waitForOSKeyStoreLogin(login = false) {
// Need to fetch this from the parent in order for it to be correct.
if (
!(await SpecialPowers.spawnChrome([], () => {
// Need to re-import this because we're running in the parent.
// eslint-disable-next-line no-shadow
const { FormAutofillUtils } = ChromeUtils.importESModule(
"resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
);
return FormAutofillUtils.getOSAuthEnabled(
FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
);
}))
) {
return;
}
await invokeAsyncChromeTask("FormAutofillTest:OSKeyStoreLogin", { login });
}
function patchRecordCCNumber(record) {
const ccNumberFmt = "****" + record.cc["cc-number"].substr(-4);
return {
cc: Object.assign({}, record.cc, { ccNumberFmt }),
expected: record.expected,
};
}
// 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) {
const promise = expectPopup();
await focusAndWaitForFieldsIdentified(fieldSelector);
synthesizeKey("KEY_ArrowDown");
await promise;
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();