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:
steveck-chung 2017-11-24 17:04:00 +08:00
parent 627d9acca4
commit a8b7199f62
4 changed files with 341 additions and 55 deletions

View File

@ -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;

View File

@ -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]

View File

@ -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>

View File

@ -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}`);
});
}
});
});