diff --git a/browser/extensions/formautofill/test/unit/head.js b/browser/extensions/formautofill/test/unit/head.js index 3b7b8d2f37fc..9a0f882a2adb 100644 --- a/browser/extensions/formautofill/test/unit/head.js +++ b/browser/extensions/formautofill/test/unit/head.js @@ -190,8 +190,29 @@ async function initProfileStorage( return profileStorage; } +function verifySectionAutofillResult(sections, expectedSectionsInfo) { + sections.forEach((section, index) => { + const expectedSection = expectedSectionsInfo[index]; + + const fieldDetails = section.fieldDetails; + const expectedFieldDetails = expectedSection.fields; + + info(`verify autofill section[${index}]`); + + fieldDetails.forEach((field, fieldIndex) => { + const expeceted = expectedFieldDetails[fieldIndex]; + + Assert.equal( + expeceted.autofill, + field.element.value, + `Autofilled value for element(id=${field.element.id}, field name=${field.fieldName}) should be equal` + ); + }); + }); +} + function verifySectionFieldDetails(sections, expectedSectionsInfo) { - sections.map((section, index) => { + sections.forEach((section, index) => { const expectedSection = expectedSectionsInfo[index]; const fieldDetails = section.fieldDetails; @@ -228,10 +249,11 @@ function verifySectionFieldDetails(sections, expectedSectionsInfo) { ...expectedFieldDetail, }; - delete field.elementWeakRef; - delete field.confidence; - delete field.part; const keys = new Set([...Object.keys(field), ...Object.keys(expected)]); + ["autofill", "elementWeakRef", "confidence", "part"].forEach(k => + keys.delete(k) + ); + for (const key of keys) { const expectedValue = expected[key]; const actualValue = field[key]; @@ -252,46 +274,80 @@ function autofillFieldSelector(doc) { return doc.querySelectorAll("input, select"); } -// The `patterns.expectedResult` array contains test data for different address or credit card sections. -// Each section in the array is represented by an object and can include the following properties: -// - description (optional): A string describing the section, primarily used for debugging purposes. -// - default (optional): An object that sets the default values for the fields within this section. -// The default object contains the same keys as the individual field objects. -// - fields: An array of field objects within the section. -// -// Each field object can have the following keys: -// - fieldName: The name of the field (e.g., "street-name", "cc-name" or "cc-number"). -// - reason: The reason for the field value (e.g., "autocomplete", "regex-heuristic" or "fathom"). -// - section: The section to which the field belongs (e.g., "billing", "shipping"). -// - part: The part of the field. -// - contactType: The contact type of the field. -// - addressType: The address type of the field. -// -// For more information on the field object properties, refer to the FieldDetails class. -// -// Example test data: -// expectedResult: [ -// { -// description: "address form with only two address fields" -// fields: [ -// { fieldName: "organization", reason: "autocomplete" }, -// { fieldName: "street-address", reason: "regex-heuristic" }, -// ] -// }, -// { -// default: { -// reason: "regex-heuristic", -// section: "billing", -// }, -// fields: [ -// { fieldName: "cc-number", reason: "fathom" }, -// { fieldName: "cc-nane" }, -// { fieldName: "cc-exp" }, -// ], -// }, -// ] -// -async function runHeuristicsTest(patterns, fixturePathPrefix) { +/** + * Runs heuristics test for form autofill on given patterns. + * + * @param {Array} patterns - An array of test patterns to run the heuristics test on. + * @param {string} pattern.description - Description of this heuristic test + * @param {string} pattern.fixurePath - The path of the test document + * @param {string} pattern.fixureData - Test document by string. Use either fixurePath or fixtureData. + * @param {object} pattern.profile - The profile to autofill. This is required only when running autofill test + * @param {Array} pattern.expectedResult - The expected result of this heuristic test. See below for detailed explanation + * + * @param {string} [fixturePathPrefix=""] - The prefix to the path of fixture files. + * @param {object} [options={ testAutofill: false }] - An options object containing additional configuration for running the test. + * @param {boolean} [options.testAutofill=false] - A boolean indicating whether to run the test for autofill or not. + * @returns {Promise} A promise that resolves when all the tests are completed. + * + * The `patterns.expectedResult` array contains test data for different address or credit card sections. + * Each section in the array is represented by an object and can include the following properties: + * - description (optional): A string describing the section, primarily used for debugging purposes. + * - default (optional): An object that sets the default values for all the fields within this section. + * The default object contains the same keys as the individual field objects. + * - fields: An array of field details (class FieldDetails) within the section. + * + * Each field object can have the following keys: + * - fieldName: The name of the field (e.g., "street-name", "cc-name" or "cc-number"). + * - reason: The reason for the field value (e.g., "autocomplete", "regex-heuristic" or "fathom"). + * - section: The section to which the field belongs (e.g., "billing", "shipping"). + * - part: The part of the field. + * - contactType: The contact type of the field. + * - addressType: The address type of the field. + * - autofill: Set to true when running autofill test + * + * For more information on the field object properties, refer to the FieldDetails class. + * + * Example test data: + * runHeuristicsTest( + * [{ + * description: "first test pattern", + * fixuturePath: "autocomplete_off.html", + * profile: {organization: "Mozilla", country: "US", tel: "123"}, + * expectedResult: [ + * { + * description: "First section" + * fields: [ + * { fieldName: "organization", reason: "autocomplete", autofill: "Mozilla" }, + * { fieldName: "country", reason: "regex-heuristic", autofill: "US" }, + * { fieldName: "tel", reason: "regex-heuristic", autofill: "123" }, + * ] + * }, + * { + * default: { + * reason: "regex-heuristic", + * section: "billing", + * }, + * fields: [ + * { fieldName: "cc-number", reason: "fathom" }, + * { fieldName: "cc-nane" }, + * { fieldName: "cc-exp" }, + * ], + * }], + * }, + * { + * // second test pattern // + * } + * ], + * "/fixturepath", + * {testAutofill: true} // test options + * ) + */ + +async function runHeuristicsTest( + patterns, + fixturePathPrefix = "", + options = { testAutofill: false } +) { add_setup(async () => { ({ FormAutofillHeuristics } = ChromeUtils.importESModule( "resource://gre/modules/shared/FormAutofillHeuristics.sys.mjs" @@ -348,6 +404,14 @@ async function runHeuristicsTest(patterns, fixturePathPrefix) { verifySectionFieldDetails(sections, testPattern.expectedResult); + if (options.testAutofill) { + for (const section of sections) { + section.focusedInput = section.fieldDetails[0].element; + await section.autofillFields(testPattern.profile); + } + verifySectionAutofillResult(sections, testPattern.expectedResult); + } + if (testPattern.prefs) { testPattern.prefs.forEach(pref => Services.prefs.clearUserPref(pref[0]) @@ -357,6 +421,10 @@ async function runHeuristicsTest(patterns, fixturePathPrefix) { }); } +async function runAutofillHeuristicsTest(patterns, fixturePathPrefix = "") { + runHeuristicsTest(patterns, fixturePathPrefix, { testAutofill: true }); +} + /** * Returns the Sync change counter for a profile storage record. Synced records * store additional metadata for tracking changes and resolving merge conflicts. diff --git a/browser/extensions/formautofill/test/unit/test_autofill_duplicate_fields.js b/browser/extensions/formautofill/test/unit/test_autofill_duplicate_fields.js new file mode 100644 index 000000000000..72165a5e0564 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_autofill_duplicate_fields.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PROFILE = { + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + organization: "Mozilla", + "street-address": "331 E Evelyn Ave", + "address-level2": "Mountain View", + "address-level1": "CA", + "postal-code": "94041", + country: "US", + tel: "+16509030800", + email: "address@mozilla.org", +}; + +runAutofillHeuristicsTest([ + { + fixtureData: ` + + + + + + `, + profile: TEST_PROFILE, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "email", autofill: TEST_PROFILE.email }, + { fieldName: "postal-code", autofill: TEST_PROFILE["postal-code"] }, + { fieldName: "country", autofill: TEST_PROFILE.country }, + ], + }, + ], + }, + { + description: "autofill multiple email fields(2)", + fixtureData: ` + + + + + + + `, + profile: TEST_PROFILE, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "email", autofill: TEST_PROFILE.email }, + { fieldName: "email", autofill: TEST_PROFILE.email }, + { fieldName: "postal-code", autofill: TEST_PROFILE["postal-code"] }, + { fieldName: "country", autofill: TEST_PROFILE.country }, + ], + }, + ], + }, + { + description: "autofill multiple email fields(3)", + fixtureData: ` + + + + + + + + `, + profile: TEST_PROFILE, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "email", autofill: TEST_PROFILE.email }, + { fieldName: "email", autofill: TEST_PROFILE.email }, + { fieldName: "email", autofill: TEST_PROFILE.email }, + { fieldName: "postal-code", autofill: TEST_PROFILE["postal-code"] }, + { fieldName: "country", autofill: TEST_PROFILE.country }, + ], + }, + ], + }, +]); diff --git a/browser/extensions/formautofill/test/unit/xpcshell.ini b/browser/extensions/formautofill/test/unit/xpcshell.ini index fef0d1672183..5f9269ab9f0e 100644 --- a/browser/extensions/formautofill/test/unit/xpcshell.ini +++ b/browser/extensions/formautofill/test/unit/xpcshell.ini @@ -30,6 +30,7 @@ head = head_addressComponent.js [test_addressRecords.js] skip-if = apple_silicon # bug 1729554 +[test_autofill_duplicate_fields.js] [test_autofillFormFields.js] skip-if = tsan # Times out, bug 1612707 diff --git a/toolkit/components/formautofill/shared/FieldScanner.sys.mjs b/toolkit/components/formautofill/shared/FieldScanner.sys.mjs index f8dcb560fc57..3b00fc5ae2fb 100644 --- a/toolkit/components/formautofill/shared/FieldScanner.sys.mjs +++ b/toolkit/components/formautofill/shared/FieldScanner.sys.mjs @@ -110,18 +110,12 @@ export class FieldDetail { } } - get sectionName() { - return this.section || this.addressType; + get element() { + return this.elementWeakRef.get(); } - isSame(other) { - return ( - this.fieldName == other.fieldName && - this.section == other.section && - this.addressType == other.addressType && - !this.part && - !other.part - ); + get sectionName() { + return this.section || this.addressType; } }