mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-11 04:15:43 +00:00
Bug 1418884 - [Form Autofill] Make getAbbreviatedSubregionName/findOption supports more locales. r=lchang,scottwu
MozReview-Commit-ID: HD8xFNHJwDR --HG-- extra : rebase_source : 944ea41bb9470c3707b0bdc5e053bd433a060cbe
This commit is contained in:
parent
627d9acca4
commit
a8b7199f62
@ -49,6 +49,7 @@ let AddressDataLoader = {
|
||||
country: false,
|
||||
level1: new Set(),
|
||||
},
|
||||
|
||||
/**
|
||||
* Load address data and extension script into a sandbox from different paths.
|
||||
* @param {string} path
|
||||
@ -77,11 +78,38 @@ let AddressDataLoader = {
|
||||
}
|
||||
return sandbox;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert certain properties' string value into array. We should make sure
|
||||
* the cached data is parsed.
|
||||
* @param {object} data Original metadata from addressReferences.
|
||||
* @returns {object} parsed metadata with property value that converts to array.
|
||||
*/
|
||||
_parse(data) {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const properties = ["languages", "sub_keys", "sub_names", "sub_lnames"];
|
||||
for (let key of properties) {
|
||||
if (!data[key]) {
|
||||
continue;
|
||||
}
|
||||
// No need to normalize data if the value is array already.
|
||||
if (Array.isArray(data[key])) {
|
||||
return data;
|
||||
}
|
||||
|
||||
data[key] = data[key].split("~");
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* We'll cache addressData in the loader once the data loaded from scripts.
|
||||
* It'll become the example below after loading addressReferences with extension:
|
||||
* addressData: {
|
||||
"data/US": {"lang": "en", ...// Data defined in libaddressinput metadata
|
||||
* "data/US": {"lang": ["en"], ...// Data defined in libaddressinput metadata
|
||||
* "alternative_names": ... // Data defined in extension }
|
||||
* "data/CA": {} // Other supported country metadata
|
||||
* "data/TW": {} // Other supported country metadata
|
||||
@ -89,16 +117,16 @@ let AddressDataLoader = {
|
||||
* }
|
||||
* @param {string} country
|
||||
* @param {string} level1
|
||||
* @returns {object}
|
||||
* @returns {object} Default locale metadata
|
||||
*/
|
||||
getData(country, level1 = null) {
|
||||
_loadData(country, level1 = null) {
|
||||
// Load the addressData if needed
|
||||
if (!this._dataLoaded.country) {
|
||||
this._addressData = this._loadScripts(ADDRESS_METADATA_PATH).addressData;
|
||||
this._dataLoaded.country = true;
|
||||
}
|
||||
if (!level1) {
|
||||
return this._addressData[`data/${country}`];
|
||||
return this._parse(this._addressData[`data/${country}`]);
|
||||
}
|
||||
// If level1 is set, load addressReferences under country folder with specific
|
||||
// country/level 1 for level 2 information.
|
||||
@ -107,7 +135,30 @@ let AddressDataLoader = {
|
||||
this._loadScripts(`${ADDRESS_METADATA_PATH}${country}/`).addressData);
|
||||
this._dataLoaded.level1.add(country);
|
||||
}
|
||||
return this._addressData[`data/${country}/${level1}`];
|
||||
return this._parse(this._addressData[`data/${country}/${level1}`]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the region metadata with default locale and other locales (if exists).
|
||||
* @param {string} country
|
||||
* @param {string} level1
|
||||
* @returns {object} Return default locale and other locales metadata.
|
||||
*/
|
||||
getData(country, level1) {
|
||||
let defaultLocale = this._loadData(country, level1);
|
||||
if (!defaultLocale) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let countryData = this._parse(this._addressData[`data/${country}`]);
|
||||
let locales = [];
|
||||
// TODO: Should be able to support multi-locale level 1/ level 2 metadata query
|
||||
// in Bug 1421886
|
||||
if (countryData.languages) {
|
||||
let list = countryData.languages.filter(key => key !== countryData.lang);
|
||||
locales = list.map(key => this._parse(this._addressData[`${defaultLocale.id}--${key}`]));
|
||||
}
|
||||
return {defaultLocale, locales};
|
||||
},
|
||||
};
|
||||
|
||||
@ -289,16 +340,18 @@ this.FormAutofillUtils = {
|
||||
|
||||
/**
|
||||
* Get country address data and fallback to US if not found.
|
||||
* See AddressDataLoader.getData for more details of addressData structure.
|
||||
* See AddressDataLoader._loadData for more details of addressData structure.
|
||||
* @param {string} [country=FormAutofillUtils.DEFAULT_REGION]
|
||||
* The country code for requesting specific country's metadata. It'll be
|
||||
* default region if parameter is not set.
|
||||
* @param {string} [level1=null]
|
||||
* Retrun address level 1/level 2 metadata if parameter is set.
|
||||
* @returns {object}
|
||||
* Return the metadata of specific region.
|
||||
* @returns {object|null}
|
||||
* Return metadata of specific region with default locale and other supported
|
||||
* locales. We need to return a deafult country metadata for layout format
|
||||
* and collator, but for sub-region metadata we'll just return null if not found.
|
||||
*/
|
||||
getCountryAddressData(country = FormAutofillUtils.DEFAULT_REGION, level1 = null) {
|
||||
getCountryAddressRawData(country = FormAutofillUtils.DEFAULT_REGION, level1 = null) {
|
||||
let metadata = AddressDataLoader.getData(country, level1);
|
||||
if (!metadata) {
|
||||
if (level1) {
|
||||
@ -310,8 +363,35 @@ this.FormAutofillUtils = {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to US if we couldn't get data from default region.
|
||||
return metadata || AddressDataLoader.getData("US");
|
||||
// TODO: Now we fallback to US if we couldn't get data from default region,
|
||||
// but it could be removed in bug 1423464 if it's not necessary.
|
||||
if (!metadata) {
|
||||
metadata = AddressDataLoader.getData("US");
|
||||
}
|
||||
return metadata;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get country address data with default locale.
|
||||
* @param {string} country
|
||||
* @param {string} level1
|
||||
* @returns {object|null} Return metadata of specific region with default locale.
|
||||
*/
|
||||
getCountryAddressData(country, level1) {
|
||||
let metadata = this.getCountryAddressRawData(country, level1);
|
||||
return metadata && metadata.defaultLocale;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get country address data with all locales.
|
||||
* @param {string} country
|
||||
* @param {string} level1
|
||||
* @returns {array<object>|null}
|
||||
* Return metadata of specific region with all the locales.
|
||||
*/
|
||||
getCountryAddressDataWithLocales(country, level1) {
|
||||
let metadata = this.getCountryAddressRawData(country, level1);
|
||||
return metadata && [metadata.defaultLocale, ...metadata.locales];
|
||||
},
|
||||
|
||||
/**
|
||||
@ -327,7 +407,7 @@ this.FormAutofillUtils = {
|
||||
|
||||
if (!this._collators[country]) {
|
||||
let dataset = this.getCountryAddressData(country);
|
||||
let languages = dataset.languages ? dataset.languages.split("~") : [dataset.lang];
|
||||
let languages = dataset.languages || [dataset.lang];
|
||||
this._collators[country] = languages.map(lang => new Intl.Collator(lang, {sensitivity: "base", ignorePunctuation: true}));
|
||||
}
|
||||
return this._collators[country];
|
||||
@ -438,33 +518,33 @@ this.FormAutofillUtils = {
|
||||
let values = Array.isArray(subregionValues) ? subregionValues : [subregionValues];
|
||||
|
||||
let collators = this.getCollators(country);
|
||||
let {sub_keys: subKeys, sub_names: subNames} = this.getCountryAddressData(country);
|
||||
for (let metadata of this.getCountryAddressDataWithLocales(country)) {
|
||||
let {sub_keys: subKeys, sub_names: subNames, sub_lnames: subLnames} = metadata;
|
||||
// Apply sub_lnames if sub_names does not exist
|
||||
subNames = subNames || subLnames;
|
||||
|
||||
if (!Array.isArray(subKeys)) {
|
||||
subKeys = subKeys.split("~");
|
||||
}
|
||||
if (!Array.isArray(subNames)) {
|
||||
subNames = subNames.split("~");
|
||||
}
|
||||
let speculatedSubIndexes = [];
|
||||
for (const val of values) {
|
||||
let identifiedValue = this.identifyValue(subKeys, subNames, val, collators);
|
||||
if (identifiedValue) {
|
||||
return identifiedValue;
|
||||
}
|
||||
|
||||
let speculatedSubIndexes = [];
|
||||
for (const val of values) {
|
||||
let identifiedValue = this.identifyValue(subKeys, subNames, val, collators);
|
||||
if (identifiedValue) {
|
||||
return identifiedValue;
|
||||
// Predict the possible state by partial-matching if no exact match.
|
||||
[subKeys, subNames].forEach(sub => {
|
||||
speculatedSubIndexes.push(sub.findIndex(token => {
|
||||
let pattern = new RegExp("\\b" + this.escapeRegExp(token) + "\\b");
|
||||
|
||||
return pattern.test(val);
|
||||
}));
|
||||
});
|
||||
}
|
||||
let subKey = subKeys[speculatedSubIndexes.find(i => !!~i)];
|
||||
if (subKey) {
|
||||
return subKey;
|
||||
}
|
||||
|
||||
// Predict the possible state by partial-matching if no exact match.
|
||||
[subKeys, subNames].forEach(sub => {
|
||||
speculatedSubIndexes.push(sub.findIndex(token => {
|
||||
let pattern = new RegExp("\\b" + this.escapeRegExp(token) + "\\b");
|
||||
|
||||
return pattern.test(val);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
return subKeys[speculatedSubIndexes.find(i => !!~i)] || null;
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -484,7 +564,6 @@ this.FormAutofillUtils = {
|
||||
return null;
|
||||
}
|
||||
|
||||
let dataset = this.getCountryAddressData(address.country);
|
||||
let collators = this.getCollators(address.country);
|
||||
|
||||
for (let option of selectEl.options) {
|
||||
@ -496,29 +575,26 @@ this.FormAutofillUtils = {
|
||||
|
||||
switch (fieldName) {
|
||||
case "address-level1": {
|
||||
if (!Array.isArray(dataset.sub_keys)) {
|
||||
dataset.sub_keys = dataset.sub_keys.split("~");
|
||||
}
|
||||
if (!Array.isArray(dataset.sub_names)) {
|
||||
dataset.sub_names = dataset.sub_names.split("~");
|
||||
}
|
||||
let keys = dataset.sub_keys;
|
||||
let names = dataset.sub_names;
|
||||
let identifiedValue = this.identifyValue(keys, names, value, collators);
|
||||
|
||||
// No point going any further if we cannot identify value from address
|
||||
let {country} = address;
|
||||
let identifiedValue = this.getAbbreviatedSubregionName([value], country);
|
||||
// No point going any further if we cannot identify value from address level 1
|
||||
if (!identifiedValue) {
|
||||
return null;
|
||||
}
|
||||
for (let dataset of this.getCountryAddressDataWithLocales(country)) {
|
||||
let keys = dataset.sub_keys;
|
||||
// Apply sub_lnames if sub_names does not exist
|
||||
let names = dataset.sub_names || dataset.sub_lnames;
|
||||
|
||||
// Go through options one by one to find a match.
|
||||
// Also check if any option contain the address-level1 key.
|
||||
let pattern = new RegExp("\\b" + this.escapeRegExp(identifiedValue) + "\\b", "i");
|
||||
for (let option of selectEl.options) {
|
||||
let optionValue = this.identifyValue(keys, names, option.value, collators);
|
||||
let optionText = this.identifyValue(keys, names, option.text, collators);
|
||||
if (identifiedValue === optionValue || identifiedValue === optionText || pattern.test(option.value)) {
|
||||
return option;
|
||||
// Go through options one by one to find a match.
|
||||
// Also check if any option contain the address-level1 key.
|
||||
let pattern = new RegExp("\\b" + this.escapeRegExp(identifiedValue) + "\\b", "i");
|
||||
for (let option of selectEl.options) {
|
||||
let optionValue = this.identifyValue(keys, names, option.value, collators);
|
||||
let optionText = this.identifyValue(keys, names, option.text, collators);
|
||||
if (identifiedValue === optionValue || identifiedValue === optionText || pattern.test(option.value)) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -16,5 +16,6 @@ scheme=https
|
||||
scheme=https
|
||||
[test_form_changes.html]
|
||||
[test_formautofill_preview_highlight.html]
|
||||
[test_multi_locale_CA_address_form.html]
|
||||
[test_multiple_forms.html]
|
||||
[test_on_address_submission.html]
|
||||
|
@ -0,0 +1,201 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test basic autofill</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
|
||||
<script type="text/javascript" src="formautofill_common.js"></script>
|
||||
<script type="text/javascript" src="satchel_common.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
Form autofill test: simple form address autofill
|
||||
|
||||
<script>
|
||||
/* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/SpawnTask.js */
|
||||
/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
|
||||
/* import-globals-from formautofill_common.js */
|
||||
|
||||
"use strict";
|
||||
|
||||
let MOCK_STORAGE = [{
|
||||
organization: "Mozilla Vancouver",
|
||||
"street-address": "163 W Hastings St.\n#209\n3-line",
|
||||
tel: "+17787851540",
|
||||
country: "CA",
|
||||
"address-level1": "BC",
|
||||
}, {
|
||||
organization: "Mozilla Toronto",
|
||||
"street-address": "366 Adelaide St.\nW Suite 500\n3-line",
|
||||
tel: "+14168483114",
|
||||
country: "CA",
|
||||
"address-level1": "ON",
|
||||
}, {
|
||||
organization: "Prince of Wales Northern Heritage",
|
||||
"street-address": "4750 48 St.\nYellowknife\n3-line",
|
||||
tel: "+18677679347",
|
||||
country: "CA",
|
||||
"address-level1": "Northwest Territories",
|
||||
}, {
|
||||
organization: "ExpoCité",
|
||||
"street-address": "250 Boulevard Wilfrid-Hamel\nVille de Québec\n3-line",
|
||||
tel: "+14186917110",
|
||||
country: "CA",
|
||||
"address-level1": "Québec",
|
||||
}];
|
||||
|
||||
function checkElementFilled(element, expectedvalue) {
|
||||
return [
|
||||
new Promise(resolve => {
|
||||
element.addEventListener("input", function onInput() {
|
||||
ok(true, "Checking " + element.name + " field fires input event");
|
||||
resolve();
|
||||
}, {once: true});
|
||||
}),
|
||||
new Promise(resolve => {
|
||||
element.addEventListener("change", function onChange() {
|
||||
ok(true, "Checking " + element.name + " field fires change event");
|
||||
is(element.value, expectedvalue, "Checking " + element.name + " field");
|
||||
resolve();
|
||||
}, {once: true});
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function checkAutoCompleteInputFilled(element, expectedvalue) {
|
||||
return new Promise(resolve => {
|
||||
element.addEventListener("DOMAutoComplete", function onChange() {
|
||||
is(element.value, expectedvalue, "Checking " + element.name + " field");
|
||||
resolve();
|
||||
}, {once: true});
|
||||
});
|
||||
}
|
||||
|
||||
function checkFormFilled(selector, address) {
|
||||
info("expecting form filled");
|
||||
let promises = [];
|
||||
let form = document.querySelector(selector);
|
||||
for (let prop in address) {
|
||||
let element = form.querySelector(`[name=${prop}]`);
|
||||
if (document.activeElement == element) {
|
||||
promises.push(checkAutoCompleteInputFilled(element, address[prop]));
|
||||
} else {
|
||||
let converted = address[prop];
|
||||
if (prop == "street-address") {
|
||||
converted = FormAutofillUtils.toOneLineAddress(converted);
|
||||
}
|
||||
promises.push(...checkElementFilled(element, converted));
|
||||
}
|
||||
}
|
||||
doKey("return");
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
async function setupAddressStorage() {
|
||||
for (let address of MOCK_STORAGE) {
|
||||
await addAddress(address);
|
||||
}
|
||||
}
|
||||
|
||||
initPopupListener();
|
||||
|
||||
// Autofill the address with address level 1 code.
|
||||
add_task(async function autofill_with_level1_code() {
|
||||
await setupAddressStorage();
|
||||
|
||||
await setInput("#organization-en", "Mozilla Toronto");
|
||||
doKey("down");
|
||||
await expectPopup();
|
||||
|
||||
doKey("down");
|
||||
// Replace address level 1 code with full name in English for test result
|
||||
let result = Object.assign({}, MOCK_STORAGE[1], {"address-level1": "Ontario"});
|
||||
await checkFormFilled("#form-en", result);
|
||||
|
||||
await setInput("#organization-fr", "Mozilla Vancouver");
|
||||
doKey("down");
|
||||
await expectPopup();
|
||||
|
||||
doKey("down");
|
||||
// Replace address level 1 code with full name in French for test result
|
||||
result = Object.assign({}, MOCK_STORAGE[0], {"address-level1": "Colombie-Britannique"});
|
||||
await checkFormFilled("#form-fr", result);
|
||||
document.querySelector("#form-en").reset();
|
||||
document.querySelector("#form-fr").reset();
|
||||
});
|
||||
|
||||
// Autofill the address with address level 1 full name.
|
||||
add_task(async function autofill_with_level1_full_name() {
|
||||
await setInput("#organization-en", "ExpoCité");
|
||||
doKey("down");
|
||||
await expectPopup();
|
||||
|
||||
doKey("down");
|
||||
// Replace address level 1 code with full name in French for test result
|
||||
let result = Object.assign({}, MOCK_STORAGE[3], {"address-level1": "Quebec"});
|
||||
await checkFormFilled("#form-en", result);
|
||||
|
||||
await setInput("#organization-fr", "Prince of Wales Northern Heritage");
|
||||
doKey("down");
|
||||
await expectPopup();
|
||||
|
||||
doKey("down");
|
||||
// Replace address level 1 code with full name in English for test result
|
||||
result = Object.assign({}, MOCK_STORAGE[2], {"address-level1": "Territoires du Nord-Ouest"});
|
||||
await checkFormFilled("#form-fr", result);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<p id="display"></p>
|
||||
|
||||
<div id="content">
|
||||
|
||||
<form id="form-en">
|
||||
<p>This is a basic CA form with en address level 1 select.</p>
|
||||
<p><label>organization: <input id="organization-en" name="organization" autocomplete="organization" type="text"></label></p>
|
||||
<p><label>streetAddress: <input id="street-address-en" name="street-address" autocomplete="street-address" type="text"></label></p>
|
||||
<p><label>address-line1: <input id="address-line1-en" name="address-line1" autocomplete="address-line1" type="text"></label></p>
|
||||
<p><label>tel: <input id="tel-en" name="tel" autocomplete="tel" type="text"></label></p>
|
||||
<p><label>email: <input id="email-en" name="email" autocomplete="email" type="text"></label></p>
|
||||
<p><label>country: <select id="country-en" name="country" autocomplete="country">
|
||||
<option/>
|
||||
<option value="US">United States</option>
|
||||
<option value="CA">Canada</option>
|
||||
</select></label></p>
|
||||
<p><label>states: <select id="address-level1-en" name="address-level1" autocomplete="address-level1">
|
||||
<option/>
|
||||
<option value="British Columbia">British Columbia</option>
|
||||
<option value="Ontario">Ontario</option>
|
||||
<option value="Northwest Territories">Northwest Territories</option>
|
||||
<option value="Quebec">Quebec</option>
|
||||
</select></label></p>
|
||||
</form>
|
||||
|
||||
<form id="form-fr">
|
||||
<p>This is a basic CA form with fr address level 1 select.</p>
|
||||
<p><label>organization: <input id="organization-fr" name="organization" autocomplete="organization" type="text"></label></p>
|
||||
<p><label>streetAddress: <input id="street-address-fr" name="street-address" autocomplete="street-address" type="text"></label></p>
|
||||
<p><label>address-line1: <input id="address-line1-fr" name="address-line1" autocomplete="address-line1" type="text"></label></p>
|
||||
<p><label>tel: <input id="tel-fr" name="tel" autocomplete="tel" type="text"></label></p>
|
||||
<p><label>email: <input id="email-fr" name="email" autocomplete="email" type="text"></label></p>
|
||||
<p><label>country: <select id="country-fr" name="country" autocomplete="country">
|
||||
<option/>
|
||||
<option value="US">United States</option>
|
||||
<option value="CA">Canada</option>
|
||||
</select></label></p>
|
||||
<p><label>states: <select id="address-level1-fr" name="address-level1" autocomplete="address-level1">
|
||||
<option/>
|
||||
<option value="Colombie-Britannique">Colombie-Britannique</option>
|
||||
<option value="Ontario">Ontario</option>
|
||||
<option value="Territoires du Nord-Ouest">Territoires du Nord-Ouest</option>
|
||||
<option value="Québec">Québec</option>
|
||||
</select></label></p>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<pre id="test"></pre>
|
||||
</body>
|
||||
</html>
|
@ -69,5 +69,13 @@ SUPPORT_COUNTRIES_TESTCASES.forEach(testcase => {
|
||||
let metadata = FormAutofillUtils.getCountryAddressData(testcase.country);
|
||||
Assert.ok(testcase.properties.every(key => metadata[key]),
|
||||
"These properties should exist: " + testcase.properties);
|
||||
// Verify the multi-locale country
|
||||
if (metadata.languages && metadata.languages.length > 1) {
|
||||
let locales = FormAutofillUtils.getCountryAddressDataWithLocales(testcase.country);
|
||||
Assert.equal(metadata.languages.length, locales.length, "Total supported locales should be matched");
|
||||
metadata.languages.forEach((lang, index) => {
|
||||
Assert.equal(lang, locales[index].lang, `Should support ${lang}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user