summaryrefslogtreecommitdiffstats
path: root/browser/extensions
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/extensions/formautofill/content/addressFormLayout.mjs187
-rw-r--r--browser/extensions/formautofill/content/autofillEditForms.js640
-rw-r--r--browser/extensions/formautofill/content/autofillEditForms.mjs288
-rw-r--r--browser/extensions/formautofill/content/editAddress.xhtml94
-rw-r--r--browser/extensions/formautofill/content/editCreditCard.xhtml52
-rw-r--r--browser/extensions/formautofill/content/editDialog.js233
-rw-r--r--browser/extensions/formautofill/content/editDialog.mjs253
-rw-r--r--browser/extensions/formautofill/content/manageAddresses.xhtml11
-rw-r--r--browser/extensions/formautofill/content/manageCreditCards.xhtml11
-rw-r--r--browser/extensions/formautofill/content/manageDialog.js454
-rw-r--r--browser/extensions/formautofill/content/manageDialog.mjs474
-rw-r--r--browser/extensions/formautofill/skin/shared/editAddress.css15
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser.toml2
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_ui_lines.js32
-rw-r--r--browser/extensions/formautofill/test/browser/browser_editAddressDialog.js153
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser.toml16
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js33
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js11
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js32
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js22
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js6
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_osAuth.js200
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js19
-rw-r--r--browser/extensions/formautofill/test/browser/head.js34
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/mochitest.toml1
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html7
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html12
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html7
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html8
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html8
-rw-r--r--browser/extensions/formautofill/test/mochitest/formautofill_common.js24
-rw-r--r--browser/extensions/pictureinpicture/data/picture_in_picture_overrides.js154
-rw-r--r--browser/extensions/pictureinpicture/moz.build3
-rw-r--r--browser/extensions/pictureinpicture/video-wrappers/canalplus.js56
-rw-r--r--browser/extensions/pictureinpicture/video-wrappers/jwplayerWrapper.js39
-rw-r--r--browser/extensions/pictureinpicture/video-wrappers/yahoo.js38
36 files changed, 1977 insertions, 1652 deletions
diff --git a/browser/extensions/formautofill/content/addressFormLayout.mjs b/browser/extensions/formautofill/content/addressFormLayout.mjs
new file mode 100644
index 0000000000..5e48e6afaa
--- /dev/null
+++ b/browser/extensions/formautofill/content/addressFormLayout.mjs
@@ -0,0 +1,187 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+});
+
+// Defines template descriptors for generating elements in convertLayoutToUI.
+const fieldTemplates = {
+ commonAttributes(item) {
+ return {
+ id: item.fieldId,
+ name: item.fieldId,
+ required: item.required,
+ value: item.value ?? "",
+ };
+ },
+ input(item) {
+ return {
+ tag: "input",
+ type: item.type ?? "text",
+ ...this.commonAttributes(item),
+ };
+ },
+ textarea(item) {
+ return {
+ tag: "textarea",
+ ...this.commonAttributes(item),
+ };
+ },
+ select(item) {
+ return {
+ tag: "select",
+ children: item.options.map(({ value, text }) => ({
+ tag: "option",
+ selected: value === item.value,
+ value,
+ text,
+ })),
+ ...this.commonAttributes(item),
+ };
+ },
+};
+
+/**
+ * Creates an HTML element with specified attributes and children.
+ *
+ * @param {string} tag - Tag name for the element to create.
+ * @param {object} options - Options object containing attributes and children.
+ * @param {object} options.attributes - Element's Attributes/Props (id, class, etc.)
+ * @param {Array} options.children - Element's children (array of objects with tag and options).
+ * @returns {HTMLElement} The newly created element.
+ */
+const createElement = (tag, { children = [], ...attributes }) => {
+ const element = document.createElement(tag);
+
+ for (let [attributeName, attributeValue] of Object.entries(attributes)) {
+ if (attributeName in element) {
+ element[attributeName] = attributeValue;
+ } else {
+ element.setAttribute(attributeName, attributeValue);
+ }
+ }
+
+ for (let { tag: childTag, ...childRest } of children) {
+ element.appendChild(createElement(childTag, childRest));
+ }
+
+ return element;
+};
+
+/**
+ * Generator that creates UI elements from `fields` object, using localization from `l10nStrings`.
+ *
+ * @param {Array} fields - Array of objects as returned from `FormAutofillUtils.getFormLayout`.
+ * @param {object} l10nStrings - Key-value pairs for field label localization.
+ * @yields {HTMLElement} - A localized label element with constructed from a field.
+ */
+function* convertLayoutToUI(fields, l10nStrings) {
+ for (const item of fields) {
+ // eslint-disable-next-line no-nested-ternary
+ const fieldTag = item.options
+ ? "select"
+ : item.multiline
+ ? "textarea"
+ : "input";
+
+ const fieldUI = {
+ label: {
+ id: `${item.fieldId}-container`,
+ class: `container ${item.newLine ? "new-line" : ""}`,
+ },
+ field: fieldTemplates[fieldTag](item),
+ span: {
+ class: "label-text",
+ textContent: l10nStrings[item.l10nId] ?? "",
+ },
+ };
+
+ const label = createElement("label", fieldUI.label);
+ const { tag, ...rest } = fieldUI.field;
+ const field = createElement(tag, rest);
+ label.appendChild(field);
+ const span = createElement("span", fieldUI.span);
+ label.appendChild(span);
+
+ yield label;
+ }
+}
+
+/**
+ * Retrieves the current form data from the current form element on the page.
+ *
+ * @returns {object} An object containing key-value pairs of form data.
+ */
+export const getCurrentFormData = () => {
+ const formElement = document.querySelector("form");
+ const formData = new FormData(formElement);
+ return Object.fromEntries(formData.entries());
+};
+
+/**
+ * Checks if the form can be submitted based on the number of non-empty values.
+ * TODO(Bug 1891734): Add address validation. Right now we don't do any validation. (2 fields mimics the old behaviour ).
+ *
+ * @returns {boolean} True if the form can be submitted
+ */
+export const canSubmitForm = () => {
+ const formData = getCurrentFormData();
+ const validValues = Object.values(formData).filter(Boolean);
+ return validValues.length >= 2;
+};
+
+/**
+ * Generates a form layout based on record data and localization strings.
+ *
+ * @param {HTMLFormElement} formElement - Target form element.
+ * @param {object} record - Address record, includes at least country code defaulted to FormAutofill.DEFAULT_REGION.
+ * @param {object} l10nStrings - Localization strings map.
+ */
+export const createFormLayoutFromRecord = (
+ formElement,
+ record = { country: lazy.FormAutofill.DEFAULT_REGION },
+ l10nStrings = {}
+) => {
+ // Always clear select values because they are not persisted between countries.
+ // For example from US with state NY, we don't want the address-level1 to be NY
+ // when changing to another country that doesn't have state options
+ const selects = formElement.querySelectorAll("select:not(#country)");
+ for (const select of selects) {
+ select.value = "";
+ }
+
+ // Get old data to persist before clearing form
+ const formData = getCurrentFormData();
+ record = {
+ ...record,
+ ...formData,
+ };
+
+ formElement.innerHTML = "";
+ const fields = lazy.FormAutofillUtils.getFormLayout(record);
+
+ const layoutGenerator = convertLayoutToUI(fields, l10nStrings);
+
+ for (const fieldElement of layoutGenerator) {
+ formElement.appendChild(fieldElement);
+ }
+
+ document.querySelector("#country").addEventListener(
+ "change",
+ ev =>
+ // Allow some time for the user to type
+ // before we set the new country and re-render
+ setTimeout(() => {
+ record.country = ev.target.value;
+ createFormLayoutFromRecord(formElement, record, l10nStrings);
+ }, 300),
+ { once: true }
+ );
+
+ // Used to notify tests that the form has been updated and is ready
+ window.dispatchEvent(new CustomEvent("FormReadyForTests"));
+};
diff --git a/browser/extensions/formautofill/content/autofillEditForms.js b/browser/extensions/formautofill/content/autofillEditForms.js
deleted file mode 100644
index 290b436a64..0000000000
--- a/browser/extensions/formautofill/content/autofillEditForms.js
+++ /dev/null
@@ -1,640 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-/* exported EditAddress, EditCreditCard */
-/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
-
-"use strict";
-
-const { FormAutofill } = ChromeUtils.importESModule(
- "resource://autofill/FormAutofill.sys.mjs"
-);
-const { FormAutofillUtils } = ChromeUtils.importESModule(
- "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
-);
-
-class EditAutofillForm {
- constructor(elements) {
- this._elements = elements;
- }
-
- /**
- * Fill the form with a record object.
- *
- * @param {object} [record = {}]
- */
- loadRecord(record = {}) {
- for (let field of this._elements.form.elements) {
- let value = record[field.id];
- value = typeof value == "undefined" ? "" : value;
-
- if (record.guid) {
- field.value = value;
- } else if (field.localName == "select") {
- this.setDefaultSelectedOptionByValue(field, value);
- } else {
- // Use .defaultValue instead of .value to avoid setting the `dirty` flag
- // which triggers form validation UI.
- field.defaultValue = value;
- }
- }
- if (!record.guid) {
- // Reset the dirty value flag and validity state.
- this._elements.form.reset();
- } else {
- for (let field of this._elements.form.elements) {
- this.updatePopulatedState(field);
- this.updateCustomValidity(field);
- }
- }
- }
-
- setDefaultSelectedOptionByValue(select, value) {
- for (let option of select.options) {
- option.defaultSelected = option.value == value;
- }
- }
-
- /**
- * Get a record from the form suitable for a save/update in storage.
- *
- * @returns {object}
- */
- buildFormObject() {
- let initialObject = {};
- if (this.hasMailingAddressFields) {
- // Start with an empty string for each mailing-address field so that any
- // fields hidden for the current country are blanked in the return value.
- initialObject = {
- "street-address": "",
- "address-level3": "",
- "address-level2": "",
- "address-level1": "",
- "postal-code": "",
- };
- }
-
- return Array.from(this._elements.form.elements).reduce((obj, input) => {
- if (!input.disabled) {
- obj[input.id] = input.value;
- }
- return obj;
- }, initialObject);
- }
-
- /**
- * Handle events
- *
- * @param {DOMEvent} event
- */
- handleEvent(event) {
- switch (event.type) {
- case "change": {
- this.handleChange(event);
- break;
- }
- case "input": {
- this.handleInput(event);
- break;
- }
- }
- }
-
- /**
- * Handle change events
- *
- * @param {DOMEvent} event
- */
- handleChange(event) {
- this.updatePopulatedState(event.target);
- }
-
- /**
- * Handle input events
- */
- handleInput(_e) {}
-
- /**
- * Attach event listener
- */
- attachEventListeners() {
- this._elements.form.addEventListener("input", this);
- }
-
- /**
- * Set the field-populated attribute if the field has a value.
- *
- * @param {DOMElement} field The field that will be checked for a value.
- */
- updatePopulatedState(field) {
- let span = field.parentNode.querySelector(".label-text");
- if (!span) {
- return;
- }
- span.toggleAttribute("field-populated", !!field.value.trim());
- }
-
- /**
- * Run custom validity routines specific to the field and type of form.
- *
- * @param {DOMElement} _field The field that will be validated.
- */
- updateCustomValidity(_field) {}
-}
-
-class EditAddress extends EditAutofillForm {
- /**
- * @param {HTMLElement[]} elements
- * @param {object} record
- * @param {object} config
- * @param {boolean} [config.noValidate=undefined] Whether to validate the form
- */
- constructor(elements, record, config) {
- super(elements);
-
- Object.assign(this, config);
- let { form } = this._elements;
- Object.assign(this._elements, {
- addressLevel3Label: form.querySelector(
- "#address-level3-container > .label-text"
- ),
- addressLevel2Label: form.querySelector(
- "#address-level2-container > .label-text"
- ),
- addressLevel1Label: form.querySelector(
- "#address-level1-container > .label-text"
- ),
- postalCodeLabel: form.querySelector(
- "#postal-code-container > .label-text"
- ),
- country: form.querySelector("#country"),
- });
-
- this.populateCountries();
- // Need to populate the countries before trying to set the initial country.
- // Also need to use this._record so it has the default country selected.
- this.loadRecord(record);
- this.attachEventListeners();
-
- form.noValidate = !!config.noValidate;
- }
-
- loadRecord(record) {
- this._record = record;
- if (!record) {
- record = {
- country: FormAutofill.DEFAULT_REGION,
- };
- }
-
- let { addressLevel1Options } = FormAutofillUtils.getFormFormat(
- record.country
- );
- this.populateAddressLevel1(addressLevel1Options, record.country);
-
- super.loadRecord(record);
- this.loadAddressLevel1(record["address-level1"], record.country);
- this.formatForm(record.country);
- }
-
- get hasMailingAddressFields() {
- let { addressFields } = this._elements.form.dataset;
- return (
- !addressFields ||
- addressFields.trim().split(/\s+/).includes("mailing-address")
- );
- }
-
- /**
- * `mailing-address` is a special attribute token to indicate mailing fields + country.
- *
- * @param {object[]} mailingFieldsOrder - `fieldsOrder` from `getFormFormat`
- * @param {string} addressFields - white-space-separated string of requested address fields to show
- * @returns {object[]} in the same structure as `mailingFieldsOrder` but including non-mail fields
- */
- static computeVisibleFields(mailingFieldsOrder, addressFields) {
- if (addressFields) {
- let requestedFieldClasses = addressFields.trim().split(/\s+/);
- let fieldClasses = [];
- if (requestedFieldClasses.includes("mailing-address")) {
- fieldClasses = fieldClasses.concat(mailingFieldsOrder);
- // `country` isn't part of the `mailingFieldsOrder` so add it when filling a mailing-address
- requestedFieldClasses.splice(
- requestedFieldClasses.indexOf("mailing-address"),
- 1,
- "country"
- );
- }
-
- for (let fieldClassName of requestedFieldClasses) {
- fieldClasses.push({
- fieldId: fieldClassName,
- newLine: fieldClassName == "name",
- });
- }
- return fieldClasses;
- }
-
- // This is the default which is shown in the management interface and includes all fields.
- return mailingFieldsOrder.concat([
- {
- fieldId: "country",
- },
- {
- fieldId: "tel",
- },
- {
- fieldId: "email",
- newLine: true,
- },
- ]);
- }
-
- /**
- * Format the form based on country. The address-level1 and postal-code labels
- * should be specific to the given country.
- *
- * @param {string} country
- */
- formatForm(country) {
- const {
- addressLevel3L10nId,
- addressLevel2L10nId,
- addressLevel1L10nId,
- addressLevel1Options,
- postalCodeL10nId,
- fieldsOrder: mailingFieldsOrder,
- postalCodePattern,
- countryRequiredFields,
- } = FormAutofillUtils.getFormFormat(country);
-
- document.l10n.setAttributes(
- this._elements.addressLevel3Label,
- addressLevel3L10nId
- );
- document.l10n.setAttributes(
- this._elements.addressLevel2Label,
- addressLevel2L10nId
- );
- document.l10n.setAttributes(
- this._elements.addressLevel1Label,
- addressLevel1L10nId
- );
- document.l10n.setAttributes(
- this._elements.postalCodeLabel,
- postalCodeL10nId
- );
- let addressFields = this._elements.form.dataset.addressFields;
- let extraRequiredFields = this._elements.form.dataset.extraRequiredFields;
- let fieldClasses = EditAddress.computeVisibleFields(
- mailingFieldsOrder,
- addressFields
- );
- let requiredFields = new Set(countryRequiredFields);
- if (extraRequiredFields) {
- for (let extraRequiredField of extraRequiredFields.trim().split(/\s+/)) {
- requiredFields.add(extraRequiredField);
- }
- }
- this.arrangeFields(fieldClasses, requiredFields);
- this.updatePostalCodeValidation(postalCodePattern);
- this.populateAddressLevel1(addressLevel1Options, country);
- }
-
- /**
- * Update address field visibility and order based on libaddressinput data.
- *
- * @param {object[]} fieldsOrder array of objects with `fieldId` and optional `newLine` properties
- * @param {Set} requiredFields Set of `fieldId` strings that mark which fields are required
- */
- arrangeFields(fieldsOrder, requiredFields) {
- /**
- * @see FormAutofillStorage.VALID_ADDRESS_FIELDS
- */
- let fields = [
- // `name` is a wrapper for the 3 name fields.
- "name",
- "organization",
- "street-address",
- "address-level3",
- "address-level2",
- "address-level1",
- "postal-code",
- "country",
- "tel",
- "email",
- ];
- let inputs = [];
- for (let i = 0; i < fieldsOrder.length; i++) {
- let { fieldId, newLine } = fieldsOrder[i];
-
- let container = this._elements.form.querySelector(
- `#${fieldId}-container`
- );
- let containerInputs = [
- ...container.querySelectorAll("input, textarea, select"),
- ];
- containerInputs.forEach(function (input) {
- input.disabled = false;
- // libaddressinput doesn't list 'country' or 'name' as required.
- input.required =
- fieldId == "country" ||
- fieldId == "name" ||
- requiredFields.has(fieldId);
- });
- inputs.push(...containerInputs);
- container.style.display = "flex";
- container.style.order = i;
- container.style.pageBreakAfter = newLine ? "always" : "auto";
- // Remove the field from the list of fields
- fields.splice(fields.indexOf(fieldId), 1);
- }
- for (let i = 0; i < inputs.length; i++) {
- // Assign tabIndex starting from 1
- inputs[i].tabIndex = i + 1;
- }
- // Hide the remaining fields
- for (let field of fields) {
- let container = this._elements.form.querySelector(`#${field}-container`);
- container.style.display = "none";
- for (let input of [
- ...container.querySelectorAll("input, textarea, select"),
- ]) {
- input.disabled = true;
- }
- }
- }
-
- updatePostalCodeValidation(postalCodePattern) {
- let postalCodeInput = this._elements.form.querySelector("#postal-code");
- if (postalCodePattern && postalCodeInput.style.display != "none") {
- postalCodeInput.setAttribute("pattern", postalCodePattern);
- } else {
- postalCodeInput.removeAttribute("pattern");
- }
- }
-
- /**
- * Set the address-level1 value on the form field (input or select, whichever is present).
- *
- * @param {string} addressLevel1Value Value of the address-level1 from the autofill record
- * @param {string} country The corresponding country
- */
- loadAddressLevel1(addressLevel1Value, country) {
- let field = this._elements.form.querySelector("#address-level1");
-
- if (field.localName == "input") {
- field.value = addressLevel1Value || "";
- return;
- }
-
- let matchedSelectOption = FormAutofillUtils.findAddressSelectOption(
- field,
- {
- country,
- "address-level1": addressLevel1Value,
- },
- "address-level1"
- );
- if (matchedSelectOption && !matchedSelectOption.selected) {
- field.value = matchedSelectOption.value;
- field.dispatchEvent(new Event("input", { bubbles: true }));
- field.dispatchEvent(new Event("change", { bubbles: true }));
- } else if (addressLevel1Value) {
- // If the option wasn't found, insert an option at the beginning of
- // the select that matches the stored value.
- field.insertBefore(
- new Option(addressLevel1Value, addressLevel1Value, true, true),
- field.firstChild
- );
- }
- }
-
- /**
- * Replace the text input for address-level1 with a select dropdown if
- * a fixed set of names exists. Otherwise show a text input.
- *
- * @param {Map?} options Map of options with regionCode -> name mappings
- * @param {string} country The corresponding country
- */
- populateAddressLevel1(options, country) {
- let field = this._elements.form.querySelector("#address-level1");
-
- if (field.dataset.country == country) {
- return;
- }
-
- if (!options) {
- if (field.localName == "input") {
- return;
- }
-
- let input = document.createElement("input");
- input.setAttribute("type", "text");
- input.id = "address-level1";
- input.required = field.required;
- input.disabled = field.disabled;
- input.tabIndex = field.tabIndex;
- field.replaceWith(input);
- return;
- }
-
- if (field.localName == "input") {
- let select = document.createElement("select");
- select.id = "address-level1";
- select.required = field.required;
- select.disabled = field.disabled;
- select.tabIndex = field.tabIndex;
- field.replaceWith(select);
- field = select;
- }
-
- field.textContent = "";
- field.dataset.country = country;
- let fragment = document.createDocumentFragment();
- fragment.appendChild(new Option(undefined, undefined, true, true));
- for (let [regionCode, regionName] of options) {
- let option = new Option(regionName, regionCode);
- fragment.appendChild(option);
- }
- field.appendChild(fragment);
- }
-
- populateCountries() {
- let fragment = document.createDocumentFragment();
- // Sort countries by their visible names.
- let countries = [...FormAutofill.countries.entries()].sort((e1, e2) =>
- e1[1].localeCompare(e2[1])
- );
- for (let [country] of countries) {
- const countryName = Services.intl.getRegionDisplayNames(undefined, [
- country.toLowerCase(),
- ]);
- const option = new Option(countryName, country);
- fragment.appendChild(option);
- }
- this._elements.country.appendChild(fragment);
- }
-
- handleChange(event) {
- if (event.target == this._elements.country) {
- this.formatForm(event.target.value);
- }
- super.handleChange(event);
- }
-
- attachEventListeners() {
- this._elements.form.addEventListener("change", this);
- super.attachEventListeners();
- }
-}
-
-class EditCreditCard extends EditAutofillForm {
- /**
- * @param {HTMLElement[]} elements
- * @param {object} record with a decrypted cc-number
- * @param {object} addresses in an object with guid keys for the billing address picker.
- */
- constructor(elements, record, addresses) {
- super(elements);
-
- this._addresses = addresses;
- Object.assign(this._elements, {
- ccNumber: this._elements.form.querySelector("#cc-number"),
- invalidCardNumberStringElement: this._elements.form.querySelector(
- "#invalidCardNumberString"
- ),
- month: this._elements.form.querySelector("#cc-exp-month"),
- year: this._elements.form.querySelector("#cc-exp-year"),
- billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
- billingAddressRow:
- this._elements.form.querySelector(".billingAddressRow"),
- });
-
- this.attachEventListeners();
- this.loadRecord(record, addresses);
- }
-
- loadRecord(record, addresses, preserveFieldValues) {
- // _record must be updated before generateYears and generateBillingAddressOptions are called.
- this._record = record;
- this._addresses = addresses;
- this.generateBillingAddressOptions(preserveFieldValues);
- if (!preserveFieldValues) {
- // Re-generating the months will reset the selected option.
- this.generateMonths();
- // Re-generating the years will reset the selected option.
- this.generateYears();
- super.loadRecord(record);
- }
- }
-
- generateMonths() {
- const count = 12;
-
- // Clear the list
- this._elements.month.textContent = "";
-
- // Empty month option
- this._elements.month.appendChild(new Option());
-
- // Populate month list. Format: "month number - month name"
- let dateFormat = new Intl.DateTimeFormat(navigator.language, {
- month: "long",
- }).format;
- for (let i = 0; i < count; i++) {
- let monthNumber = (i + 1).toString();
- let monthName = dateFormat(new Date(1970, i));
- let option = new Option();
- option.value = monthNumber;
- // XXX: Bug 1446164 - Localize this string.
- option.textContent = `${monthNumber.padStart(2, "0")} - ${monthName}`;
- this._elements.month.appendChild(option);
- }
- }
-
- generateYears() {
- const count = 11;
- const currentYear = new Date().getFullYear();
- const ccExpYear = this._record && this._record["cc-exp-year"];
-
- // Clear the list
- this._elements.year.textContent = "";
-
- // Provide an empty year option
- this._elements.year.appendChild(new Option());
-
- if (ccExpYear && ccExpYear < currentYear) {
- this._elements.year.appendChild(new Option(ccExpYear));
- }
-
- for (let i = 0; i < count; i++) {
- let year = currentYear + i;
- let option = new Option(year);
- this._elements.year.appendChild(option);
- }
-
- if (ccExpYear && ccExpYear > currentYear + count) {
- this._elements.year.appendChild(new Option(ccExpYear));
- }
- }
-
- generateBillingAddressOptions(preserveFieldValues) {
- let billingAddressGUID;
- if (preserveFieldValues && this._elements.billingAddress.value) {
- billingAddressGUID = this._elements.billingAddress.value;
- } else if (this._record) {
- billingAddressGUID = this._record.billingAddressGUID;
- }
-
- this._elements.billingAddress.textContent = "";
-
- this._elements.billingAddress.appendChild(new Option("", ""));
-
- let hasAddresses = false;
- for (let [guid, address] of Object.entries(this._addresses)) {
- hasAddresses = true;
- let selected = guid == billingAddressGUID;
- let option = new Option(
- FormAutofillUtils.getAddressLabel(address),
- guid,
- selected,
- selected
- );
- this._elements.billingAddress.appendChild(option);
- }
-
- this._elements.billingAddressRow.hidden = !hasAddresses;
- }
-
- attachEventListeners() {
- this._elements.form.addEventListener("change", this);
- super.attachEventListeners();
- }
-
- handleInput(event) {
- // Clear the error message if cc-number is valid
- if (
- event.target == this._elements.ccNumber &&
- FormAutofillUtils.isCCNumber(this._elements.ccNumber.value)
- ) {
- this._elements.ccNumber.setCustomValidity("");
- }
- super.handleInput(event);
- }
-
- updateCustomValidity(field) {
- super.updateCustomValidity(field);
-
- // Mark the cc-number field as invalid if the number is empty or invalid.
- if (
- field == this._elements.ccNumber &&
- !FormAutofillUtils.isCCNumber(field.value)
- ) {
- let invalidCardNumberString =
- this._elements.invalidCardNumberStringElement.textContent;
- field.setCustomValidity(invalidCardNumberString || " ");
- }
- }
-}
diff --git a/browser/extensions/formautofill/content/autofillEditForms.mjs b/browser/extensions/formautofill/content/autofillEditForms.mjs
new file mode 100644
index 0000000000..ca74850acd
--- /dev/null
+++ b/browser/extensions/formautofill/content/autofillEditForms.mjs
@@ -0,0 +1,288 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+});
+
+class EditAutofillForm {
+ constructor(elements) {
+ this._elements = elements;
+ }
+
+ /**
+ * Fill the form with a record object.
+ *
+ * @param {object} [record = {}]
+ */
+ loadRecord(record = {}) {
+ for (let field of this._elements.form.elements) {
+ let value = record[field.id];
+ value = typeof value == "undefined" ? "" : value;
+
+ if (record.guid) {
+ field.value = value;
+ } else if (field.localName == "select") {
+ this.setDefaultSelectedOptionByValue(field, value);
+ } else {
+ // Use .defaultValue instead of .value to avoid setting the `dirty` flag
+ // which triggers form validation UI.
+ field.defaultValue = value;
+ }
+ }
+ if (!record.guid) {
+ // Reset the dirty value flag and validity state.
+ this._elements.form.reset();
+ } else {
+ for (let field of this._elements.form.elements) {
+ this.updatePopulatedState(field);
+ this.updateCustomValidity(field);
+ }
+ }
+ }
+
+ setDefaultSelectedOptionByValue(select, value) {
+ for (let option of select.options) {
+ option.defaultSelected = option.value == value;
+ }
+ }
+
+ /**
+ * Get a record from the form suitable for a save/update in storage.
+ *
+ * @returns {object}
+ */
+ buildFormObject() {
+ let initialObject = {};
+ if (this.hasMailingAddressFields) {
+ // Start with an empty string for each mailing-address field so that any
+ // fields hidden for the current country are blanked in the return value.
+ initialObject = {
+ "street-address": "",
+ "address-level3": "",
+ "address-level2": "",
+ "address-level1": "",
+ "postal-code": "",
+ };
+ }
+
+ return Array.from(this._elements.form.elements).reduce((obj, input) => {
+ if (!input.disabled) {
+ obj[input.id] = input.value;
+ }
+ return obj;
+ }, initialObject);
+ }
+
+ /**
+ * Handle events
+ *
+ * @param {DOMEvent} event
+ */
+ handleEvent(event) {
+ switch (event.type) {
+ case "change": {
+ this.handleChange(event);
+ break;
+ }
+ case "input": {
+ this.handleInput(event);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle change events
+ *
+ * @param {DOMEvent} event
+ */
+ handleChange(event) {
+ this.updatePopulatedState(event.target);
+ }
+
+ /**
+ * Handle input events
+ */
+ handleInput(_e) {}
+
+ /**
+ * Attach event listener
+ */
+ attachEventListeners() {
+ this._elements.form.addEventListener("input", this);
+ }
+
+ /**
+ * Set the field-populated attribute if the field has a value.
+ *
+ * @param {DOMElement} field The field that will be checked for a value.
+ */
+ updatePopulatedState(field) {
+ let span = field.parentNode.querySelector(".label-text");
+ if (!span) {
+ return;
+ }
+ span.toggleAttribute("field-populated", !!field.value.trim());
+ }
+
+ /**
+ * Run custom validity routines specific to the field and type of form.
+ *
+ * @param {DOMElement} _field The field that will be validated.
+ */
+ updateCustomValidity(_field) {}
+}
+
+export class EditCreditCard extends EditAutofillForm {
+ /**
+ * @param {HTMLElement[]} elements
+ * @param {object} record with a decrypted cc-number
+ * @param {object} addresses in an object with guid keys for the billing address picker.
+ */
+ constructor(elements, record, addresses) {
+ super(elements);
+
+ this._addresses = addresses;
+ Object.assign(this._elements, {
+ ccNumber: this._elements.form.querySelector("#cc-number"),
+ invalidCardNumberStringElement: this._elements.form.querySelector(
+ "#invalidCardNumberString"
+ ),
+ month: this._elements.form.querySelector("#cc-exp-month"),
+ year: this._elements.form.querySelector("#cc-exp-year"),
+ billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
+ billingAddressRow:
+ this._elements.form.querySelector(".billingAddressRow"),
+ });
+
+ this.attachEventListeners();
+ this.loadRecord(record, addresses);
+ }
+
+ loadRecord(record, addresses, preserveFieldValues) {
+ // _record must be updated before generateYears and generateBillingAddressOptions are called.
+ this._record = record;
+ this._addresses = addresses;
+ this.generateBillingAddressOptions(preserveFieldValues);
+ if (!preserveFieldValues) {
+ // Re-generating the months will reset the selected option.
+ this.generateMonths();
+ // Re-generating the years will reset the selected option.
+ this.generateYears();
+ super.loadRecord(record);
+ }
+ }
+
+ generateMonths() {
+ const count = 12;
+
+ // Clear the list
+ this._elements.month.textContent = "";
+
+ // Empty month option
+ this._elements.month.appendChild(new Option());
+
+ // Populate month list. Format: "month number - month name"
+ let dateFormat = new Intl.DateTimeFormat(navigator.language, {
+ month: "long",
+ }).format;
+ for (let i = 0; i < count; i++) {
+ let monthNumber = (i + 1).toString();
+ let monthName = dateFormat(new Date(1970, i));
+ let option = new Option();
+ option.value = monthNumber;
+ // XXX: Bug 1446164 - Localize this string.
+ option.textContent = `${monthNumber.padStart(2, "0")} - ${monthName}`;
+ this._elements.month.appendChild(option);
+ }
+ }
+
+ generateYears() {
+ const count = 11;
+ const currentYear = new Date().getFullYear();
+ const ccExpYear = this._record && this._record["cc-exp-year"];
+
+ // Clear the list
+ this._elements.year.textContent = "";
+
+ // Provide an empty year option
+ this._elements.year.appendChild(new Option());
+
+ if (ccExpYear && ccExpYear < currentYear) {
+ this._elements.year.appendChild(new Option(ccExpYear));
+ }
+
+ for (let i = 0; i < count; i++) {
+ let year = currentYear + i;
+ let option = new Option(year);
+ this._elements.year.appendChild(option);
+ }
+
+ if (ccExpYear && ccExpYear > currentYear + count) {
+ this._elements.year.appendChild(new Option(ccExpYear));
+ }
+ }
+
+ generateBillingAddressOptions(preserveFieldValues) {
+ let billingAddressGUID;
+ if (preserveFieldValues && this._elements.billingAddress.value) {
+ billingAddressGUID = this._elements.billingAddress.value;
+ } else if (this._record) {
+ billingAddressGUID = this._record.billingAddressGUID;
+ }
+
+ this._elements.billingAddress.textContent = "";
+
+ this._elements.billingAddress.appendChild(new Option("", ""));
+
+ let hasAddresses = false;
+ for (let [guid, address] of Object.entries(this._addresses)) {
+ hasAddresses = true;
+ let selected = guid == billingAddressGUID;
+ let option = new Option(
+ lazy.FormAutofillUtils.getAddressLabel(address),
+ guid,
+ selected,
+ selected
+ );
+ this._elements.billingAddress.appendChild(option);
+ }
+
+ this._elements.billingAddressRow.hidden = !hasAddresses;
+ }
+
+ attachEventListeners() {
+ this._elements.form.addEventListener("change", this);
+ super.attachEventListeners();
+ }
+
+ handleInput(event) {
+ // Clear the error message if cc-number is valid
+ if (
+ event.target == this._elements.ccNumber &&
+ lazy.FormAutofillUtils.isCCNumber(this._elements.ccNumber.value)
+ ) {
+ this._elements.ccNumber.setCustomValidity("");
+ }
+ super.handleInput(event);
+ }
+
+ updateCustomValidity(field) {
+ super.updateCustomValidity(field);
+
+ // Mark the cc-number field as invalid if the number is empty or invalid.
+ if (
+ field == this._elements.ccNumber &&
+ !lazy.FormAutofillUtils.isCCNumber(field.value)
+ ) {
+ let invalidCardNumberString =
+ this._elements.invalidCardNumberStringElement.textContent;
+ field.setCustomValidity(invalidCardNumberString || " ");
+ }
+ }
+}
diff --git a/browser/extensions/formautofill/content/editAddress.xhtml b/browser/extensions/formautofill/content/editAddress.xhtml
index 47ae4a2a3b..a23fa5ab8c 100644
--- a/browser/extensions/formautofill/content/editAddress.xhtml
+++ b/browser/extensions/formautofill/content/editAddress.xhtml
@@ -19,65 +19,13 @@
rel="stylesheet"
href="chrome://formautofill/content/skin/editDialog.css"
/>
- <script src="chrome://formautofill/content/editDialog.js"></script>
- <script src="chrome://formautofill/content/autofillEditForms.js"></script>
<script
type="module"
src="chrome://global/content/elements/moz-button-group.mjs"
></script>
</head>
<body>
- <form id="form" class="editAddressForm" autocomplete="off">
- <!--
- The <span class="label-text" …/> needs to be after the form field in the same element in
- order to get proper label styling with :focus and :user-invalid
- -->
- <label id="name-container" class="container">
- <input id="name" type="text" required="required" />
- <span data-l10n-id="autofill-address-name" class="label-text" />
- </label>
- <label id="organization-container" class="container">
- <input id="organization" type="text" />
- <span data-l10n-id="autofill-address-organization" class="label-text" />
- </label>
- <label id="street-address-container" class="container">
- <textarea id="street-address" rows="3" />
- <span data-l10n-id="autofill-address-street" class="label-text" />
- </label>
- <label id="address-level3-container" class="container">
- <input id="address-level3" type="text" />
- <span class="label-text" />
- </label>
- <label id="address-level2-container" class="container">
- <input id="address-level2" type="text" />
- <span class="label-text" />
- </label>
- <label id="address-level1-container" class="container">
- <!-- The address-level1 input will get replaced by a select dropdown
- by autofillEditForms.js when the selected country has provided
- specific options. -->
- <input id="address-level1" type="text" />
- <span class="label-text" />
- </label>
- <label id="postal-code-container" class="container">
- <input id="postal-code" type="text" />
- <span class="label-text" />
- </label>
- <label id="country-container" class="container">
- <select id="country" required="required">
- <option />
- </select>
- <span data-l10n-id="autofill-address-country" class="label-text" />
- </label>
- <label id="tel-container" class="container">
- <input id="tel" type="tel" dir="auto" />
- <span data-l10n-id="autofill-address-tel" class="label-text" />
- </label>
- <label id="email-container" class="container">
- <input id="email" type="email" required="required" />
- <span data-l10n-id="autofill-address-email" class="label-text" />
- </label>
- </form>
+ <form id="form" class="editAddressForm" autocomplete="off"></form>
<div id="controls-container">
<span
id="country-warning-message"
@@ -88,31 +36,25 @@
<button id="save" class="primary" data-l10n-id="autofill-save-button" />
</moz-button-group>
</div>
- <script>
- <![CDATA[
- "use strict";
+ <!-- eslint-disable -->
+ <script type="module">
+ import { createFormLayoutFromRecord } from "chrome://formautofill/content/addressFormLayout.mjs";
+ import { EditAddressDialog } from "chrome://formautofill/content/editDialog.mjs";
- const {
- record,
- noValidate,
- } = window.arguments?.[0] ?? {};
+ const { record, noValidate, l10nStrings } = window.arguments?.[0] ?? {};
+ const formElement = document.querySelector("form");
+ formElement.noValidate = !!noValidate;
+ createFormLayoutFromRecord(formElement, record, l10nStrings);
- /* import-globals-from autofillEditForms.js */
- const fieldContainer = new EditAddress({
- form: document.getElementById("form"),
- }, record, {
- noValidate,
- });
-
- /* import-globals-from editDialog.js */
- new EditAddressDialog({
- title: document.querySelector("title"),
- fieldContainer,
- controlsContainer: document.getElementById("controls-container"),
- cancel: document.getElementById("cancel"),
- save: document.getElementById("save"),
- }, record);
- ]]>
+ new EditAddressDialog(
+ {
+ title: document.querySelector("title"),
+ cancel: document.getElementById("cancel"),
+ save: document.getElementById("save"),
+ },
+ record
+ );
</script>
+ <!-- eslint-enable -->
</body>
</html>
diff --git a/browser/extensions/formautofill/content/editCreditCard.xhtml b/browser/extensions/formautofill/content/editCreditCard.xhtml
index 920be841c5..8fceb5709b 100644
--- a/browser/extensions/formautofill/content/editCreditCard.xhtml
+++ b/browser/extensions/formautofill/content/editCreditCard.xhtml
@@ -19,8 +19,6 @@
rel="stylesheet"
href="chrome://formautofill/content/skin/editDialog.css"
/>
- <script src="chrome://formautofill/content/editDialog.js"></script>
- <script src="chrome://formautofill/content/autofillEditForms.js"></script>
</head>
<body>
<form id="form" class="editCreditCardForm contentPane" autocomplete="off">
@@ -87,36 +85,32 @@
<button id="cancel" data-l10n-id="autofill-cancel-button" />
<button id="save" class="primary" data-l10n-id="autofill-save-button" />
</div>
- <script>
- <![CDATA[
- "use strict";
- /* import-globals-from editDialog.js */
+ <!-- eslint-disable -->
+ <script type="module">
+ import { EditCreditCardDialog } from "chrome://formautofill/content/editDialog.mjs";
+ import { EditCreditCard } from "chrome://formautofill/content/autofillEditForms.mjs";
+ const { record } = window.arguments?.[0] ?? {};
- (async () => {
- const {
- record,
- } = window.arguments?.[0] ?? {};
+ const fieldContainer = new EditCreditCard(
+ {
+ form: document.getElementById("form"),
+ },
+ record,
+ []
+ );
- const addresses = {};
- for (let address of await formAutofillStorage.addresses.getAll()) {
- addresses[address.guid] = address;
- }
-
- /* import-globals-from autofillEditForms.js */
- const fieldContainer = new EditCreditCard({
- form: document.getElementById("form"),
- }, record, addresses);
-
- new EditCreditCardDialog({
- title: document.querySelector("title"),
- fieldContainer,
- controlsContainer: document.getElementById("controls-container"),
- cancel: document.getElementById("cancel"),
- save: document.getElementById("save"),
- }, record);
- })();
- ]]>
+ new EditCreditCardDialog(
+ {
+ title: document.querySelector("title"),
+ fieldContainer,
+ controlsContainer: document.getElementById("controls-container"),
+ cancel: document.getElementById("cancel"),
+ save: document.getElementById("save"),
+ },
+ record
+ );
</script>
+ <!-- eslint-enable -->
</body>
</html>
diff --git a/browser/extensions/formautofill/content/editDialog.js b/browser/extensions/formautofill/content/editDialog.js
deleted file mode 100644
index 467acbdd07..0000000000
--- a/browser/extensions/formautofill/content/editDialog.js
+++ /dev/null
@@ -1,233 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-/* exported EditAddressDialog, EditCreditCardDialog */
-/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
-
-"use strict";
-
-ChromeUtils.defineESModuleGetters(this, {
- AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs",
- formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
-});
-
-class AutofillEditDialog {
- constructor(subStorageName, elements, record) {
- this._storageInitPromise = formAutofillStorage.initialize();
- this._subStorageName = subStorageName;
- this._elements = elements;
- this._record = record;
- this.localizeDocument();
- window.addEventListener("DOMContentLoaded", this, { once: true });
- }
-
- async init() {
- this.updateSaveButtonState();
- this.attachEventListeners();
- // For testing only: signal to tests that the dialog is ready for testing.
- // This is likely no longer needed since retrieving from storage is fully
- // handled in manageDialog.js now.
- window.dispatchEvent(new CustomEvent("FormReady"));
- }
-
- /**
- * Get storage and ensure it has been initialized.
- *
- * @returns {object}
- */
- async getStorage() {
- await this._storageInitPromise;
- return formAutofillStorage[this._subStorageName];
- }
-
- /**
- * Asks FormAutofillParent to save or update an record.
- *
- * @param {object} record
- * @param {string} guid [optional]
- */
- async saveRecord(record, guid) {
- let storage = await this.getStorage();
- if (guid) {
- await storage.update(guid, record);
- } else {
- await storage.add(record);
- }
- }
-
- /**
- * Handle events
- *
- * @param {DOMEvent} event
- */
- handleEvent(event) {
- switch (event.type) {
- case "DOMContentLoaded": {
- this.init();
- break;
- }
- case "click": {
- this.handleClick(event);
- break;
- }
- case "input": {
- this.handleInput(event);
- break;
- }
- case "keypress": {
- this.handleKeyPress(event);
- break;
- }
- case "contextmenu": {
- if (
- !HTMLInputElement.isInstance(event.target) &&
- !HTMLTextAreaElement.isInstance(event.target)
- ) {
- event.preventDefault();
- }
- break;
- }
- }
- }
-
- /**
- * Handle click events
- *
- * @param {DOMEvent} event
- */
- handleClick(event) {
- if (event.target == this._elements.cancel) {
- window.close();
- }
- if (event.target == this._elements.save) {
- this.handleSubmit();
- }
- }
-
- /**
- * Handle input events
- */
- handleInput(_e) {
- this.updateSaveButtonState();
- }
-
- /**
- * Handle key press events
- *
- * @param {DOMEvent} event
- */
- handleKeyPress(event) {
- if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
- window.close();
- }
- }
-
- updateSaveButtonState() {
- // Toggle disabled attribute on the save button based on
- // whether the form is filled or empty.
- if (!Object.keys(this._elements.fieldContainer.buildFormObject()).length) {
- this._elements.save.setAttribute("disabled", true);
- } else {
- this._elements.save.removeAttribute("disabled");
- }
- }
-
- /**
- * Attach event listener
- */
- attachEventListeners() {
- window.addEventListener("keypress", this);
- window.addEventListener("contextmenu", this);
- this._elements.controlsContainer.addEventListener("click", this);
- document.addEventListener("input", this);
- }
-
- // An interface to be inherited.
- localizeDocument() {}
-
- recordFormSubmit() {
- let method = this._record?.guid ? "edit" : "add";
- AutofillTelemetry.recordManageEvent(this.telemetryType, method);
- }
-}
-
-class EditAddressDialog extends AutofillEditDialog {
- telemetryType = AutofillTelemetry.ADDRESS;
-
- constructor(elements, record) {
- super("addresses", elements, record);
- if (record) {
- AutofillTelemetry.recordManageEvent(this.telemetryType, "show_entry");
- }
- }
-
- localizeDocument() {
- if (this._record?.guid) {
- document.l10n.setAttributes(
- this._elements.title,
- "autofill-edit-address-title"
- );
- }
- }
-
- async handleSubmit() {
- await this.saveRecord(
- this._elements.fieldContainer.buildFormObject(),
- this._record ? this._record.guid : null
- );
- this.recordFormSubmit();
-
- window.close();
- }
-}
-
-class EditCreditCardDialog extends AutofillEditDialog {
- telemetryType = AutofillTelemetry.CREDIT_CARD;
-
- constructor(elements, record) {
- elements.fieldContainer._elements.billingAddress.disabled = true;
- super("creditCards", elements, record);
- elements.fieldContainer._elements.ccNumber.addEventListener(
- "blur",
- this._onCCNumberFieldBlur.bind(this)
- );
- if (record) {
- AutofillTelemetry.recordManageEvent(this.telemetryType, "show_entry");
- }
- }
-
- _onCCNumberFieldBlur() {
- let elem = this._elements.fieldContainer._elements.ccNumber;
- this._elements.fieldContainer.updateCustomValidity(elem);
- }
-
- localizeDocument() {
- if (this._record?.guid) {
- document.l10n.setAttributes(
- this._elements.title,
- "autofill-edit-card-title2"
- );
- }
- }
-
- async handleSubmit() {
- let creditCard = this._elements.fieldContainer.buildFormObject();
- if (!this._elements.fieldContainer._elements.form.reportValidity()) {
- return;
- }
-
- try {
- await this.saveRecord(
- creditCard,
- this._record ? this._record.guid : null
- );
-
- this.recordFormSubmit();
-
- window.close();
- } catch (ex) {
- console.error(ex);
- }
- }
-}
diff --git a/browser/extensions/formautofill/content/editDialog.mjs b/browser/extensions/formautofill/content/editDialog.mjs
new file mode 100644
index 0000000000..5371051e12
--- /dev/null
+++ b/browser/extensions/formautofill/content/editDialog.mjs
@@ -0,0 +1,253 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
+
+import {
+ getCurrentFormData,
+ canSubmitForm,
+} from "chrome://formautofill/content/addressFormLayout.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs",
+ formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
+});
+
+class AutofillEditDialog {
+ constructor(subStorageName, elements, record) {
+ this._storageInitPromise = lazy.formAutofillStorage.initialize();
+ this._subStorageName = subStorageName;
+ this._elements = elements;
+ this._record = record;
+ this.localizeDocument();
+ window.addEventListener("load", this, { once: true });
+ }
+
+ async init() {
+ this.updateSaveButtonState();
+ this.attachEventListeners();
+ // For testing only: signal to tests that the dialog is ready for testing.
+ // This is likely no longer needed since retrieving from storage is fully
+ // handled in manageDialog.js now.
+ window.dispatchEvent(new CustomEvent("FormReadyForTests"));
+ }
+
+ /**
+ * Get storage and ensure it has been initialized.
+ *
+ * @returns {object}
+ */
+ async getStorage() {
+ await this._storageInitPromise;
+ return lazy.formAutofillStorage[this._subStorageName];
+ }
+
+ /**
+ * Asks FormAutofillParent to save or update an record.
+ *
+ * @param {object} record
+ * @param {string} guid [optional]
+ */
+ async saveRecord(record, guid) {
+ let storage = await this.getStorage();
+ if (guid) {
+ await storage.update(guid, record);
+ } else {
+ await storage.add(record);
+ }
+ }
+
+ /**
+ * Handle events
+ *
+ * @param {DOMEvent} event
+ */
+ handleEvent(event) {
+ switch (event.type) {
+ case "load": {
+ this.init();
+ break;
+ }
+ case "click": {
+ this.handleClick(event);
+ break;
+ }
+ case "input": {
+ this.handleInput(event);
+ break;
+ }
+ case "keypress": {
+ this.handleKeyPress(event);
+ break;
+ }
+ case "contextmenu": {
+ if (
+ !HTMLInputElement.isInstance(event.target) &&
+ !HTMLTextAreaElement.isInstance(event.target)
+ ) {
+ event.preventDefault();
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle click events
+ *
+ * @param {DOMEvent} event
+ */
+ handleClick(event) {
+ if (event.target == this._elements.cancel) {
+ window.close();
+ }
+ if (event.target == this._elements.save) {
+ this.handleSubmit();
+ }
+ }
+
+ /**
+ * Handle input events
+ */
+ handleInput(_e) {
+ this.updateSaveButtonState();
+ }
+
+ /**
+ * Handle key press events
+ *
+ * @param {DOMEvent} event
+ */
+ handleKeyPress(event) {
+ if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
+ window.close();
+ }
+ }
+
+ updateSaveButtonState() {
+ // Toggle disabled attribute on the save button based on
+ // whether the form is filled or empty.
+ if (!Object.keys(this._elements.fieldContainer.buildFormObject()).length) {
+ this._elements.save.setAttribute("disabled", true);
+ } else {
+ this._elements.save.removeAttribute("disabled");
+ }
+ }
+
+ /**
+ * Attach event listener
+ */
+ attachEventListeners() {
+ window.addEventListener("keypress", this);
+ window.addEventListener("contextmenu", this);
+ this._elements.save.addEventListener("click", this);
+ this._elements.cancel.addEventListener("click", this);
+ document.addEventListener("input", this);
+ }
+
+ // An interface to be inherited.
+ localizeDocument() {}
+
+ recordFormSubmit() {
+ let method = this._record?.guid ? "edit" : "add";
+ lazy.AutofillTelemetry.recordManageEvent(this.telemetryType, method);
+ }
+}
+
+export class EditAddressDialog extends AutofillEditDialog {
+ telemetryType = lazy.AutofillTelemetry.ADDRESS;
+
+ constructor(elements, record) {
+ super("addresses", elements, record);
+ if (record) {
+ lazy.AutofillTelemetry.recordManageEvent(
+ this.telemetryType,
+ "show_entry"
+ );
+ }
+ }
+
+ localizeDocument() {
+ if (this._record?.guid) {
+ document.l10n.setAttributes(
+ this._elements.title,
+ "autofill-edit-address-title"
+ );
+ }
+ }
+
+ updateSaveButtonState() {
+ // Toggle disabled attribute on the save button based on
+ // whether the form is filled or empty.
+ if (!canSubmitForm()) {
+ this._elements.save.setAttribute("disabled", true);
+ } else {
+ this._elements.save.removeAttribute("disabled");
+ }
+ }
+
+ async handleSubmit() {
+ await this.saveRecord(
+ getCurrentFormData(),
+ this._record ? this._record.guid : null
+ );
+ this.recordFormSubmit();
+
+ window.close();
+ }
+}
+
+export class EditCreditCardDialog extends AutofillEditDialog {
+ telemetryType = lazy.AutofillTelemetry.CREDIT_CARD;
+
+ constructor(elements, record) {
+ elements.fieldContainer._elements.billingAddress.disabled = true;
+ super("creditCards", elements, record);
+ elements.fieldContainer._elements.ccNumber.addEventListener(
+ "blur",
+ this._onCCNumberFieldBlur.bind(this)
+ );
+ if (record) {
+ lazy.AutofillTelemetry.recordManageEvent(
+ this.telemetryType,
+ "show_entry"
+ );
+ }
+ }
+
+ _onCCNumberFieldBlur() {
+ let elem = this._elements.fieldContainer._elements.ccNumber;
+ this._elements.fieldContainer.updateCustomValidity(elem);
+ }
+
+ localizeDocument() {
+ if (this._record?.guid) {
+ document.l10n.setAttributes(
+ this._elements.title,
+ "autofill-edit-card-title2"
+ );
+ }
+ }
+
+ async handleSubmit() {
+ let creditCard = this._elements.fieldContainer.buildFormObject();
+ if (!this._elements.fieldContainer._elements.form.reportValidity()) {
+ return;
+ }
+
+ try {
+ await this.saveRecord(
+ creditCard,
+ this._record ? this._record.guid : null
+ );
+
+ this.recordFormSubmit();
+
+ window.close();
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+}
diff --git a/browser/extensions/formautofill/content/manageAddresses.xhtml b/browser/extensions/formautofill/content/manageAddresses.xhtml
index 68e810179e..2c8f0608f7 100644
--- a/browser/extensions/formautofill/content/manageAddresses.xhtml
+++ b/browser/extensions/formautofill/content/manageAddresses.xhtml
@@ -16,7 +16,6 @@
rel="stylesheet"
href="chrome://formautofill/content/manageDialog.css"
/>
- <script src="chrome://formautofill/content/manageDialog.js"></script>
</head>
<body>
<fieldset>
@@ -39,9 +38,12 @@
data-l10n-id="autofill-manage-edit-button"
/>
</div>
- <script>
- "use strict";
- /* global ManageAddresses */
+ <!-- eslint-disable -->
+ <!-- For some reason eslint complains here about import only available for sourceType: "module" -->
+ <!-- even though type is set to module.-->
+ <script type="module">
+ import { ManageAddresses } from "chrome://formautofill/content/manageDialog.mjs";
+
new ManageAddresses({
records: document.getElementById("addresses"),
controlsContainer: document.getElementById("controls-container"),
@@ -50,5 +52,6 @@
edit: document.getElementById("edit"),
});
</script>
+ <!-- eslint-enable -->
</body>
</html>
diff --git a/browser/extensions/formautofill/content/manageCreditCards.xhtml b/browser/extensions/formautofill/content/manageCreditCards.xhtml
index e7baf9d364..69aae82df9 100644
--- a/browser/extensions/formautofill/content/manageCreditCards.xhtml
+++ b/browser/extensions/formautofill/content/manageCreditCards.xhtml
@@ -18,7 +18,6 @@
rel="stylesheet"
href="chrome://formautofill/content/manageDialog.css"
/>
- <script src="chrome://formautofill/content/manageDialog.js"></script>
</head>
<body>
<fieldset>
@@ -41,9 +40,12 @@
data-l10n-id="autofill-manage-edit-button"
/>
</div>
- <script>
- "use strict";
- /* global ManageCreditCards */
+ <!-- eslint-disable -->
+ <!-- For some reason eslint complains here about import only available for sourceType: "module" -->
+ <!-- eventhough type is set to module -->
+ <script type="module">
+ import { ManageCreditCards } from "chrome://formautofill/content/manageDialog.mjs";
+
new ManageCreditCards({
records: document.getElementById("credit-cards"),
controlsContainer: document.getElementById("controls-container"),
@@ -52,5 +54,6 @@
edit: document.getElementById("edit"),
});
</script>
+ <!-- eslint-enable -->
</body>
</html>
diff --git a/browser/extensions/formautofill/content/manageDialog.js b/browser/extensions/formautofill/content/manageDialog.js
deleted file mode 100644
index ad5cefbb15..0000000000
--- a/browser/extensions/formautofill/content/manageDialog.js
+++ /dev/null
@@ -1,454 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-/* exported ManageAddresses, ManageCreditCards */
-
-"use strict";
-
-const EDIT_ADDRESS_URL = "chrome://formautofill/content/editAddress.xhtml";
-const EDIT_CREDIT_CARD_URL =
- "chrome://formautofill/content/editCreditCard.xhtml";
-
-const { AppConstants } = ChromeUtils.importESModule(
- "resource://gre/modules/AppConstants.sys.mjs"
-);
-const { FormAutofill } = ChromeUtils.importESModule(
- "resource://autofill/FormAutofill.sys.mjs"
-);
-const { AutofillTelemetry } = ChromeUtils.importESModule(
- "resource://gre/modules/shared/AutofillTelemetry.sys.mjs"
-);
-
-ChromeUtils.defineESModuleGetters(this, {
- CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
- FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
- OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
- formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
-});
-
-this.log = null;
-ChromeUtils.defineLazyGetter(this, "log", () =>
- FormAutofill.defineLogGetter(this, "manageAddresses")
-);
-
-class ManageRecords {
- constructor(subStorageName, elements) {
- this._storageInitPromise = formAutofillStorage.initialize();
- this._subStorageName = subStorageName;
- this._elements = elements;
- this._newRequest = false;
- this._isLoadingRecords = false;
- this.prefWin = window.opener;
- window.addEventListener("DOMContentLoaded", this, { once: true });
- }
-
- async init() {
- await this.loadRecords();
- this.attachEventListeners();
- // For testing only: Notify when the dialog is ready for interaction
- window.dispatchEvent(new CustomEvent("FormReady"));
- }
-
- uninit() {
- log.debug("uninit");
- this.detachEventListeners();
- this._elements = null;
- }
-
- /**
- * Get the selected options on the addresses element.
- *
- * @returns {Array<DOMElement>}
- */
- get _selectedOptions() {
- return Array.from(this._elements.records.selectedOptions);
- }
-
- /**
- * Get storage and ensure it has been initialized.
- *
- * @returns {object}
- */
- async getStorage() {
- await this._storageInitPromise;
- return formAutofillStorage[this._subStorageName];
- }
-
- /**
- * Load records and render them. This function is a wrapper for _loadRecords
- * to ensure any reentrant will be handled well.
- */
- async loadRecords() {
- // This function can be early returned when there is any reentrant happends.
- // "_newRequest" needs to be set to ensure all changes will be applied.
- if (this._isLoadingRecords) {
- this._newRequest = true;
- return;
- }
- this._isLoadingRecords = true;
-
- await this._loadRecords();
-
- // _loadRecords should be invoked again if there is any multiple entrant
- // during running _loadRecords(). This step ensures that the latest request
- // still is applied.
- while (this._newRequest) {
- this._newRequest = false;
- await this._loadRecords();
- }
- this._isLoadingRecords = false;
-
- // For testing only: Notify when records are loaded
- this._elements.records.dispatchEvent(new CustomEvent("RecordsLoaded"));
- }
-
- async _loadRecords() {
- let storage = await this.getStorage();
- let records = await storage.getAll();
- // Sort by last used time starting with most recent
- records.sort((a, b) => {
- let aLastUsed = a.timeLastUsed || a.timeLastModified;
- let bLastUsed = b.timeLastUsed || b.timeLastModified;
- return bLastUsed - aLastUsed;
- });
- await this.renderRecordElements(records);
- this.updateButtonsStates(this._selectedOptions.length);
- }
-
- /**
- * Render the records onto the page while maintaining selected options if
- * they still exist.
- *
- * @param {Array<object>} records
- */
- async renderRecordElements(records) {
- let selectedGuids = this._selectedOptions.map(option => option.value);
- this.clearRecordElements();
- for (let record of records) {
- let { id, args, raw } = await this.getLabelInfo(record);
- let option = new Option(
- raw ?? "",
- record.guid,
- false,
- selectedGuids.includes(record.guid)
- );
- if (id) {
- document.l10n.setAttributes(option, id, args);
- }
-
- option.record = record;
- this._elements.records.appendChild(option);
- }
- }
-
- /**
- * Remove all existing record elements.
- */
- clearRecordElements() {
- let parent = this._elements.records;
- while (parent.lastChild) {
- parent.removeChild(parent.lastChild);
- }
- }
-
- /**
- * Remove records by selected options.
- *
- * @param {Array<DOMElement>} options
- */
- async removeRecords(options) {
- let storage = await this.getStorage();
- // Pause listening to storage change event to avoid triggering `loadRecords`
- // when removing records
- Services.obs.removeObserver(this, "formautofill-storage-changed");
-
- for (let option of options) {
- storage.remove(option.value);
- option.remove();
- }
- this.updateButtonsStates(this._selectedOptions);
-
- // Resume listening to storage change event
- Services.obs.addObserver(this, "formautofill-storage-changed");
- // For testing only: notify record(s) has been removed
- this._elements.records.dispatchEvent(new CustomEvent("RecordsRemoved"));
-
- for (let i = 0; i < options.length; i++) {
- AutofillTelemetry.recordManageEvent(this.telemetryType, "delete");
- }
- }
-
- /**
- * Enable/disable the Edit and Remove buttons based on number of selected
- * options.
- *
- * @param {number} selectedCount
- */
- updateButtonsStates(selectedCount) {
- log.debug("updateButtonsStates:", selectedCount);
- if (selectedCount == 0) {
- this._elements.edit.setAttribute("disabled", "disabled");
- this._elements.remove.setAttribute("disabled", "disabled");
- } else if (selectedCount == 1) {
- this._elements.edit.removeAttribute("disabled");
- this._elements.remove.removeAttribute("disabled");
- } else if (selectedCount > 1) {
- this._elements.edit.setAttribute("disabled", "disabled");
- this._elements.remove.removeAttribute("disabled");
- }
- this._elements.add.disabled = !Services.prefs.getBoolPref(
- `extensions.formautofill.${this._subStorageName}.enabled`
- );
- }
-
- /**
- * Handle events
- *
- * @param {DOMEvent} event
- */
- handleEvent(event) {
- switch (event.type) {
- case "DOMContentLoaded": {
- this.init();
- break;
- }
- case "click": {
- this.handleClick(event);
- break;
- }
- case "change": {
- this.updateButtonsStates(this._selectedOptions.length);
- break;
- }
- case "unload": {
- this.uninit();
- break;
- }
- case "keypress": {
- this.handleKeyPress(event);
- break;
- }
- case "contextmenu": {
- event.preventDefault();
- break;
- }
- }
- }
-
- /**
- * Handle click events
- *
- * @param {DOMEvent} event
- */
- handleClick(event) {
- if (event.target == this._elements.remove) {
- this.removeRecords(this._selectedOptions);
- } else if (event.target == this._elements.add) {
- this.openEditDialog();
- } else if (
- event.target == this._elements.edit ||
- (event.target.parentNode == this._elements.records && event.detail > 1)
- ) {
- this.openEditDialog(this._selectedOptions[0].record);
- }
- }
-
- /**
- * Handle key press events
- *
- * @param {DOMEvent} event
- */
- handleKeyPress(event) {
- if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
- window.close();
- }
- if (event.keyCode == KeyEvent.DOM_VK_DELETE) {
- this.removeRecords(this._selectedOptions);
- }
- }
-
- observe(_subject, topic, _data) {
- switch (topic) {
- case "formautofill-storage-changed": {
- this.loadRecords();
- }
- }
- }
-
- /**
- * Attach event listener
- */
- attachEventListeners() {
- window.addEventListener("unload", this, { once: true });
- window.addEventListener("keypress", this);
- window.addEventListener("contextmenu", this);
- this._elements.records.addEventListener("change", this);
- this._elements.records.addEventListener("click", this);
- this._elements.controlsContainer.addEventListener("click", this);
- Services.obs.addObserver(this, "formautofill-storage-changed");
- }
-
- /**
- * Remove event listener
- */
- detachEventListeners() {
- window.removeEventListener("keypress", this);
- window.removeEventListener("contextmenu", this);
- this._elements.records.removeEventListener("change", this);
- this._elements.records.removeEventListener("click", this);
- this._elements.controlsContainer.removeEventListener("click", this);
- Services.obs.removeObserver(this, "formautofill-storage-changed");
- }
-}
-
-class ManageAddresses extends ManageRecords {
- telemetryType = AutofillTelemetry.ADDRESS;
-
- constructor(elements) {
- super("addresses", elements);
- elements.add.setAttribute(
- "search-l10n-ids",
- FormAutofillUtils.EDIT_ADDRESS_L10N_IDS.join(",")
- );
- AutofillTelemetry.recordManageEvent(this.telemetryType, "show");
- }
-
- /**
- * Open the edit address dialog to create/edit an address.
- *
- * @param {object} address [optional]
- */
- openEditDialog(address) {
- this.prefWin.gSubDialog.open(EDIT_ADDRESS_URL, undefined, {
- record: address,
- // Don't validate in preferences since it's fine for fields to be missing
- // for autofill purposes. For PaymentRequest addresses get more validation.
- noValidate: true,
- });
- }
-
- getLabelInfo(address) {
- return { raw: FormAutofillUtils.getAddressLabel(address) };
- }
-}
-
-class ManageCreditCards extends ManageRecords {
- telemetryType = AutofillTelemetry.CREDIT_CARD;
-
- constructor(elements) {
- super("creditCards", elements);
- elements.add.setAttribute(
- "search-l10n-ids",
- FormAutofillUtils.EDIT_CREDITCARD_L10N_IDS.join(",")
- );
-
- this._isDecrypted = false;
- AutofillTelemetry.recordManageEvent(this.telemetryType, "show");
- }
-
- /**
- * Open the edit address dialog to create/edit a credit card.
- *
- * @param {object} creditCard [optional]
- */
- async openEditDialog(creditCard) {
- // Ask for reauth if user is trying to edit an existing credit card.
- if (creditCard) {
- const promptMessage = FormAutofillUtils.reauthOSPromptMessage(
- "autofill-edit-payment-method-os-prompt-macos",
- "autofill-edit-payment-method-os-prompt-windows",
- "autofill-edit-payment-method-os-prompt-other"
- );
-
- const loggedIn = await FormAutofillUtils.ensureLoggedIn(promptMessage);
- if (!loggedIn.authenticated) {
- return;
- }
- }
-
- let decryptedCCNumObj = {};
- if (creditCard && creditCard["cc-number-encrypted"]) {
- try {
- decryptedCCNumObj["cc-number"] = await OSKeyStore.decrypt(
- creditCard["cc-number-encrypted"]
- );
- } catch (ex) {
- if (ex.result == Cr.NS_ERROR_ABORT) {
- // User shouldn't be ask to reauth here, but it could happen.
- // Return here and skip opening the dialog.
- return;
- }
- // We've got ourselves a real error.
- // Recover from encryption error so the user gets a chance to re-enter
- // unencrypted credit card number.
- decryptedCCNumObj["cc-number"] = "";
- console.error(ex);
- }
- }
- let decryptedCreditCard = Object.assign({}, creditCard, decryptedCCNumObj);
- this.prefWin.gSubDialog.open(
- EDIT_CREDIT_CARD_URL,
- { features: "resizable=no" },
- {
- record: decryptedCreditCard,
- }
- );
- }
-
- /**
- * Get credit card display label. It should display masked numbers and the
- * cardholder's name, separated by a comma.
- *
- * @param {object} creditCard
- * @returns {Promise<string>}
- */
- async getLabelInfo(creditCard) {
- // The card type is displayed visually using an image. For a11y, we need
- // to expose it as text. We do this using aria-label. However,
- // aria-label overrides the text content, so we must include that also.
- // Since the text content is generated by Fluent, aria-label must be
- // generated by Fluent also.
- const type = creditCard["cc-type"];
- const typeL10nId = CreditCard.getNetworkL10nId(type);
- const typeName = typeL10nId
- ? await document.l10n.formatValue(typeL10nId)
- : type ?? ""; // Unknown card type
- return CreditCard.getLabelInfo({
- name: creditCard["cc-name"],
- number: creditCard["cc-number"],
- month: creditCard["cc-exp-month"],
- year: creditCard["cc-exp-year"],
- type: typeName,
- });
- }
-
- async renderRecordElements(records) {
- // Revert back to encrypted form when re-rendering happens
- this._isDecrypted = false;
- // Display third-party card icons when possible
- this._elements.records.classList.toggle(
- "branded",
- AppConstants.MOZILLA_OFFICIAL
- );
- await super.renderRecordElements(records);
-
- let options = this._elements.records.options;
- for (let option of options) {
- let record = option.record;
- if (record && record["cc-type"]) {
- option.setAttribute("cc-type", record["cc-type"]);
- } else {
- option.removeAttribute("cc-type");
- }
- }
- }
-
- updateButtonsStates(selectedCount) {
- super.updateButtonsStates(selectedCount);
- }
-
- handleClick(event) {
- super.handleClick(event);
- }
-}
diff --git a/browser/extensions/formautofill/content/manageDialog.mjs b/browser/extensions/formautofill/content/manageDialog.mjs
new file mode 100644
index 0000000000..bca0f48f40
--- /dev/null
+++ b/browser/extensions/formautofill/content/manageDialog.mjs
@@ -0,0 +1,474 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EDIT_ADDRESS_URL = "chrome://formautofill/content/editAddress.xhtml";
+const EDIT_CREDIT_CARD_URL =
+ "chrome://formautofill/content/editCreditCard.xhtml";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { FormAutofill } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofill.sys.mjs"
+);
+const { AutofillTelemetry } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/AutofillTelemetry.sys.mjs"
+);
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+ OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
+ formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "log", () =>
+ FormAutofill.defineLogGetter(lazy, "manageAddresses")
+);
+
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "l10n",
+ () => new Localization([" browser/preferences/formAutofill.ftl"], true)
+);
+
+class ManageRecords {
+ constructor(subStorageName, elements) {
+ this._storageInitPromise = lazy.formAutofillStorage.initialize();
+ this._subStorageName = subStorageName;
+ this._elements = elements;
+ this._newRequest = false;
+ this._isLoadingRecords = false;
+ this.prefWin = window.opener;
+ window.addEventListener("load", this, { once: true });
+ }
+
+ async init() {
+ await this.loadRecords();
+ this.attachEventListeners();
+ // For testing only: Notify when the dialog is ready for interaction
+ window.dispatchEvent(new CustomEvent("FormReadyForTests"));
+ }
+
+ uninit() {
+ lazy.log.debug("uninit");
+ this.detachEventListeners();
+ this._elements = null;
+ }
+
+ /**
+ * Get the selected options on the addresses element.
+ *
+ * @returns {Array<DOMElement>}
+ */
+ get _selectedOptions() {
+ return Array.from(this._elements.records.selectedOptions);
+ }
+
+ /**
+ * Get storage and ensure it has been initialized.
+ *
+ * @returns {object}
+ */
+ async getStorage() {
+ await this._storageInitPromise;
+ return lazy.formAutofillStorage[this._subStorageName];
+ }
+
+ /**
+ * Load records and render them. This function is a wrapper for _loadRecords
+ * to ensure any reentrant will be handled well.
+ */
+ async loadRecords() {
+ // This function can be early returned when there is any reentrant happends.
+ // "_newRequest" needs to be set to ensure all changes will be applied.
+ if (this._isLoadingRecords) {
+ this._newRequest = true;
+ return;
+ }
+ this._isLoadingRecords = true;
+
+ await this._loadRecords();
+
+ // _loadRecords should be invoked again if there is any multiple entrant
+ // during running _loadRecords(). This step ensures that the latest request
+ // still is applied.
+ while (this._newRequest) {
+ this._newRequest = false;
+ await this._loadRecords();
+ }
+ this._isLoadingRecords = false;
+
+ // For testing only: Notify when records are loaded
+ this._elements.records.dispatchEvent(new CustomEvent("RecordsLoaded"));
+ }
+
+ async _loadRecords() {
+ let storage = await this.getStorage();
+ let records = await storage.getAll();
+ // Sort by last used time starting with most recent
+ records.sort((a, b) => {
+ let aLastUsed = a.timeLastUsed || a.timeLastModified;
+ let bLastUsed = b.timeLastUsed || b.timeLastModified;
+ return bLastUsed - aLastUsed;
+ });
+ await this.renderRecordElements(records);
+ this.updateButtonsStates(this._selectedOptions.length);
+ }
+
+ /**
+ * Render the records onto the page while maintaining selected options if
+ * they still exist.
+ *
+ * @param {Array<object>} records
+ */
+ async renderRecordElements(records) {
+ let selectedGuids = this._selectedOptions.map(option => option.value);
+ this.clearRecordElements();
+ for (let record of records) {
+ let { id, args, raw } = await this.getLabelInfo(record);
+ let option = new Option(
+ raw ?? "",
+ record.guid,
+ false,
+ selectedGuids.includes(record.guid)
+ );
+ if (id) {
+ document.l10n.setAttributes(option, id, args);
+ }
+
+ option.record = record;
+ this._elements.records.appendChild(option);
+ }
+ }
+
+ /**
+ * Remove all existing record elements.
+ */
+ clearRecordElements() {
+ const parentElement = this._elements.records;
+ while (parentElement.lastChild) {
+ parentElement.removeChild(parentElement.lastChild);
+ }
+ }
+
+ /**
+ * Remove records by selected options.
+ *
+ * @param {Array<DOMElement>} options
+ */
+ async removeRecords(options) {
+ let storage = await this.getStorage();
+ // Pause listening to storage change event to avoid triggering `loadRecords`
+ // when removing records
+ Services.obs.removeObserver(this, "formautofill-storage-changed");
+
+ for (let option of options) {
+ storage.remove(option.value);
+ option.remove();
+ }
+ this.updateButtonsStates(this._selectedOptions);
+
+ // Resume listening to storage change event
+ Services.obs.addObserver(this, "formautofill-storage-changed");
+ // For testing only: notify record(s) has been removed
+ this._elements.records.dispatchEvent(new CustomEvent("RecordsRemoved"));
+
+ for (let i = 0; i < options.length; i++) {
+ AutofillTelemetry.recordManageEvent(this.telemetryType, "delete");
+ }
+ }
+
+ /**
+ * Enable/disable the Edit and Remove buttons based on number of selected
+ * options.
+ *
+ * @param {number} selectedCount
+ */
+ updateButtonsStates(selectedCount) {
+ lazy.log.debug("updateButtonsStates:", selectedCount);
+ if (selectedCount == 0) {
+ this._elements.edit.setAttribute("disabled", "disabled");
+ this._elements.remove.setAttribute("disabled", "disabled");
+ } else if (selectedCount == 1) {
+ this._elements.edit.removeAttribute("disabled");
+ this._elements.remove.removeAttribute("disabled");
+ } else if (selectedCount > 1) {
+ this._elements.edit.setAttribute("disabled", "disabled");
+ this._elements.remove.removeAttribute("disabled");
+ }
+ this._elements.add.disabled = !Services.prefs.getBoolPref(
+ `extensions.formautofill.${this._subStorageName}.enabled`
+ );
+ }
+
+ /**
+ * Handle events
+ *
+ * @param {DOMEvent} event
+ */
+ handleEvent(event) {
+ switch (event.type) {
+ case "load": {
+ this.init();
+ break;
+ }
+ case "click": {
+ this.handleClick(event);
+ break;
+ }
+ case "change": {
+ this.updateButtonsStates(this._selectedOptions.length);
+ break;
+ }
+ case "unload": {
+ this.uninit();
+ break;
+ }
+ case "keypress": {
+ this.handleKeyPress(event);
+ break;
+ }
+ case "contextmenu": {
+ event.preventDefault();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle click events
+ *
+ * @param {DOMEvent} event
+ */
+ handleClick(event) {
+ if (event.target == this._elements.remove) {
+ this.removeRecords(this._selectedOptions);
+ } else if (event.target == this._elements.add) {
+ this.openEditDialog();
+ } else if (
+ event.target == this._elements.edit ||
+ (event.target.parentNode == this._elements.records && event.detail > 1)
+ ) {
+ this.openEditDialog(this._selectedOptions[0].record);
+ }
+ }
+
+ /**
+ * Handle key press events
+ *
+ * @param {DOMEvent} event
+ */
+ handleKeyPress(event) {
+ if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
+ window.close();
+ }
+ if (event.keyCode == KeyEvent.DOM_VK_DELETE) {
+ this.removeRecords(this._selectedOptions);
+ }
+ }
+
+ observe(_subject, topic, _data) {
+ switch (topic) {
+ case "formautofill-storage-changed": {
+ this.loadRecords();
+ }
+ }
+ }
+
+ /**
+ * Attach event listener
+ */
+ attachEventListeners() {
+ window.addEventListener("unload", this, { once: true });
+ window.addEventListener("keypress", this);
+ window.addEventListener("contextmenu", this);
+ this._elements.records.addEventListener("change", this);
+ this._elements.records.addEventListener("click", this);
+ this._elements.controlsContainer.addEventListener("click", this);
+ Services.obs.addObserver(this, "formautofill-storage-changed");
+ }
+
+ /**
+ * Remove event listener
+ */
+ detachEventListeners() {
+ window.removeEventListener("keypress", this);
+ window.removeEventListener("contextmenu", this);
+ this._elements.records.removeEventListener("change", this);
+ this._elements.records.removeEventListener("click", this);
+ this._elements.controlsContainer.removeEventListener("click", this);
+ Services.obs.removeObserver(this, "formautofill-storage-changed");
+ }
+}
+
+export class ManageAddresses extends ManageRecords {
+ telemetryType = AutofillTelemetry.ADDRESS;
+
+ constructor(elements) {
+ super("addresses", elements);
+ elements.add.setAttribute(
+ "search-l10n-ids",
+ lazy.FormAutofillUtils.EDIT_ADDRESS_L10N_IDS.join(",")
+ );
+ AutofillTelemetry.recordManageEvent(this.telemetryType, "show");
+ }
+
+ static getAddressL10nStrings() {
+ const l10nIds = [
+ ...lazy.FormAutofillUtils.MANAGE_ADDRESSES_L10N_IDS,
+ ...lazy.FormAutofillUtils.EDIT_ADDRESS_L10N_IDS,
+ ];
+
+ return l10nIds.reduce(
+ (acc, id) => ({
+ ...acc,
+ [id]: lazy.l10n.formatValueSync(id),
+ }),
+ {}
+ );
+ }
+
+ /**
+ * Open the edit address dialog to create/edit an address.
+ *
+ * @param {object} address [optional]
+ */
+ openEditDialog(address) {
+ this.prefWin.gSubDialog.open(EDIT_ADDRESS_URL, undefined, {
+ record: address,
+ // Don't validate in preferences since it's fine for fields to be missing
+ // for autofill purposes. For PaymentRequest addresses get more validation.
+ noValidate: true,
+ l10nStrings: ManageAddresses.getAddressL10nStrings(),
+ });
+ }
+
+ getLabelInfo(address) {
+ return { raw: lazy.FormAutofillUtils.getAddressLabel(address) };
+ }
+}
+
+export class ManageCreditCards extends ManageRecords {
+ telemetryType = AutofillTelemetry.CREDIT_CARD;
+
+ constructor(elements) {
+ super("creditCards", elements);
+ elements.add.setAttribute(
+ "search-l10n-ids",
+ lazy.FormAutofillUtils.EDIT_CREDITCARD_L10N_IDS.join(",")
+ );
+
+ this._isDecrypted = false;
+ AutofillTelemetry.recordManageEvent(this.telemetryType, "show");
+ }
+
+ /**
+ * Open the edit address dialog to create/edit a credit card.
+ *
+ * @param {object} creditCard [optional]
+ */
+ async openEditDialog(creditCard) {
+ // Ask for reauth if user is trying to edit an existing credit card.
+ if (creditCard) {
+ const promptMessage = lazy.FormAutofillUtils.reauthOSPromptMessage(
+ "autofill-edit-payment-method-os-prompt-macos",
+ "autofill-edit-payment-method-os-prompt-windows",
+ "autofill-edit-payment-method-os-prompt-other"
+ );
+
+ const verified = await lazy.FormAutofillUtils.verifyUserOSAuth(
+ FormAutofill.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ promptMessage
+ );
+ if (!verified) {
+ return;
+ }
+ }
+ let decryptedCCNumObj = {};
+ if (creditCard && creditCard["cc-number-encrypted"]) {
+ try {
+ decryptedCCNumObj["cc-number"] = await lazy.OSKeyStore.decrypt(
+ creditCard["cc-number-encrypted"]
+ );
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_ABORT) {
+ // User shouldn't be ask to reauth here, but it could happen.
+ // Return here and skip opening the dialog.
+ return;
+ }
+ // We've got ourselves a real error.
+ // Recover from encryption error so the user gets a chance to re-enter
+ // unencrypted credit card number.
+ decryptedCCNumObj["cc-number"] = "";
+ console.error(ex);
+ }
+ }
+ let decryptedCreditCard = Object.assign({}, creditCard, decryptedCCNumObj);
+ this.prefWin.gSubDialog.open(
+ EDIT_CREDIT_CARD_URL,
+ { features: "resizable=no" },
+ {
+ record: decryptedCreditCard,
+ }
+ );
+ }
+
+ /**
+ * Get credit card display label. It should display masked numbers and the
+ * cardholder's name, separated by a comma.
+ *
+ * @param {object} creditCard
+ * @returns {Promise<string>}
+ */
+ async getLabelInfo(creditCard) {
+ // The card type is displayed visually using an image. For a11y, we need
+ // to expose it as text. We do this using aria-label. However,
+ // aria-label overrides the text content, so we must include that also.
+ // Since the text content is generated by Fluent, aria-label must be
+ // generated by Fluent also.
+ const type = creditCard["cc-type"];
+ const typeL10nId = lazy.CreditCard.getNetworkL10nId(type);
+ const typeName = typeL10nId
+ ? await document.l10n.formatValue(typeL10nId)
+ : type ?? ""; // Unknown card type
+ return lazy.CreditCard.getLabelInfo({
+ name: creditCard["cc-name"],
+ number: creditCard["cc-number"],
+ month: creditCard["cc-exp-month"],
+ year: creditCard["cc-exp-year"],
+ type: typeName,
+ });
+ }
+
+ async renderRecordElements(records) {
+ // Revert back to encrypted form when re-rendering happens
+ this._isDecrypted = false;
+ // Display third-party card icons when possible
+ this._elements.records.classList.toggle(
+ "branded",
+ AppConstants.MOZILLA_OFFICIAL
+ );
+ await super.renderRecordElements(records);
+
+ let options = this._elements.records.options;
+ for (let option of options) {
+ let record = option.record;
+ if (record && record["cc-type"]) {
+ option.setAttribute("cc-type", record["cc-type"]);
+ } else {
+ option.removeAttribute("cc-type");
+ }
+ }
+ }
+
+ updateButtonsStates(selectedCount) {
+ super.updateButtonsStates(selectedCount);
+ }
+
+ handleClick(event) {
+ super.handleClick(event);
+ }
+}
diff --git a/browser/extensions/formautofill/skin/shared/editAddress.css b/browser/extensions/formautofill/skin/shared/editAddress.css
index c50024e542..7660fd8e55 100644
--- a/browser/extensions/formautofill/skin/shared/editAddress.css
+++ b/browser/extensions/formautofill/skin/shared/editAddress.css
@@ -18,6 +18,15 @@ dialog:not([subdialog]) .editAddressForm {
margin-top: var(--grid-column-row-gap) !important;
margin-inline: calc(var(--grid-column-row-gap) / 2);
flex-grow: 1;
+
+ &.new-line {
+ flex: 0 1 100%;
+ }
+
+ input, textarea, select {
+ width: 100%;
+ margin: 0;
+ }
}
#country-container {
@@ -29,12 +38,6 @@ dialog:not([subdialog]) .editAddressForm {
max-width: calc(50% - var(--grid-column-row-gap));
}
-#name-container,
-#street-address-container {
- /* Name and street address are always full-width */
- flex: 0 1 100%;
-}
-
#street-address {
resize: vertical;
}
diff --git a/browser/extensions/formautofill/test/browser/address/browser.toml b/browser/extensions/formautofill/test/browser/address/browser.toml
index 8b7f1ec760..e8c72ae1b1 100644
--- a/browser/extensions/formautofill/test/browser/address/browser.toml
+++ b/browser/extensions/formautofill/test/browser/address/browser.toml
@@ -38,6 +38,8 @@ support-files = [
["browser_address_doorhanger_ui.js"]
+["browser_address_doorhanger_ui_lines.js"]
+
["browser_address_doorhanger_unsupported_region.js"]
["browser_address_telemetry.js"]
diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_ui_lines.js b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_ui_lines.js
new file mode 100644
index 0000000000..01e888a5f8
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_ui_lines.js
@@ -0,0 +1,32 @@
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.addresses.capture.enabled", true],
+ ["extensions.formautofill.addresses.supported", "on"],
+ ],
+ });
+});
+
+add_task(
+ async function test_address_line_displays_normalized_state_in_save_doorhanger() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: ADDRESS_FORM_URL },
+ async function (browser) {
+ await showAddressDoorhanger(browser, {
+ "#address-level1": "Nova Scotia",
+ "#address-level2": "Somerset",
+ "#country": "CA",
+ });
+
+ const p = getNotification().querySelector(
+ `.address-save-update-row-container p:first-child`
+ );
+ is(p.textContent, "Somerset, NS");
+
+ await clickAddressDoorhangerButton(SECONDARY_BUTTON);
+ }
+ );
+ }
+);
diff --git a/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js b/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js
index 62797739fc..dec367d8e9 100644
--- a/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js
+++ b/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js
@@ -1,7 +1,7 @@
"use strict";
-const { FormAutofillUtils } = ChromeUtils.importESModule(
- "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+const { FormAutofill } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofill.sys.mjs"
);
ChromeUtils.defineESModuleGetters(this, {
@@ -53,7 +53,12 @@ add_task(async function test_defaultCountry() {
Region._setHomeRegion("XX", false);
await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
let doc = win.document;
- is(doc.querySelector("#country").value, "", "Default country set to empty");
+ const countries = [...FormAutofill.countries.keys()];
+ is(
+ countries[0],
+ doc.querySelector("#country").value,
+ "Default country set to first option in the list"
+ );
doc.querySelector("#cancel").click();
});
Region._setHomeRegion("US", false);
@@ -250,10 +255,9 @@ add_task(async function test_saveAddressCA() {
"Postal Code",
"CA postal-code label should be 'Postal Code'"
);
- is(
- doc.querySelector("#address-level3-container").style.display,
- "none",
- "CA address-level3 should be hidden"
+ ok(
+ !doc.querySelector("#address-level3-container"),
+ "CA address-level3 should not be rendered"
);
// Input address info and verify move through form with tab keys
@@ -313,15 +317,13 @@ add_task(async function test_saveAddressDE() {
"Postal Code",
"DE postal-code label should be 'Postal Code'"
);
- is(
- doc.querySelector("#address-level1-container").style.display,
- "none",
- "DE address-level1 should be hidden"
+ ok(
+ !doc.querySelector("#address-level1-container"),
+ "DE address-level1 should not be rendered"
);
- is(
- doc.querySelector("#address-level3-container").style.display,
- "none",
- "DE address-level3 should be hidden"
+ ok(
+ !doc.querySelector("#address-level3-container"),
+ "DE address-level3 should not be rendered"
);
// Input address info and verify move through form with tab keys
doc.querySelector("#name").focus();
@@ -434,57 +436,28 @@ add_task(async function test_saveAddressIE() {
add_task(async function test_countryAndStateFieldLabels() {
await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
- let doc = win.document;
- // Change country to verify labels
- doc.querySelector("#country").focus();
-
- let mutableLabels = [
- "postal-code-container",
- "address-level1-container",
- "address-level2-container",
- "address-level3-container",
- ].map(containerID =>
- doc.getElementById(containerID).querySelector(":scope > .label-text")
- );
-
+ const doc = win.document;
for (let countryOption of doc.querySelector("#country").options) {
- if (countryOption.value == "") {
- info("Skipping the empty country option");
- continue;
- }
-
// Clear L10N textContent to not leave leftovers between country tests
- for (let labelEl of mutableLabels) {
+ for (const labelEl of doc.querySelectorAll(".label-text")) {
doc.l10n.setAttributes(labelEl, "");
labelEl.textContent = "";
}
+ // Change country to verify labels
+ doc.querySelector("#country").focus();
+
info(`Selecting '${countryOption.label}' (${countryOption.value})`);
EventUtils.synthesizeKey(countryOption.label, {}, win);
- let l10nResolve;
- let l10nReady = new Promise(resolve => {
- l10nResolve = resolve;
- });
- let verifyL10n = () => {
- if (mutableLabels.every(labelEl => labelEl.textContent)) {
- win.removeEventListener("MozAfterPaint", verifyL10n);
- l10nResolve();
- }
- };
- win.addEventListener("MozAfterPaint", verifyL10n);
- await l10nReady;
-
- // Check that the labels were filled
- for (let labelEl of mutableLabels) {
- isnot(
- labelEl.textContent,
- "",
- "Ensure textContent is non-empty for: " + countryOption.value
- );
- }
+ await waitForFocusAndFormReady(win);
+
+ const allLabelsHaveText = [...doc.querySelectorAll(".label-text")].every(
+ labelEl => labelEl.textContent
+ );
+
+ ok(allLabelsHaveText, "All labels are rendered and have text content");
- let stateOptions = doc.querySelector("#address-level1").options;
/* eslint-disable max-len */
let expectedStateOptions = {
BS: {
@@ -510,22 +483,22 @@ add_task(async function test_countryAndStateFieldLabels() {
/* eslint-enable max-len */
if (expectedStateOptions[countryOption.value]) {
+ const stateOptions = doc.querySelector("#address-level1").options;
let { keys, names } = expectedStateOptions[countryOption.value];
is(
stateOptions.length,
- keys.length + 1,
- "stateOptions should list all options plus a blank entry"
+ keys.length,
+ "stateOptions should have the same length as the expected options"
);
- is(stateOptions[0].value, "", "First State option should be blank");
for (let i = 1; i < stateOptions.length; i++) {
is(
stateOptions[i].value,
- keys[i - 1],
+ keys[i],
"Each State should be listed in alphabetical name order (key)"
);
is(
stateOptions[i].text,
- names[i - 1],
+ names[i],
"Each State should be listed in alphabetical name order (name)"
);
}
@@ -539,14 +512,15 @@ add_task(async function test_countryAndStateFieldLabels() {
});
add_task(async function test_hiddenFieldNotSaved() {
- await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
- let doc = win.document;
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
+ const doc = win.document;
doc.querySelector("#address-level2").focus();
EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level2"], {}, win);
doc.querySelector("#address-level1").focus();
EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level1"], {}, win);
doc.querySelector("#country").focus();
EventUtils.synthesizeKey("Germany", {}, win);
+ await waitForFocusAndFormReady(win);
doc.querySelector("#save").focus();
EventUtils.synthesizeKey("VK_RETURN", {}, win);
});
@@ -598,10 +572,11 @@ add_task(async function test_hiddenFieldRemovedWhenCountryChanged() {
await testDialog(
EDIT_ADDRESS_DIALOG_URL,
- win => {
- let doc = win.document;
+ async win => {
+ const doc = win.document;
doc.querySelector("#country").focus();
EventUtils.synthesizeKey("Germany", {}, win);
+ await waitForFocusAndFormReady(win);
win.document.querySelector("#save").click();
},
{
@@ -628,63 +603,33 @@ add_task(async function test_hiddenFieldRemovedWhenCountryChanged() {
add_task(async function test_countrySpecificFieldsGetRequiredness() {
Region._setHomeRegion("RO", false);
await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
- let doc = win.document;
+ const doc = win.document;
is(
doc.querySelector("#country").value,
"RO",
"Default country set to Romania"
);
let provinceField = doc.getElementById("address-level1");
- ok(
- !provinceField.required,
- "address-level1 should not be marked as required"
- );
- ok(provinceField.disabled, "address-level1 should be marked as disabled");
- is(
- provinceField.parentNode.style.display,
- "none",
- "address-level1 is hidden for Romania"
- );
+ ok(!provinceField, "address-level1 should not be rendered");
doc.querySelector("#country").focus();
EventUtils.synthesizeKey("United States", {}, win);
+ await waitForFocusAndFormReady(win);
+ const stateField = doc.getElementById("address-level1");
- await TestUtils.waitForCondition(
- () => {
- provinceField = doc.getElementById("address-level1");
- return provinceField.parentNode.style.display != "none";
- },
- "Wait for address-level1 to become visible",
- 10
- );
-
- ok(provinceField.required, "address-level1 should be marked as required");
- ok(
- !provinceField.disabled,
- "address-level1 should not be marked as disabled"
- );
+ ok(stateField.required, "address-level1 should be marked as required");
+ ok(!stateField.disabled, "address-level1 should not be marked as disabled");
// Dispatch a dummy key event so that <select>'s incremental search is cleared.
EventUtils.synthesizeKey("VK_ACCEPT", {}, win);
-
doc.querySelector("#country").focus();
EventUtils.synthesizeKey("Romania", {}, win);
- await TestUtils.waitForCondition(
- () => {
- provinceField = doc.getElementById("address-level1");
- return provinceField.parentNode.style.display == "none";
- },
- "Wait for address-level1 to become hidden",
- 10
- );
-
+ await waitForFocusAndFormReady(win);
ok(
- provinceField.required,
- "address-level1 will still be marked as required"
+ !doc.getElementById("address-level1"),
+ "address-level1 is not rendered "
);
- ok(provinceField.disabled, "address-level1 should be marked as disabled");
-
doc.querySelector("#cancel").click();
});
});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser.toml b/browser/extensions/formautofill/test/browser/creditCard/browser.toml
index 580ce936d4..ead527488e 100644
--- a/browser/extensions/formautofill/test/browser/creditCard/browser.toml
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser.toml
@@ -1,7 +1,6 @@
[DEFAULT]
prefs = [
"extensions.formautofill.creditCards.enabled=true",
- "extensions.formautofill.reauth.enabled=true",
"toolkit.telemetry.ipcBatchTimeout=0", # lower the interval for event telemetry in the content process to update the parent process
]
support-files = [
@@ -41,11 +40,13 @@ skip-if = [
]
["browser_creditCard_doorhanger_display.js"]
-skip-if = [
- "apple_catalina && !debug", # perma-fail see Bug 1655601
- "apple_silicon && !debug", # perma-fail see Bug 1655601
- "win11_2009 && ccov", # Bug 1655600
-]
+skip-if = ["true"] # Bug 1895422
+# Bug 1895422 - Fix this test for linux then uncomment.
+# skip-if = [
+# "apple_catalina && !debug", # perma-fail see Bug 1655601
+# "apple_silicon && !debug", # perma-fail see Bug 1655601
+# "win11_2009 && ccov", # Bug 1655600
+# ]
["browser_creditCard_doorhanger_fields.js"]
skip-if = [
@@ -102,6 +103,9 @@ skip-if = ["apple_silicon && !debug"] # Bug 1714221
["browser_creditCard_heuristics_cc_type.js"]
skip-if = ["apple_silicon && !debug"] # Bug 1714221
+["browser_creditCard_osAuth.js"]
+skip-if = ["os == 'linux'"]
+
["browser_creditCard_submission_autodetect_type.js"]
skip-if = ["apple_silicon && !debug"]
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js b/browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js
index f7fc731e54..0398c6242d 100644
--- a/browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js
@@ -16,6 +16,26 @@ add_task(async function setup_storage() {
);
});
+async function disableOSAuthForThisTest() {
+ // Revert head.js change that mocks os auth
+ sinon.restore();
+
+ let oldValue = FormAutofillUtils.getOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
+ );
+ FormAutofillUtils.setOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ false
+ );
+
+ registerCleanupFunction(() => {
+ FormAutofillUtils.setOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ oldValue
+ );
+ });
+}
+
add_task(async function test_active_delay() {
// This is a workaround for the fact that we don't have a way
// to know when the popup was opened exactly and this makes our test
@@ -26,11 +46,11 @@ add_task(async function test_active_delay() {
// gets opened and listen for it in this test before we check if the item
// is disabled.
await SpecialPowers.pushPrefEnv({
- set: [
- ["security.notification_enable_delay", 1000],
- ["extensions.formautofill.reauth.enabled", false],
- ],
+ set: [["security.notification_enable_delay", 1000]],
});
+
+ await disableOSAuthForThisTest();
+
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CC_URL },
async function (browser) {
@@ -86,10 +106,7 @@ add_task(async function test_active_delay() {
add_task(async function test_no_delay() {
await SpecialPowers.pushPrefEnv({
- set: [
- ["security.notification_enable_delay", 1000],
- ["extensions.formautofill.reauth.enabled", false],
- ],
+ set: [["security.notification_enable_delay", 1000]],
});
await BrowserTestUtils.withNewTab(
{ gBrowser, url: ADDRESS_URL },
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js
index 82122925d7..b5a8019f0d 100644
--- a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js
@@ -132,12 +132,14 @@ add_task(async function test_update_doorhanger_click_save() {
await setStorage(TEST_CREDIT_CARD_1);
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
+ let osKeyStoreLoginShown = null;
let onChanged = waitForStorageChangedEvents("add");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_URL },
async function (browser) {
- let osKeyStoreLoginShown =
- OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
let onPopupShown = waitForPopupShown();
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
@@ -153,7 +155,10 @@ add_task(async function test_update_doorhanger_click_save() {
await onPopupShown;
await clickDoorhangerButton(SECONDARY_BUTTON);
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ ok(osKeyStoreLoginShown, "OS re-auth promise Complete");
+ }
}
);
await onChanged;
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js
index 715eceb3eb..8db32d9462 100644
--- a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js
@@ -107,16 +107,20 @@ add_task(async function test_doorhanger_not_shown_when_autofill_untouched() {
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
+ let osKeyStoreLoginShown = null;
let onUsed = waitForStorageChangedEvents("notifyUsed");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_URL },
async function (browser) {
- let osKeyStoreLoginShown =
- OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ }
await waitForAutofill(browser, "#cc-name", "John Doe");
await SpecialPowers.spawn(browser, [], async function () {
@@ -186,12 +190,15 @@ add_task(
await setStorage(TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2);
let creditCards = await getCreditCards();
is(creditCards.length, 2, "2 credit card in storage");
+ let osKeyStoreLoginShown = null;
let onUsed = waitForStorageChangedEvents("notifyUsed");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_URL },
async function (browser) {
- let osKeyStoreLoginShown =
- OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown =
+ OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await openPopupOn(browser, "form #cc-number");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
@@ -214,7 +221,9 @@ add_task(
await sleep(1000);
is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ }
}
);
await onUsed;
@@ -242,12 +251,15 @@ add_task(
let creditCards = await getCreditCards();
is(creditCards.length, 2, "2 credit card in storage");
+ let osKeyStoreLoginShown = null;
let onUsed = waitForStorageChangedEvents("notifyUsed");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_URL },
async function (browser) {
- let osKeyStoreLoginShown =
- OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown =
+ OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await openPopupOn(browser, "form #cc-number");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
@@ -267,7 +279,9 @@ add_task(
await sleep(1000);
is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ }
}
);
await onUsed;
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js
index c1ebef737e..7ba8bfab91 100644
--- a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js
@@ -13,18 +13,23 @@ add_task(async function test_update_autofill_name_field() {
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
+ let osKeyStoreLoginShown = null;
let onChanged = waitForStorageChangedEvents("update", "notifyUsed");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_URL },
async function (browser) {
- let osKeyStoreLoginShown =
- OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
let onPopupShown = waitForPopupShown();
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ ok(osKeyStoreLoginShown, "OS Auth Dialog shown and authenticated");
+ }
await waitForAutofill(browser, "#cc-name", "John Doe");
await focusUpdateSubmitForm(browser, {
@@ -63,17 +68,22 @@ add_task(async function test_update_autofill_exp_date_field() {
await setStorage(TEST_CREDIT_CARD_1);
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
+ let osKeyStoreLoginShown = null;
let onChanged = waitForStorageChangedEvents("update", "notifyUsed");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_URL },
async function (browser) {
- let osKeyStoreLoginShown =
- OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
let onPopupShown = waitForPopupShown();
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ ok(osKeyStoreLoginShown, "OS Auth Dialog shown and authenticated");
+ }
await waitForAutofill(browser, "#cc-name", "John Doe");
await focusUpdateSubmitForm(browser, {
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js
index 2781e5acf6..774c3d7b25 100644
--- a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js
@@ -30,8 +30,10 @@ add_task(async function test_iframe_submit_untouched_creditCard_form() {
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_IFRAME_URL },
async function (browser) {
- let osKeyStoreLoginShown =
- OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let osKeyStoreLoginShown = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
let iframeBC = browser.browsingContext.children[0];
await openPopupOnSubframe(browser, iframeBC, "form #cc-name");
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_osAuth.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_osAuth.js
new file mode 100644
index 0000000000..0fe6e1e07c
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_osAuth.js
@@ -0,0 +1,200 @@
+"use strict";
+
+const PAGE_PREFS = "about:preferences";
+const PAGE_PRIVACY = PAGE_PREFS + "#privacy";
+const SELECTORS = {
+ savedCreditCardsBtn: "#creditCardAutofill button",
+ reauthCheckbox: "#creditCardReauthenticate checkbox",
+};
+
+// On mac, this test times out in chaos mode
+requestLongerTimeout(2);
+
+add_setup(async function () {
+ // Revert head.js change that mocks os auth
+ sinon.restore();
+
+ // Load in a few credit cards
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.reduceTimerPrecision", false]],
+ });
+ await setStorage(TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2);
+});
+
+add_task(async function test_os_auth_enabled_with_checkbox() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved("sync-pane-loaded");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+
+ await SpecialPowers.spawn(
+ browser,
+ [SELECTORS, AppConstants.NIGHTLY_BUILD],
+ async (selectors, isNightly) => {
+ is(
+ content.document.querySelector(selectors.reauthCheckbox).checked,
+ isNightly,
+ "OSReauth for credit cards should be checked"
+ );
+ }
+ );
+ is(
+ FormAutofillUtils.getOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
+ ),
+ AppConstants.NIGHTLY_BUILD,
+ "OSAuth should be enabled."
+ );
+ }
+ );
+});
+
+add_task(async function test_os_auth_disabled_with_checkbox() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved("sync-pane-loaded");
+ FormAutofillUtils.setOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ false
+ );
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+
+ await SpecialPowers.spawn(browser, [SELECTORS], async selectors => {
+ is(
+ content.document.querySelector(selectors.reauthCheckbox).checked,
+ false,
+ "OSReauth for credit cards should be unchecked"
+ );
+ });
+ is(
+ FormAutofillUtils.getOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
+ ),
+ false,
+ "OSAuth should be disabled"
+ );
+ }
+ );
+ FormAutofillUtils.setOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ true
+ );
+});
+
+add_task(async function test_OSAuth_enabled_with_random_value_in_pref() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved("sync-pane-loaded");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF, "poutine-gravy"],
+ ],
+ });
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ await SpecialPowers.spawn(browser, [SELECTORS], async selectors => {
+ let reauthCheckbox = content.document.querySelector(
+ selectors.reauthCheckbox
+ );
+ is(
+ reauthCheckbox.checked,
+ true,
+ "OSReauth for credit cards should be checked"
+ );
+ });
+ is(
+ FormAutofillUtils.getOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
+ ),
+ true,
+ "OSAuth should be enabled since the pref does not decrypt to 'opt out'."
+ );
+ }
+ );
+});
+
+add_task(async function test_osAuth_enabled_behaviour() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved("sync-pane-loaded");
+ await SpecialPowers.pushPrefEnv({
+ set: [[FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF, ""]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ // The rest of the test uses Edit mode which causes an OS prompt in official builds.
+ return;
+ }
+ let reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await SpecialPowers.spawn(browser, [SELECTORS], async selectors => {
+ content.document.querySelector(selectors.savedCreditCardsBtn).click();
+ });
+ let ccManageDialog = await waitForSubDialogLoad(
+ content,
+ MANAGE_CREDIT_CARDS_DIALOG_URL
+ );
+ await SpecialPowers.spawn(ccManageDialog, [], async () => {
+ let selRecords = content.document.getElementById("credit-cards");
+ await EventUtils.synthesizeMouseAtCenter(
+ selRecords.children[0],
+ [],
+ content
+ );
+ content.document.querySelector("#edit").click();
+ });
+ await reauthObserved; // If the OS does not popup, this will cause a timeout in the test.
+ await waitForSubDialogLoad(content, EDIT_CREDIT_CARD_DIALOG_URL);
+ }
+ );
+});
+
+add_task(async function test_osAuth_disabled_behavior() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved("sync-pane-loaded");
+ FormAutofillUtils.setOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ false
+ );
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ await SpecialPowers.spawn(
+ browser,
+ [SELECTORS.savedCreditCardsBtn, SELECTORS.reauthCheckbox],
+ async (saveButton, reauthCheckbox) => {
+ is(
+ content.document.querySelector(reauthCheckbox).checked,
+ false,
+ "OSReauth for credit cards should NOT be checked"
+ );
+ content.document.querySelector(saveButton).click();
+ }
+ );
+ let ccManageDialog = await waitForSubDialogLoad(
+ content,
+ MANAGE_CREDIT_CARDS_DIALOG_URL
+ );
+ await SpecialPowers.spawn(ccManageDialog, [], async () => {
+ let selRecords = content.document.getElementById("credit-cards");
+ await EventUtils.synthesizeMouseAtCenter(
+ selRecords.children[0],
+ [],
+ content
+ );
+ content.document.getElementById("edit").click();
+ });
+ info("The OS Auth dialog should NOT show up");
+ // If OSAuth prompt shows up, the next line would cause a timeout since the edit dialog would not show up.
+ await waitForSubDialogLoad(content, EDIT_CREDIT_CARD_DIALOG_URL);
+ }
+ );
+ FormAutofillUtils.setOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ true
+ );
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js
index 7a4bff1e45..ea455df12a 100644
--- a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js
@@ -154,12 +154,15 @@ async function openTabAndUseCreditCard(
creditCard,
{ closeTab = true, submitForm = true } = {}
) {
- let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let osKeyStoreLoginShown = null;
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
CREDITCARD_FORM_URL
);
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
let browser = tab.linkedBrowser;
await openPopupOn(browser, "form #cc-name");
@@ -167,7 +170,9 @@ async function openTabAndUseCreditCard(
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
}
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ }
await waitForAutofill(browser, "#cc-number", creditCard["cc-number"]);
await focusUpdateSubmitForm(
browser,
@@ -692,10 +697,14 @@ add_task(async function test_submit_creditCard_update() {
let creditCards = await getCreditCards();
Assert.equal(creditCards.length, 1, "1 credit card in storage");
- let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let osKeyStoreLoginShown = null;
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_URL },
async function (browser) {
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown =
+ OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
let onPopupShown = waitForPopupShown();
let onChanged;
if (expectChanged !== undefined) {
@@ -705,7 +714,9 @@ add_task(async function test_submit_creditCard_update() {
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ }
await waitForAutofill(browser, "#cc-name", "John Doe");
await focusUpdateSubmitForm(browser, {
diff --git a/browser/extensions/formautofill/test/browser/head.js b/browser/extensions/formautofill/test/browser/head.js
index 3f87f7b5ef..d82ed5076e 100644
--- a/browser/extensions/formautofill/test/browser/head.js
+++ b/browser/extensions/formautofill/test/browser/head.js
@@ -1,5 +1,9 @@
"use strict";
+const { ManageAddresses } = ChromeUtils.importESModule(
+ "chrome://formautofill/content/manageDialog.mjs"
+);
+
const { OSKeyStore } = ChromeUtils.importESModule(
"resource://gre/modules/OSKeyStore.sys.mjs"
);
@@ -20,6 +24,27 @@ const { FormAutofillNameUtils } = ChromeUtils.importESModule(
"resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs"
);
+const { FormAutofillUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+);
+
+let { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+// Always pretend OS Auth is enabled in this dir.
+if (
+ gTestPath.includes("browser/creditCard") &&
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin() &&
+ OSKeyStore.canReauth()
+) {
+ info("Stubbing out getOSAuthEnabled so it always returns true");
+ sinon.stub(FormAutofillUtils, "getOSAuthEnabled").returns(true);
+ registerCleanupFunction(() => {
+ sinon.restore();
+ });
+}
+
const MANAGE_ADDRESSES_DIALOG_URL =
"chrome://formautofill/content/manageAddresses.xhtml";
const MANAGE_CREDIT_CARDS_DIALOG_URL =
@@ -822,7 +847,7 @@ async function removeAllRecords() {
async function waitForFocusAndFormReady(win) {
return Promise.all([
new Promise(resolve => waitForFocus(resolve, win)),
- BrowserTestUtils.waitForEvent(win, "FormReady"),
+ BrowserTestUtils.waitForEvent(win, "FormReadyForTests"),
]);
}
@@ -855,9 +880,12 @@ async function testDialog(url, testFn, arg = undefined) {
"cc-number": await OSKeyStore.decrypt(arg.record["cc-number-encrypted"]),
});
}
- let win = window.openDialog(url, null, "width=600,height=600", arg);
+ const win = window.openDialog(url, null, "width=600,height=600", {
+ ...arg,
+ l10nStrings: ManageAddresses.getAddressL10nStrings(),
+ });
await waitForFocusAndFormReady(win);
- let unloadPromise = BrowserTestUtils.waitForEvent(win, "unload");
+ const unloadPromise = BrowserTestUtils.waitForEvent(win, "unload");
await testFn(win);
return unloadPromise;
}
diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/mochitest.toml b/browser/extensions/formautofill/test/mochitest/creditCard/mochitest.toml
index ffd504bb45..0d6ea02569 100644
--- a/browser/extensions/formautofill/test/mochitest/creditCard/mochitest.toml
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/mochitest.toml
@@ -2,7 +2,6 @@
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",
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
index 717d40946f..9fad869629 100644
--- 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
@@ -197,10 +197,15 @@ add_task(async function check_fields_after_form_autofill() {
})));
synthesizeKey("KEY_ArrowDown");
- let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ let osKeyStoreLoginShown = Promise.resolve();
+ if(OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ }
await new Promise(resolve => SimpleTest.executeSoon(resolve));
await triggerAutofillAndCheckProfile(MOCK_STORAGE[1].cc);
await osKeyStoreLoginShown;
+ // Enforcing this since it is unable to change back in chaos mode.
+ SpecialPowers.clearUserPref("toolkit.osKeyStore.unofficialBuildOnlyLogin");
});
// Fallback to history search after autofill values (for non-empty fields).
diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html
index a1a3322c4e..4803151aae 100644
--- a/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html
@@ -124,6 +124,11 @@ add_task(async function simple_clear() {
await triggerPopupAndHoverItem("#tel", 0);
await confirmClear("#tel");
await checkIsFormCleared();
+
+ // Ensure the correctness of the autocomplete popup after the form is cleared
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ is(4, getMenuEntries().length, `Checking length of expected menu`);
});
add_task(async function clear_adapted_record() {
@@ -154,7 +159,10 @@ add_task(async function clear_distinct_section() {
document.getElementById("form1").reset();
await triggerPopupAndHoverItem("#cc-name", 0);
- let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ let osKeyStoreLoginShown = Promise.resolve();
+ if(OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ }
await triggerAutofillAndCheckProfile(MOCK_CC_STORAGE_EXPECTED_FILL[0]);
await osKeyStoreLoginShown;
@@ -175,6 +183,8 @@ add_task(async function clear_distinct_section() {
await triggerPopupAndHoverItem("#cc-name", 0);
await confirmClear("#cc-name");
await checkIsFormCleared();
+ // Enforcing this since it is unable to change back in chaos mode.
+ SpecialPowers.clearUserPref("toolkit.osKeyStore.unofficialBuildOnlyLogin");
});
</script>
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
index 6ebef3bba1..f054bc5871 100644
--- 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
@@ -127,7 +127,10 @@ add_task(async function clear_distinct_section() {
todo(false, "Cannot test OS key store login on official builds.");
return;
}
- let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ let osKeyStoreLoginShown = Promise.resolve();
+ if(OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ }
await triggerPopupAndHoverItem("#cc-name", 0);
await triggerAutofillAndCheckProfile(MOCK_CC_STORAGE[0]);
await osKeyStoreLoginShown;
@@ -147,6 +150,8 @@ add_task(async function clear_distinct_section() {
"cc-exp-month": "MM",
"cc-exp-year": "YY"
});
+ // Enforcing this since it is unable to change back in chaos mode.
+ SpecialPowers.clearUserPref("toolkit.osKeyStore.unofficialBuildOnlyLogin");
});
</script>
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
index a6d0572ac6..6b317f2392 100644
--- 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
@@ -143,7 +143,11 @@ add_task(async function check_filled_highlight() {
return;
}
await triggerPopupAndHoverItem("#cc-name", 0);
- let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ let osKeyStoreLoginShown = Promise.resolve();
+
+if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+}
// filled 1st credit card option
synthesizeKey("KEY_Enter");
await osKeyStoreLoginShown;
@@ -151,6 +155,8 @@ add_task(async function check_filled_highlight() {
let profile = MOCK_STORAGE_EXPECTED_FILL[0];
await setupListeners(elements, profile);
await checkMultipleCCNumberFormStyle(profile, false);
+ // Enforcing this since it is unable to change back in chaos mode.
+ SpecialPowers.clearUserPref("toolkit.osKeyStore.unofficialBuildOnlyLogin");
});
</script>
<p id="display"></p>
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
index 090eb9290e..5517153f1a 100644
--- 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
@@ -87,11 +87,17 @@ add_task(async function check_filled_highlight() {
return;
}
await triggerPopupAndHoverItem("#cc-number", 0);
- let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+
+ let osKeyStoreLoginShown = Promise.resolve();
+ if(OSKeyStore.canReauth()) {
+ 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);
+ // Enforcing this since it is unable to change back in chaos mode.
+ SpecialPowers.clearUserPref("toolkit.osKeyStore.unofficialBuildOnlyLogin");
});
</script>
<p id="display"></p>
diff --git a/browser/extensions/formautofill/test/mochitest/formautofill_common.js b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
index 0e371ba3af..dab2d58b4a 100644
--- a/browser/extensions/formautofill/test/mochitest/formautofill_common.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
@@ -2,6 +2,10 @@
/* 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";
@@ -14,6 +18,10 @@ 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));
@@ -353,7 +361,21 @@ async function canTestOSKeyStoreLogin() {
}
async function waitForOSKeyStoreLogin(login = false) {
- await invokeAsyncChromeTask("FormAutofillTest:OSKeyStoreLogin", { login });
+ // Need to fetch this from the parent in order for it to be correct.
+ let isOSAuthEnabled = 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
+ );
+ });
+ if (isOSAuthEnabled) {
+ await invokeAsyncChromeTask("FormAutofillTest:OSKeyStoreLogin", { login });
+ }
}
function patchRecordCCNumber(record) {
diff --git a/browser/extensions/pictureinpicture/data/picture_in_picture_overrides.js b/browser/extensions/pictureinpicture/data/picture_in_picture_overrides.js
index ecc4945135..e8b690a386 100644
--- a/browser/extensions/pictureinpicture/data/picture_in_picture_overrides.js
+++ b/browser/extensions/pictureinpicture/data/picture_in_picture_overrides.js
@@ -57,7 +57,7 @@ let AVAILABLE_PIP_OVERRIDES;
aol: {
"https://*.aol.com/*": {
- videoWrapperScriptPath: "video-wrappers/yahoo.js",
+ videoWrapperScriptPath: "video-wrappers/videojsWrapper.js",
},
},
@@ -81,12 +81,41 @@ let AVAILABLE_PIP_OVERRIDES;
videoWrapperScriptPath: "video-wrappers/videojsWrapper.js",
},
},
+
+ canalplus: {
+ "https://*.canalplus.com/live/*": {
+ videoWrapperScriptPath: "video-wrappers/canalplus.js",
+ disabledKeyboardControls: KEYBOARD_CONTROLS.LIVE_SEEK,
+ },
+ "https://*.canalplus.com/*": {
+ videoWrapperScriptPath: "video-wrappers/canalplus.js",
+ },
+ },
+
cbc: {
"https://*.cbc.ca/*": {
videoWrapperScriptPath: "video-wrappers/cbc.js",
},
},
+ cnbc: {
+ "https://*.cnbc.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
+ cpac: {
+ "https://*.cpac.ca/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
+ cspan: {
+ "https://*.c-span.org/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
dailymotion: {
"https://*.dailymotion.com/*": {
videoWrapperScriptPath: "video-wrappers/dailymotion.js",
@@ -105,6 +134,18 @@ let AVAILABLE_PIP_OVERRIDES;
},
},
+ fandom: {
+ "https://*.fandom.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
+ fastcompany: {
+ "https://*.fastcompany.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
frontendMasters: {
"https://*.frontendmasters.com/*": {
videoWrapperScriptPath: "video-wrappers/videojsWrapper.js",
@@ -117,6 +158,12 @@ let AVAILABLE_PIP_OVERRIDES;
},
},
+ fuse: {
+ "https://*.fuse.tv/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
hbomax: {
"https://play.hbomax.com/page/*": { policy: TOGGLE_POLICIES.HIDDEN },
"https://play.hbomax.com/player/*": {
@@ -136,10 +183,34 @@ let AVAILABLE_PIP_OVERRIDES;
},
},
+ imdb: {
+ "https://*.imdb.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
+ indpendentuk: {
+ "https://*.independent.co.uk/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
+ indy100: {
+ "https://*.indy100.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
instagram: {
"https://www.instagram.com/*": { policy: TOGGLE_POLICIES.ONE_QUARTER },
},
+ internetArchive: {
+ "https://*.archive.org/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
laracasts: {
"https://*.laracasts.com/*": { policy: TOGGLE_POLICIES.ONE_QUARTER },
},
@@ -149,12 +220,31 @@ let AVAILABLE_PIP_OVERRIDES;
visibilityThreshold: 0.7,
},
},
+
+ msnbc: {
+ "https://*.msnbc.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
mxplayer: {
"https://*.mxplayer.in/*": {
videoWrapperScriptPath: "video-wrappers/videojsWrapper.js",
},
},
+ nbcnews: {
+ "https://*.nbcnews.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
+ nbcUniversal: {
+ "https://*.nbcuni.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
nebula: {
"https://*.nebula.app/*": {
videoWrapperScriptPath: "video-wrappers/videojsWrapper.js",
@@ -197,6 +287,17 @@ let AVAILABLE_PIP_OVERRIDES;
},
},
+ primeVideo: {
+ "https://*.primevideo.com/*": {
+ visibilityThreshold: 0.9,
+ videoWrapperScriptPath: "video-wrappers/primeVideo.js",
+ },
+ "https://*.amazon.com/*": {
+ visibilityThreshold: 0.9,
+ videoWrapperScriptPath: "video-wrappers/primeVideo.js",
+ },
+ },
+
radiocanada: {
"https://*.ici.radio-canada.ca/*": {
videoWrapperScriptPath: "video-wrappers/radiocanada.js",
@@ -207,18 +308,46 @@ let AVAILABLE_PIP_OVERRIDES;
"https://*.reddit.com/*": { policy: TOGGLE_POLICIES.ONE_QUARTER },
},
+ reuters: {
+ "https://*.reuters.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
sonyliv: {
"https://*.sonyliv.com/*": {
videoWrapperScriptPath: "video-wrappers/sonyliv.js",
},
},
+ syfy: {
+ "https://*.syfy.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
ted: {
"https://*.ted.com/*": {
showHiddenTextTracks: true,
},
},
+ time: {
+ "https://*.time.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
+ timvision: {
+ "https://*.timvision.it/TV/*": {
+ videoWrapperScriptPath: "video-wrappers/canalplus.js",
+ disabledKeyboardControls: KEYBOARD_CONTROLS.LIVE_SEEK,
+ },
+ "https://*.timvision.it/*": {
+ videoWrapperScriptPath: "video-wrappers/canalplus.js",
+ },
+ },
+
tubi: {
"https://*.tubitv.com/live*": {
videoWrapperScriptPath: "video-wrappers/tubilive.js",
@@ -256,6 +385,12 @@ let AVAILABLE_PIP_OVERRIDES;
},
},
+ univision: {
+ "https://*.univision.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
viki: {
"https://*.viki.com/*": {
videoWrapperScriptPath: "video-wrappers/videojsWrapper.js",
@@ -274,9 +409,9 @@ let AVAILABLE_PIP_OVERRIDES;
},
},
- yahoofinance: {
- "https://*.finance.yahoo.com/*": {
- videoWrapperScriptPath: "video-wrappers/yahoo.js",
+ yahoo: {
+ "https://*.s.yimg.com/*": {
+ videoWrapperScriptPath: "video-wrappers/videojsWrapper.js",
},
},
@@ -301,16 +436,5 @@ let AVAILABLE_PIP_OVERRIDES;
videoWrapperScriptPath: "video-wrappers/washingtonpost.js",
},
},
-
- primeVideo: {
- "https://*.primevideo.com/*": {
- visibilityThreshold: 0.9,
- videoWrapperScriptPath: "video-wrappers/primeVideo.js",
- },
- "https://*.amazon.com/*": {
- visibilityThreshold: 0.9,
- videoWrapperScriptPath: "video-wrappers/primeVideo.js",
- },
- },
};
}
diff --git a/browser/extensions/pictureinpicture/moz.build b/browser/extensions/pictureinpicture/moz.build
index 7cc77f9594..fbdefbeb1c 100644
--- a/browser/extensions/pictureinpicture/moz.build
+++ b/browser/extensions/pictureinpicture/moz.build
@@ -31,6 +31,7 @@ FINAL_TARGET_FILES.features["pictureinpicture@mozilla.org"]["video-wrappers"] +=
"video-wrappers/airmozilla.js",
"video-wrappers/arte.js",
"video-wrappers/bbc.js",
+ "video-wrappers/canalplus.js",
"video-wrappers/cbc.js",
"video-wrappers/dailymotion.js",
"video-wrappers/disneyplus.js",
@@ -38,6 +39,7 @@ FINAL_TARGET_FILES.features["pictureinpicture@mozilla.org"]["video-wrappers"] +=
"video-wrappers/hbomax.js",
"video-wrappers/hotstar.js",
"video-wrappers/hulu.js",
+ "video-wrappers/jwplayerWrapper.js",
"video-wrappers/mock-wrapper.js",
"video-wrappers/netflix.js",
"video-wrappers/nytimes.js",
@@ -52,7 +54,6 @@ FINAL_TARGET_FILES.features["pictureinpicture@mozilla.org"]["video-wrappers"] +=
"video-wrappers/videojsWrapper.js",
"video-wrappers/voot.js",
"video-wrappers/washingtonpost.js",
- "video-wrappers/yahoo.js",
"video-wrappers/youtube.js",
]
diff --git a/browser/extensions/pictureinpicture/video-wrappers/canalplus.js b/browser/extensions/pictureinpicture/video-wrappers/canalplus.js
new file mode 100644
index 0000000000..3d725ef54a
--- /dev/null
+++ b/browser/extensions/pictureinpicture/video-wrappers/canalplus.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+class PictureInPictureVideoWrapper {
+ isLive() {
+ let documentURI = document.documentURI;
+ return documentURI.includes("/live/") || documentURI.includes("/TV/");
+ }
+
+ getDuration(video) {
+ if (this.isLive(video)) {
+ return Infinity;
+ }
+ return video.duration;
+ }
+
+ setCaptionContainerObserver(video, updateCaptionsFunction) {
+ let container =
+ document.querySelector(`[data-testid="playerRoot"]`) ||
+ document.querySelector(`[player-root="true"]`);
+
+ if (container) {
+ updateCaptionsFunction("");
+ const callback = function (mutationsList) {
+ // eslint-disable-next-line no-unused-vars
+ for (const mutation of mutationsList) {
+ let text = container.querySelector(
+ ".rxp-texttrack-region"
+ )?.innerText;
+ if (!text) {
+ updateCaptionsFunction("");
+ return;
+ }
+
+ updateCaptionsFunction(text);
+ }
+ };
+
+ // immediately invoke the callback function to add subtitles to the PiP window
+ callback([1], null);
+
+ let captionsObserver = new MutationObserver(callback);
+
+ captionsObserver.observe(container, {
+ attributes: false,
+ childList: true,
+ subtree: true,
+ });
+ }
+ }
+}
+
+this.PictureInPictureVideoWrapper = PictureInPictureVideoWrapper;
diff --git a/browser/extensions/pictureinpicture/video-wrappers/jwplayerWrapper.js b/browser/extensions/pictureinpicture/video-wrappers/jwplayerWrapper.js
new file mode 100644
index 0000000000..37591c16f8
--- /dev/null
+++ b/browser/extensions/pictureinpicture/video-wrappers/jwplayerWrapper.js
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// This wrapper supports multiple sites that use JWPlayer
+class PictureInPictureVideoWrapper {
+ setCaptionContainerObserver(video, updateCaptionsFunction) {
+ let container = document.querySelector(".jw-captions");
+
+ if (container) {
+ updateCaptionsFunction("");
+
+ const callback = function () {
+ let text = container.innerText;
+ if (!text) {
+ updateCaptionsFunction("");
+ return;
+ }
+
+ updateCaptionsFunction(text);
+ };
+
+ // immediately invoke the callback function to add subtitles to the PiP window
+ callback();
+
+ let captionsObserver = new MutationObserver(callback);
+
+ captionsObserver.observe(container, {
+ attributes: false,
+ childList: true,
+ subtree: true,
+ });
+ }
+ }
+}
+
+this.PictureInPictureVideoWrapper = PictureInPictureVideoWrapper;
diff --git a/browser/extensions/pictureinpicture/video-wrappers/yahoo.js b/browser/extensions/pictureinpicture/video-wrappers/yahoo.js
deleted file mode 100644
index 1dd932bc37..0000000000
--- a/browser/extensions/pictureinpicture/video-wrappers/yahoo.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-class PictureInPictureVideoWrapper {
- setCaptionContainerObserver(video, updateCaptionsFunction) {
- let container = document.querySelector(".vp-main");
-
- if (container) {
- updateCaptionsFunction("");
- const callback = function () {
- let text = container.querySelector(".vp-cc-element.vp-show")?.innerText;
-
- if (!text) {
- updateCaptionsFunction("");
- return;
- }
-
- updateCaptionsFunction(text);
- };
-
- // immediately invoke the callback function to add subtitles to the PiP window
- callback([1], null);
-
- let captionsObserver = new MutationObserver(callback);
-
- captionsObserver.observe(container, {
- attributes: false,
- childList: true,
- subtree: true,
- });
- }
- }
-}
-
-this.PictureInPictureVideoWrapper = PictureInPictureVideoWrapper;