tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

formautofill_common.js (17679B)


      1 /* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/SimpleTest.js */
      2 /* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/EventUtils.js */
      3 /* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
      4 /* eslint-disable no-unused-vars */
      5 // Despite a use of `spawnChrome` and thus ChromeUtils, we can't use isInstance
      6 // here as it gets used in plain mochitests which don't have the ChromeOnly
      7 // APIs for it.
      8 /* eslint-disable mozilla/use-isInstance */
      9 
     10 "use strict";
     11 
     12 let formFillChromeScript;
     13 let defaultTextColor;
     14 let defaultDisabledTextColor;
     15 let expectingPopup = null;
     16 
     17 const { FormAutofillUtils } = SpecialPowers.ChromeUtils.importESModule(
     18  "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
     19 );
     20 
     21 const { OSKeyStore } = SpecialPowers.ChromeUtils.importESModule(
     22  "resource://gre/modules/OSKeyStore.sys.mjs"
     23 );
     24 
     25 async function sleep(ms = 500, reason = "Intentionally wait for UI ready") {
     26  SimpleTest.requestFlakyTimeout(reason);
     27  await new Promise(resolve => setTimeout(resolve, ms));
     28 }
     29 
     30 async function focusAndWaitForFieldsIdentified(
     31  input,
     32  mustBeIdentified = false
     33 ) {
     34  info("expecting the target input being focused and indentified");
     35  if (typeof input === "string") {
     36    input = document.querySelector(input);
     37  }
     38  const rootElement = input.form || input.ownerDocument.documentElement;
     39  const previouslyFocused = input != document.activeElement;
     40 
     41  input.focus();
     42 
     43  if (mustBeIdentified) {
     44    rootElement.removeAttribute("test-formautofill-identified");
     45  }
     46  if (rootElement.hasAttribute("test-formautofill-identified")) {
     47    return;
     48  }
     49  if (!previouslyFocused) {
     50    await new Promise(resolve => {
     51      formFillChromeScript.addMessageListener(
     52        "FormAutofillTest:FieldsIdentified",
     53        function onIdentified() {
     54          formFillChromeScript.removeMessageListener(
     55            "FormAutofillTest:FieldsIdentified",
     56            onIdentified
     57          );
     58          resolve();
     59        }
     60      );
     61    });
     62  }
     63  // In order to ensure that "markAsAutofillField" is fully executed, a short period
     64  // of timeout is still required.
     65  await sleep(300, "Guarantee asynchronous identifyAutofillFields is invoked");
     66  rootElement.setAttribute("test-formautofill-identified", "true");
     67 }
     68 
     69 async function setInput(selector, value, userInput = false) {
     70  const input = document.querySelector("input" + selector);
     71  if (userInput) {
     72    SpecialPowers.wrap(input).setUserInput(value);
     73  } else {
     74    input.value = value;
     75  }
     76  await focusAndWaitForFieldsIdentified(input);
     77 
     78  return input;
     79 }
     80 
     81 function clickOnElement(selector) {
     82  let element = document.querySelector(selector);
     83 
     84  if (!element) {
     85    throw new Error("Can not find the element");
     86  }
     87 
     88  SimpleTest.executeSoon(() => element.click());
     89 }
     90 
     91 // The equivalent helper function to getAdaptedProfiles in
     92 // FormAutofillSection.sys.mjs that transforms the given profile to expected
     93 // filled profile.
     94 function _getAdaptedProfile(profile) {
     95  const adaptedProfile = Object.assign({}, profile);
     96 
     97  if (profile["street-address"]) {
     98    adaptedProfile["street-address"] = FormAutofillUtils.toOneLineAddress(
     99      profile["street-address"]
    100    );
    101  }
    102 
    103  return adaptedProfile;
    104 }
    105 
    106 async function checkFieldHighlighted(elem, expectedValue) {
    107  let isHighlightApplied;
    108  await SimpleTest.promiseWaitForCondition(function checkHighlight() {
    109    isHighlightApplied = elem.matches(":autofill");
    110    return isHighlightApplied === expectedValue;
    111  }, `Checking #${elem.id} highlight style`);
    112 
    113  is(isHighlightApplied, expectedValue, `Checking #${elem.id} highlight style`);
    114 }
    115 
    116 async function checkFormFieldsStyle(profile, isPreviewing = true) {
    117  const elems = document.querySelectorAll("input, select");
    118 
    119  for (const elem of elems) {
    120    let fillableValue;
    121    let previewValue;
    122    let isElementEligible =
    123      FormAutofillUtils.isCreditCardOrAddressFieldType(elem) &&
    124      FormAutofillUtils.isFieldAutofillable(elem);
    125    if (!isElementEligible) {
    126      fillableValue = "";
    127      previewValue = "";
    128    } else {
    129      fillableValue = profile && profile[elem.id];
    130      previewValue =
    131        (isPreviewing && fillableValue?.toString().replaceAll("*", "•")) || "";
    132    }
    133    await checkFieldHighlighted(elem, !!fillableValue);
    134  }
    135 }
    136 
    137 function checkFieldValue(elem, expectedValue) {
    138  if (typeof elem === "string") {
    139    elem = document.querySelector(elem);
    140  }
    141  is(elem.value, String(expectedValue), "Checking " + elem.id + " field");
    142 }
    143 
    144 async function triggerAutofillAndCheckProfile(profile) {
    145  let adaptedProfile = _getAdaptedProfile(profile);
    146  const promises = [];
    147  for (const [fieldName, value] of Object.entries(adaptedProfile)) {
    148    info(`triggerAutofillAndCheckProfile: ${fieldName}`);
    149    const element = document.getElementById(fieldName);
    150    const expectingEvent =
    151      document.activeElement == element ? "input" : "change";
    152    const checkFieldAutofilled = Promise.all([
    153      new Promise(resolve => {
    154        let beforeInputFired = false;
    155        let hadEditor = SpecialPowers.wrap(element).hasEditor;
    156        element.addEventListener(
    157          "beforeinput",
    158          event => {
    159            beforeInputFired = true;
    160            is(
    161              event.inputType,
    162              "insertReplacementText",
    163              'inputType value should be "insertReplacementText"'
    164            );
    165            is(
    166              event.data,
    167              String(value),
    168              `data value of "beforeinput" should be "${value}"`
    169            );
    170            is(
    171              event.dataTransfer,
    172              null,
    173              'dataTransfer of "beforeinput" should be null'
    174            );
    175            is(
    176              event.getTargetRanges().length,
    177              0,
    178              'getTargetRanges() of "beforeinput" should return empty array'
    179            );
    180            is(
    181              event.cancelable,
    182              SpecialPowers.getBoolPref(
    183                "dom.input_event.allow_to_cancel_set_user_input"
    184              ),
    185              `"beforeinput" event should be cancelable on ${element.tagName} unless it's suppressed by the pref`
    186            );
    187            is(
    188              event.bubbles,
    189              true,
    190              `"beforeinput" event should always bubble on ${element.tagName}`
    191            );
    192            resolve();
    193          },
    194          { once: true }
    195        );
    196        element.addEventListener(
    197          "input",
    198          event => {
    199            if (
    200              (element.tagName == "INPUT" && element.type == "text") ||
    201              element.tagName == "TEXTAREA"
    202            ) {
    203              if (hadEditor) {
    204                ok(
    205                  beforeInputFired,
    206                  `"beforeinput" event should've been fired before "input" event on ${element.tagName}`
    207                );
    208              } else {
    209                ok(
    210                  beforeInputFired,
    211                  `"beforeinput" event should've been fired before "input" event on ${element.tagName}`
    212                );
    213              }
    214              ok(
    215                event instanceof InputEvent,
    216                `"input" event should be dispatched with InputEvent interface on ${element.tagName}`
    217              );
    218              is(
    219                event.inputType,
    220                "insertReplacementText",
    221                'inputType value should be "insertReplacementText"'
    222              );
    223              is(event.data, String(value), `data value should be "${value}"`);
    224              is(event.dataTransfer, null, "dataTransfer should be null");
    225              is(
    226                event.getTargetRanges().length,
    227                0,
    228                "getTargetRanges() should return empty array"
    229              );
    230            } else {
    231              ok(
    232                !beforeInputFired,
    233                `"beforeinput" event shouldn't be fired on ${element.tagName}`
    234              );
    235              ok(
    236                event instanceof Event && !(event instanceof UIEvent),
    237                `"input" event should be dispatched with Event interface on ${element.tagName}`
    238              );
    239            }
    240            is(
    241              event.cancelable,
    242              false,
    243              `"input" event should be never cancelable on ${element.tagName}`
    244            );
    245            is(
    246              event.bubbles,
    247              true,
    248              `"input" event should always bubble on ${element.tagName}`
    249            );
    250            resolve();
    251          },
    252          { once: true }
    253        );
    254      }),
    255      new Promise(resolve =>
    256        element.addEventListener(expectingEvent, resolve, { once: true })
    257      ),
    258    ]).then(() => checkFieldValue(element, value));
    259 
    260    promises.push(checkFieldAutofilled);
    261  }
    262  // Press Enter key and trigger form autofill.
    263  synthesizeKey("KEY_Enter");
    264 
    265  return Promise.all(promises);
    266 }
    267 
    268 async function onStorageChanged(type) {
    269  info(`expecting the storage changed: ${type}`);
    270  return new Promise(resolve => {
    271    formFillChromeScript.addMessageListener(
    272      "formautofill-storage-changed",
    273      function onChanged(data) {
    274        formFillChromeScript.removeMessageListener(
    275          "formautofill-storage-changed",
    276          onChanged
    277        );
    278        is(data.data, type, `Receive ${type} storage changed event`);
    279        resolve();
    280      }
    281    );
    282  });
    283 }
    284 
    285 function makeAddressComment({ primary, secondary, status }) {
    286  return JSON.stringify({
    287    primary,
    288    secondary,
    289    status,
    290    ariaLabel: primary + " " + secondary + " " + status,
    291  });
    292 }
    293 
    294 // Compare the labels on the autocomplete menu items to the expected labels.
    295 function checkMenuEntries(expectedValues, extraRows = 1) {
    296  let actualValues = getMenuEntries().labels;
    297  let expectedLength = expectedValues.length + extraRows;
    298 
    299  is(actualValues.length, expectedLength, " Checking length of expected menu");
    300  for (let i = 0; i < expectedValues.length; i++) {
    301    is(actualValues[i], expectedValues[i], " Checking menu entry #" + i);
    302  }
    303 }
    304 
    305 // Compare the comment on the autocomplete menu items to the expected comment.
    306 // The profile field is not compared.
    307 function checkMenuEntriesComment(expectedValues, extraRows = 1) {
    308  let actualValues = getMenuEntries().comments;
    309  let expectedLength = expectedValues.length + extraRows;
    310 
    311  is(actualValues.length, expectedLength, " Checking length of expected menu");
    312  for (let i = 0; i < expectedValues.length; i++) {
    313    const expectedValue = JSON.parse(expectedValues[i]);
    314    const actualValue = JSON.parse(actualValues[i]);
    315    for (const [key, value] of Object.entries(expectedValue)) {
    316      is(
    317        actualValue[key],
    318        value,
    319        ` Checking menu entry #${i}, ${key} should be the same`
    320      );
    321    }
    322  }
    323 }
    324 
    325 function invokeAsyncChromeTask(message, payload = {}) {
    326  info(`expecting the chrome task finished: ${message}`);
    327  return formFillChromeScript.sendQuery(message, payload);
    328 }
    329 
    330 async function addAddress(address) {
    331  await invokeAsyncChromeTask("FormAutofillTest:AddAddress", { address });
    332  await sleep();
    333 }
    334 
    335 async function removeAddress(guid) {
    336  return invokeAsyncChromeTask("FormAutofillTest:RemoveAddress", { guid });
    337 }
    338 
    339 async function updateAddress(guid, address) {
    340  return invokeAsyncChromeTask("FormAutofillTest:UpdateAddress", {
    341    address,
    342    guid,
    343  });
    344 }
    345 
    346 async function checkAddresses(expectedAddresses) {
    347  return invokeAsyncChromeTask("FormAutofillTest:CheckAddresses", {
    348    expectedAddresses,
    349  });
    350 }
    351 
    352 async function cleanUpAddresses() {
    353  return invokeAsyncChromeTask("FormAutofillTest:CleanUpAddresses");
    354 }
    355 
    356 async function addCreditCard(creditcard) {
    357  await invokeAsyncChromeTask("FormAutofillTest:AddCreditCard", { creditcard });
    358  await sleep();
    359 }
    360 
    361 async function removeCreditCard(guid) {
    362  return invokeAsyncChromeTask("FormAutofillTest:RemoveCreditCard", { guid });
    363 }
    364 
    365 async function checkCreditCards(expectedCreditCards) {
    366  return invokeAsyncChromeTask("FormAutofillTest:CheckCreditCards", {
    367    expectedCreditCards,
    368  });
    369 }
    370 
    371 async function cleanUpCreditCards() {
    372  return invokeAsyncChromeTask("FormAutofillTest:CleanUpCreditCards");
    373 }
    374 
    375 async function cleanUpStorage() {
    376  await cleanUpAddresses();
    377  await cleanUpCreditCards();
    378 }
    379 
    380 async function canTestOSKeyStoreLogin() {
    381  let { canTest } = await invokeAsyncChromeTask(
    382    "FormAutofillTest:CanTestOSKeyStoreLogin"
    383  );
    384  return canTest;
    385 }
    386 
    387 /**
    388 * This function should be used along with the `waitForOSKeyStoreLogin` API.
    389 * See the comment in `waitForOSKeyStoreLogin` for more details.
    390 */
    391 async function waitForOSKeyStoreLoginTestSetupComplete() {
    392  if (
    393    !(await SpecialPowers.spawnChrome([], () => {
    394      // Need to re-import this because we're running in the parent.
    395      // eslint-disable-next-line no-shadow
    396      const { FormAutofillUtils } = ChromeUtils.importESModule(
    397        "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
    398      );
    399 
    400      return FormAutofillUtils.getOSAuthEnabled();
    401    }))
    402  ) {
    403    return;
    404  }
    405 
    406  await SimpleTest.promiseWaitForCondition(async () => {
    407    return await SpecialPowers.spawnChrome([], () => {
    408      const { OSKeyStoreTestUtils } = ChromeUtils.importESModule(
    409        "resource://testing-common/OSKeyStoreTestUtils.sys.mjs"
    410      );
    411 
    412      return Services.prefs.getStringPref(
    413        OSKeyStoreTestUtils.TEST_ONLY_REAUTH,
    414        ""
    415      );
    416    });
    417  });
    418 }
    419 
    420 /**
    421 * This API returns a promise that will be resolved when
    422 * `waitForOSKeyStoreLogin` in `OSKeyStoreTestUtils.sys.mjs` completes.
    423 * It is common to use it as follows:
    424 *   const promise = waitForOSKeyStoreLogin();
    425 *   triggerOSReauth();  // Code that triggers OS re-authentication
    426 *   await promise;
    427 *
    428 * However, the timing to switch to using test OS re-auth after calling this
    429 * function is asynchronous, which means triggering OS re-auth right after
    430 * this API may still activate the real OS re-auth popup. To avoid that, you
    431 * need to call `await waitForOSKeyStoreLoginTestSetupComplete()` before
    432 * triggering OS re-auth.
    433 */
    434 async function waitForOSKeyStoreLogin(login = false) {
    435  // Need to fetch this from the parent in order for it to be correct.
    436  if (
    437    !(await SpecialPowers.spawnChrome([], () => {
    438      // Need to re-import this because we're running in the parent.
    439      // eslint-disable-next-line no-shadow
    440      const { FormAutofillUtils } = ChromeUtils.importESModule(
    441        "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
    442      );
    443 
    444      return FormAutofillUtils.getOSAuthEnabled();
    445    }))
    446  ) {
    447    return;
    448  }
    449 
    450  await invokeAsyncChromeTask("FormAutofillTest:OSKeyStoreLogin", { login });
    451 }
    452 
    453 function patchRecordCCNumber(record) {
    454  const ccNumberFmt = "****" + record.cc["cc-number"].substr(-4);
    455 
    456  return {
    457    cc: Object.assign({}, record.cc, { ccNumberFmt }),
    458    expected: record.expected,
    459  };
    460 }
    461 
    462 // Utils for registerPopupShownListener(in satchel_common.js) that handles dropdown popup
    463 // Please call "initPopupListener()" in your test and "await expectPopup()"
    464 // if you want to wait for dropdown menu displayed.
    465 function expectPopup() {
    466  info("expecting a popup");
    467  return new Promise(resolve => {
    468    expectingPopup = resolve;
    469  });
    470 }
    471 
    472 function notExpectPopup(ms = 500) {
    473  info("not expecting a popup");
    474  return new Promise((resolve, reject) => {
    475    expectingPopup = reject.bind(this, "Unexpected Popup");
    476    // TODO: We don't have an event to notify no popup showing, so wait for 500
    477    // ms (in default) to predict any unexpected popup showing.
    478    setTimeout(resolve, ms);
    479  });
    480 }
    481 
    482 function popupShownListener() {
    483  info("popup shown for test ");
    484  if (expectingPopup) {
    485    expectingPopup();
    486    expectingPopup = null;
    487  }
    488 }
    489 
    490 function initPopupListener() {
    491  registerPopupShownListener(popupShownListener);
    492 }
    493 
    494 async function triggerPopupAndHoverItem(fieldSelector, selectIndex) {
    495  const promise = expectPopup();
    496  await focusAndWaitForFieldsIdentified(fieldSelector);
    497  synthesizeKey("KEY_ArrowDown");
    498  await promise;
    499  for (let i = 0; i <= selectIndex; i++) {
    500    synthesizeKey("KEY_ArrowDown");
    501  }
    502  await notifySelectedIndex(selectIndex);
    503 }
    504 
    505 function formAutoFillCommonSetup() {
    506  // Remove the /creditCard path segement when referenced from the 'creditCard' subdirectory.
    507  let chromeURL = SimpleTest.getTestFileURL(
    508    "formautofill_parent_utils.js"
    509  ).replace(/\/creditCard/, "");
    510  formFillChromeScript = SpecialPowers.loadChromeScript(chromeURL);
    511  formFillChromeScript.addMessageListener("onpopupshown", ({ results }) => {
    512    gLastAutoCompleteResults = results;
    513    if (gPopupShownListener) {
    514      gPopupShownListener({ results });
    515    }
    516  });
    517 
    518  add_setup(async () => {
    519    info(`expecting the storage setup`);
    520    await formFillChromeScript.sendQuery("setup");
    521  });
    522 
    523  SimpleTest.registerCleanupFunction(async () => {
    524    info(`expecting the storage cleanup`);
    525    await formFillChromeScript.sendQuery("cleanup");
    526 
    527    formFillChromeScript.destroy();
    528    expectingPopup = null;
    529  });
    530 
    531  document.addEventListener(
    532    "DOMContentLoaded",
    533    function () {
    534      defaultTextColor = window
    535        .getComputedStyle(document.querySelector("input"))
    536        .getPropertyValue("color");
    537 
    538      // This is needed for test_formautofill_preview_highlight.html to work properly
    539      let disabledInput = document.querySelector(`input[disabled]`);
    540      if (disabledInput) {
    541        defaultDisabledTextColor = window
    542          .getComputedStyle(disabledInput)
    543          .getPropertyValue("color");
    544      }
    545    },
    546    { once: true }
    547  );
    548 }
    549 
    550 /*
    551 * Extremely over-simplified detection of card type from card number just for
    552 * our tests. This is needed to test the aria-label of credit card menu entries.
    553 */
    554 function getCCTypeName(creditCard) {
    555  return creditCard["cc-number"][0] == "4" ? "Visa" : "MasterCard";
    556 }
    557 
    558 formAutoFillCommonSetup();