commit 7db2b87c5f82f50bdccced4123ca6c8e9c45f7ce
parent 2e2bb17d71f8de9b1dc59fc93899e368b065ab4a
Author: Krist Baliev <kbaliev@mozilla.com>
Date: Mon, 24 Nov 2025 13:12:42 +0000
Bug 1962815 - Correct the address level 1 autofill by using full region name instead of ISO codes by normalizing it in the fill process. r=dimi,NeilDeakin
Differential Revision: https://phabricator.services.mozilla.com/D269198
Diffstat:
4 files changed, 152 insertions(+), 3 deletions(-)
diff --git a/browser/extensions/formautofill/test/browser/browser_autofill_address_textarea.js b/browser/extensions/formautofill/test/browser/browser_autofill_address_textarea.js
@@ -3,6 +3,17 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
+const FormAutofillSharedUtils = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+);
+
+function getFullSubregionName(abbreviated, country) {
+ return FormAutofillSharedUtils.FormAutofillUtils.getFullSubregionName(
+ abbreviated,
+ country
+ );
+}
+
const TEST_PROFILE_ADDRESS = {
"given-name": "John",
"additional-name": "R.",
@@ -105,7 +116,10 @@ add_autofill_heuristic_tests([
},
{
fieldName: "address-level1",
- autofill: TEST_PROFILE_ADDRESS["address-level1"],
+ autofill: getFullSubregionName(
+ TEST_PROFILE_ADDRESS["address-level1"],
+ TEST_PROFILE_ADDRESS.country
+ ),
},
{
fieldName: "postal-code",
@@ -256,4 +270,37 @@ add_autofill_heuristic_tests([
},
],
},
+ {
+ description:
+ "(address)maxLength Guard should prevent conversion if full subregion name exceeds element's maxLength.",
+ // Fixture must contain 3 fields to meet the minimum section threshold.
+ fixtureData: `<form>
+ <label>address-level1: <input id="address-level1" autocomplete="address-level1" maxlength="2" autofocus></label>
+ <label>address-level2: <input id="address-level2" autocomplete="address-level2"></label>
+ <label>postal-code: <input id="postal-code" autocomplete="postal-code"></label>
+ </form>`,
+ profile: TEST_PROFILE_ADDRESS,
+ expectedResult: [
+ {
+ default: {
+ reason: "autocomplete",
+ },
+ fields: [
+ {
+ fieldName: "address-level1",
+ // Expecting transformer to bail out, using original abbreviated value.
+ autofill: TEST_PROFILE_ADDRESS["address-level1"],
+ },
+ {
+ fieldName: "address-level2",
+ autofill: TEST_PROFILE_ADDRESS["address-level2"],
+ },
+ {
+ fieldName: "postal-code",
+ autofill: TEST_PROFILE_ADDRESS["postal-code"],
+ },
+ ],
+ },
+ ],
+ },
]);
diff --git a/browser/extensions/formautofill/test/browser/browser_dynamic_form_autocompletion.js b/browser/extensions/formautofill/test/browser/browser_dynamic_form_autocompletion.js
@@ -7,6 +7,16 @@ const { FormAutofill } = ChromeUtils.importESModule(
"resource://autofill/FormAutofill.sys.mjs"
);
+const FormAutofillSharedUtils = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+);
+const getFullSubregionName = (abbreviated, country) => {
+ return FormAutofillSharedUtils.FormAutofillUtils.getFullSubregionName(
+ abbreviated,
+ country
+ );
+};
+
add_setup(async () => {
await SpecialPowers.pushPrefEnv({
set: [
@@ -51,7 +61,13 @@ const expectedFilledAddressFields = {
fieldName: "street-address",
autofill: TEST_ADDRESS_1["street-address"].replace("\n", " "),
},
- { fieldName: "address-level1", autofill: TEST_ADDRESS_1["address-level1"] },
+ {
+ fieldName: "address-level1",
+ autofill: getFullSubregionName(
+ TEST_ADDRESS_1["address-level1"],
+ TEST_ADDRESS_1.country
+ ),
+ },
{ fieldName: "address-level2", autofill: TEST_ADDRESS_1["address-level2"] },
{ fieldName: "postal-code", autofill: TEST_ADDRESS_1["postal-code"] },
],
@@ -228,7 +244,10 @@ add_task(
},
{
fieldName: "address-level1",
- autofill: TEST_ADDRESS_1["address-level1"],
+ autofill: getFullSubregionName(
+ TEST_ADDRESS_1["address-level1"],
+ TEST_ADDRESS_1.country
+ ),
},
{
fieldName: "address-level2",
diff --git a/toolkit/components/formautofill/shared/FormAutofillHandler.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillHandler.sys.mjs
@@ -1186,9 +1186,45 @@ class ProfileTransformer {
this.#creditCardExpiryDateTransformer();
this.#creditCardExpMonthAndYearTransformer();
this.#creditCardNameTransformer();
+ this.#addressLevelOneTransformer();
this.#adaptFieldMaxLength();
}
+ /**
+ * Replaces an abbreviated address-level1 code (e.g. "B") with the full
+ * region name (e.g. "Buenos Aires") if the target field is a text input.
+ */
+ #addressLevelOneTransformer() {
+ const fieldName = "address-level1";
+ const fieldDetail = this.getFieldDetailByName(fieldName);
+ if (!fieldDetail || !FormAutofillUtils.isTextControl(fieldDetail.element)) {
+ return;
+ }
+
+ const element = fieldDetail.element;
+ const abbreviatedValue = this.getField(fieldName);
+ const country = this.getField("country");
+
+ const fullSubregionName = FormAutofillUtils.getFullSubregionName(
+ abbreviatedValue,
+ country
+ );
+
+ if (!fullSubregionName || fullSubregionName === abbreviatedValue) {
+ return;
+ }
+
+ // No point in using full subregion name if allowed string length is too small.
+ if (
+ element.maxLength !== -1 &&
+ fullSubregionName.length > element.maxLength
+ ) {
+ return;
+ }
+
+ this.setField(fieldName, fullSubregionName);
+ }
+
// This function mostly uses getUpdatedField as it relies on the modified
// values of fields from the previous functions.
#adaptFieldMaxLength() {
diff --git a/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs
@@ -854,6 +854,53 @@ FormAutofillUtils = {
},
/**
+ * Attempts to find the full sub-region name from an abbreviated / ISO code,
+ * using the address metadata for the specified country.
+ *
+ * @param {string} abbreviatedValue A short sub-region code (e.g. "B").
+ * @param {string} country The country code (e.g. "AR").
+ * @returns {string|null} The full sub-region name (e.g. "Buenos Aires") or null.
+ */
+ getFullSubregionName(abbreviatedValue, country) {
+ if (!abbreviatedValue || !country) {
+ return null;
+ }
+
+ const collators = this.getSearchCollators(country);
+ for (const metadata of this.getCountryAddressDataWithLocales(country)) {
+ const {
+ sub_keys: subKeys,
+ sub_names: subNames,
+ sub_lnames: subLnames,
+ sub_isoids: subIsoids,
+ } = metadata;
+ if (!subKeys) {
+ continue;
+ }
+
+ // Use latin names if available, otherwise use native names.
+ const targetNames = subLnames || subNames || subKeys;
+
+ // Check if the abbreviatedValue matches an ISO ID (e.g. "B") or a key (which may also be an ISO ID).
+ let matchIndex = subKeys.findIndex(key =>
+ this.strCompare(abbreviatedValue, key, collators)
+ );
+
+ if (matchIndex === -1 && subIsoids) {
+ matchIndex = subIsoids.findIndex(isoid =>
+ this.strCompare(abbreviatedValue, isoid, collators)
+ );
+ }
+
+ if (matchIndex !== -1 && targetNames.length > matchIndex) {
+ // Return the full or latin name from the targetNames list at the matching index.
+ return targetNames[matchIndex];
+ }
+ }
+ return null;
+ },
+
+ /**
* Find the option element from select element.
* 1. Try to find the locale using the country from address.
* 2. First pass try to find exact match.