--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -109,21 +109,24 @@ AutofillProfileAutoCompleteSearch.protot
this._getAddresses({info, searchString}).then((addresses) => {
if (this.forceStop) {
return;
}
// Sort addresses by timeLastUsed for showing the lastest used address at top.
addresses.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
+ let handler = FormAutofillContent.getFormHandler(focusedInput);
+ let adaptedAddresses = handler.getAdaptedProfiles(addresses);
+
let allFieldNames = FormAutofillContent.getAllFieldNames(focusedInput);
let result = new ProfileAutoCompleteResult(searchString,
info.fieldName,
allFieldNames,
- addresses,
+ adaptedAddresses,
{});
listener.onSearchResult(this, result);
ProfileAutocomplete.setProfileAutoCompleteResult(result);
});
},
/**
@@ -437,18 +440,18 @@ var FormAutofillContent = {
*
*/
getFormDetails(element) {
let formHandler = this.getFormHandler(element);
return formHandler ? formHandler.fieldDetails : null;
},
getAllFieldNames(element) {
- let formDetails = this.getFormDetails(element);
- return formDetails.map(record => record.fieldName);
+ let formHandler = this.getFormHandler(element);
+ return formHandler ? formHandler.allFieldNames : null;
},
identifyAutofillFields(element) {
this.log.debug("identifyAutofillFields:", "" + element.ownerDocument.location);
if (!this.savedFieldNames) {
this.log.debug("identifyAutofillFields: savedFieldNames are not known yet");
Services.cpmm.sendAsyncMessage("FormAutofill:InitStorage");
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -85,22 +85,73 @@ FormAutofillHandler.prototype = {
// is changed from type="hidden" to type="tel".
return this._formFieldCount != this.form.elements.length;
},
/**
* Set fieldDetails from the form about fields that can be autofilled.
*/
collectFormFields() {
+ this._cacheValue.allFieldNames = null;
this._formFieldCount = this.form.elements.length;
let fieldDetails = FormAutofillHeuristics.getFormInfo(this.form);
this.fieldDetails = fieldDetails ? fieldDetails : [];
log.debug("Collected details on", this.fieldDetails.length, "fields");
},
+ getFieldDetailByName(fieldName) {
+ return this.fieldDetails.find(detail => detail.fieldName == fieldName);
+ },
+
+ _cacheValue: {
+ allFieldNames: null,
+ oneLineStreetAddress: null,
+ },
+
+ get allFieldNames() {
+ if (!this._cacheValue.allFieldNames) {
+ this._cacheValue.allFieldNames = this.fieldDetails.map(record => record.fieldName);
+ }
+ return this._cacheValue.allFieldNames;
+ },
+
+ _getOneLineStreetAddress(address) {
+ if (!this._cacheValue.oneLineStreetAddress) {
+ this._cacheValue.oneLineStreetAddress = {};
+ }
+ if (!this._cacheValue.oneLineStreetAddress[address]) {
+ this._cacheValue.oneLineStreetAddress[address] = FormAutofillUtils.toOneLineAddress(address);
+ }
+ return this._cacheValue.oneLineStreetAddress[address];
+ },
+
+ _addressTransformer(profile) {
+ if (profile["street-address"]) {
+ profile["street-address"] = this._getOneLineStreetAddress(profile["street-address"]);
+
+ let waitForConcat = [];
+ for (let f of ["address-line3", "address-line2", "address-line1"]) {
+ waitForConcat.unshift(profile[f]);
+ if (this.getFieldDetailByName(f)) {
+ if (waitForConcat.length > 1) {
+ profile[f] = FormAutofillUtils.toOneLineAddress(waitForConcat);
+ }
+ waitForConcat = [];
+ }
+ }
+ }
+ },
+
+ getAdaptedProfiles(originalProfiles) {
+ for (let profile of originalProfiles) {
+ this._addressTransformer(profile);
+ }
+ return originalProfiles;
+ },
+
/**
* Processes form fields that can be autofilled, and populates them with the
* profile provided by backend.
*
* @param {Object} profile
* A profile to be filled in.
* @param {Object} focusedInput
* A focused input element which is skipped for filling.
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -51,16 +51,34 @@ this.FormAutofillUtils = {
let info = this._fieldNameInfo[fieldName];
if (info) {
categories.add(info);
}
}
return categories;
},
+ getAddressSeparator() {
+ // The separator should be based on the L10N address format, and using a
+ // white space is a temporary solution.
+ return " ";
+ },
+
+ toOneLineAddress(address, delimiter = "\n") {
+ let array = typeof address == "string" ? address.split(delimiter) : address;
+
+ if (!Array.isArray(array)) {
+ return null;
+ }
+ return array
+ .map(s => s.trim())
+ .filter(s => s)
+ .join(this.getAddressSeparator());
+ },
+
defineLazyLogGetter(scope, logPrefix) {
XPCOMUtils.defineLazyGetter(scope, "log", () => {
let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
return new ConsoleAPI({
maxLogLevelPref: "extensions.formautofill.loglevel",
prefix: logPrefix,
});
});
--- a/browser/extensions/formautofill/content/manageProfiles.js
+++ b/browser/extensions/formautofill/content/manageProfiles.js
@@ -147,16 +147,21 @@ ManageProfileDialog.prototype = {
"address-level1", // Province/State (Standardized code if possible)
"country-name", // Country name
"postal-code", // Postal code
"tel", // Phone number
"email", // Email address
];
let parts = [];
+ if (address["street-address"]) {
+ address["street-address"] = FormAutofillUtils.toOneLineAddress(
+ address["street-address"]
+ );
+ }
for (const fieldName of fieldOrder) {
let string = address[fieldName];
if (string) {
parts.push(string);
}
if (parts.length == 2) {
break;
}
--- a/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html
@@ -14,25 +14,27 @@ Form autofill test: simple form address
<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";
+const {FormAutofillUtils} = SpecialPowers.Cu.import("resource://formautofill/FormAutofillUtils.jsm");
+
let MOCK_STORAGE = [{
organization: "Sesame Street",
- "street-address": "123 Sesame Street.",
+ "street-address": "123 Sesame Street.\n2-line\n3-line",
tel: "1-345-345-3456",
country: "US",
"address-level1": "NY",
}, {
organization: "Mozilla",
- "street-address": "331 E. Evelyn Avenue",
+ "street-address": "331 E. Evelyn Avenue\n2-line\n3-line",
tel: "1-650-903-0800",
country: "US",
"address-level1": "CA",
}];
function checkElementFilled(element, expectedvalue) {
return [
new Promise(resolve => {
@@ -62,17 +64,21 @@ function checkAutoCompleteInputFilled(el
function checkFormFilled(address) {
let promises = [];
for (let prop in address) {
let element = document.getElementById(prop);
if (document.activeElement == element) {
promises.push(checkAutoCompleteInputFilled(element, address[prop]));
} else {
- promises.push(...checkElementFilled(element, address[prop]));
+ 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() {
await addAddress(MOCK_STORAGE[0]);
@@ -101,31 +107,50 @@ add_task(async function history_only_men
// Form with both history and address storage.
add_task(async function check_menu_when_both_existed() {
await setupAddressStorage();
await setInput("#organization", "");
doKey("down");
await expectPopup();
checkMenuEntries(MOCK_STORAGE.map(address =>
- JSON.stringify({primary: address.organization, secondary: address["street-address"]})
+ JSON.stringify({
+ primary: address.organization,
+ secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ })
));
await setInput("#street-address", "");
doKey("down");
await expectPopup();
checkMenuEntries(MOCK_STORAGE.map(address =>
- JSON.stringify({primary: address["street-address"], secondary: address.organization})
+ JSON.stringify({
+ primary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ secondary: address.organization,
+ })
));
await setInput("#tel", "");
doKey("down");
await expectPopup();
checkMenuEntries(MOCK_STORAGE.map(address =>
- JSON.stringify({primary: address.tel, secondary: address["street-address"]})
+ JSON.stringify({
+ primary: address.tel,
+ secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ })
+ ));
+
+ await setInput("#address-line1", "");
+ doKey("down");
+ await expectPopup();
+ checkMenuEntries(MOCK_STORAGE.map(address =>
+ JSON.stringify({
+ primary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ secondary: address.organization,
+ })
));
});
// Display history search result if no matched data in addresses.
add_task(async function check_fallback_for_mismatched_field() {
await setInput("#email", "");
doKey("down");
await expectPopup();
@@ -133,17 +158,20 @@ add_task(async function check_fallback_f
});
// Autofill the address from dropdown menu.
add_task(async function check_fields_after_form_autofill() {
await setInput("#organization", "Moz");
doKey("down");
await expectPopup();
checkMenuEntries(MOCK_STORAGE.map(address =>
- JSON.stringify({primary: address.organization, secondary: address["street-address"]})
+ JSON.stringify({
+ primary: address.organization,
+ secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ })
).slice(1));
doKey("down");
await checkFormFilled(MOCK_STORAGE[1]);
});
// Fallback to history search after autofill address.
add_task(async function check_fallback_after_form_autofill() {
await setInput("#tel", "");
@@ -155,30 +183,34 @@ add_task(async function check_fallback_a
// Resume form autofill once all the autofilled fileds are changed.
add_task(async function check_form_autofill_resume() {
document.querySelector("#tel").blur();
document.querySelector("#form1").reset();
await setInput("#tel", "");
doKey("down");
await expectPopup();
checkMenuEntries(MOCK_STORAGE.map(address =>
- JSON.stringify({primary: address.tel, secondary: address["street-address"]})
+ JSON.stringify({
+ primary: address.tel,
+ secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]),
+ })
));
});
</script>
<p id="display"></p>
<div id="content">
<form id="form1">
<p>This is a basic form.</p>
<p><label>organization: <input id="organization" name="organization" autocomplete="organization" type="text"></label></p>
<p><label>streetAddress: <input id="street-address" name="street-address" autocomplete="street-address" type="text"></label></p>
+ <p><label>address-line1: <input id="address-line1" name="address-line1" autocomplete="address-line1" type="text"></label></p>
<p><label>tel: <input id="tel" name="tel" autocomplete="tel" type="text"></label></p>
<p><label>email: <input id="email" name="email" autocomplete="email" type="text"></label></p>
<p><label>country: <select id="country" name="country" autocomplete="country">
<option/>
<option value="US">United States</option>
</select></label></p>
<p><label>states: <select id="address-level1" name="address-level1" autocomplete="address-level1">
<option/>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
@@ -0,0 +1,118 @@
+/*
+ * Test for form auto fill content helper fill all inputs function.
+ */
+
+"use strict";
+
+Cu.import("resource://formautofill/FormAutofillHandler.jsm");
+
+const DEFAULT_PROFILE = {
+ "guid": "123",
+ "street-address": "2 Harrison St\nline2\nline3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2",
+ "address-line3": "line3",
+};
+
+const TESTCASES = [
+ {
+ description: "Form with street-address",
+ document: `<form>
+ <input id="street-addr" autocomplete="street-address">
+ </form>`,
+ profileData: [Object.assign({}, DEFAULT_PROFILE)],
+ expectedResult: [{
+ "guid": "123",
+ "street-address": "2 Harrison St line2 line3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ }],
+ },
+ {
+ description: "Form with street-address, address-line[1, 2, 3]",
+ document: `<form>
+ <input id="street-addr" autocomplete="street-address">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line2" autocomplete="address-line2">
+ <input id="line3" autocomplete="address-line3">
+ </form>`,
+ profileData: [Object.assign({}, DEFAULT_PROFILE)],
+ expectedResult: [{
+ "guid": "123",
+ "street-address": "2 Harrison St line2 line3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ }],
+ },
+ {
+ description: "Form with street-address, address-line1",
+ document: `<form>
+ <input id="street-addr" autocomplete="street-address">
+ <input id="line1" autocomplete="address-line1">
+ </form>`,
+ profileData: [Object.assign({}, DEFAULT_PROFILE)],
+ expectedResult: [{
+ "guid": "123",
+ "street-address": "2 Harrison St line2 line3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St line2 line3",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ }],
+ },
+ {
+ description: "Form with street-address, address-line[1, 2]",
+ document: `<form>
+ <input id="street-addr" autocomplete="street-address">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line2" autocomplete="address-line2">
+ </form>`,
+ profileData: [Object.assign({}, DEFAULT_PROFILE)],
+ expectedResult: [{
+ "guid": "123",
+ "street-address": "2 Harrison St line2 line3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2 line3",
+ "address-line3": "line3",
+ }],
+ },
+ {
+ description: "Form with street-address, address-line[1, 3]",
+ document: `<form>
+ <input id="street-addr" autocomplete="street-address">
+ <input id="line1" autocomplete="address-line1">
+ <input id="line3" autocomplete="address-line3">
+ </form>`,
+ profileData: [Object.assign({}, DEFAULT_PROFILE)],
+ expectedResult: [{
+ "guid": "123",
+ "street-address": "2 Harrison St line2 line3",
+ "-moz-street-address-one-line": "2 Harrison St line2 line3",
+ "address-line1": "2 Harrison St line2",
+ "address-line2": "line2",
+ "address-line3": "line3",
+ }],
+ },
+];
+
+for (let testcase of TESTCASES) {
+ add_task(async function() {
+ do_print("Starting testcase: " + testcase.description);
+
+ let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
+ testcase.document);
+ let form = doc.querySelector("form");
+ let formLike = FormLikeFactory.createFromForm(form);
+ let handler = new FormAutofillHandler(formLike);
+
+ handler.collectFormFields();
+ let adaptedAddresses = handler.getAdaptedProfiles(testcase.profileData);
+ Assert.deepEqual(adaptedAddresses, testcase.expectedResult);
+ });
+}
+
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_toOneLineAddress.js
@@ -0,0 +1,52 @@
+
+"use strict";
+
+Cu.import("resource://formautofill/FormAutofillUtils.jsm");
+
+add_task(async function test_getCategoriesFromFieldNames() {
+ const TEST_CASES = [
+ {
+ strings: ["A", "B", "C", "D"],
+ expectedValue: "A B C D",
+ },
+ {
+ strings: ["A", "B", "", "D"],
+ expectedValue: "A B D",
+ },
+ {
+ strings: ["", "B", "", "D"],
+ expectedValue: "B D",
+ },
+ {
+ strings: "A B C",
+ expectedValue: "A B C",
+ },
+ {
+ strings: "A\nB\n\n\nC",
+ expectedValue: "A B C",
+ },
+ {
+ strings: "A B \nC",
+ expectedValue: "A B C",
+ },
+ {
+ strings: "A-B-C",
+ expectedValue: "A B C",
+ delimiter: "-",
+ },
+ {
+ strings: "A B\n \nC",
+ expectedValue: "A B C",
+ },
+ ];
+
+ for (let tc of TEST_CASES) {
+ let result;
+ if (tc.delimiter) {
+ result = FormAutofillUtils.toOneLineAddress(tc.strings, tc.delimiter);
+ } else {
+ result = FormAutofillUtils.toOneLineAddress(tc.strings);
+ }
+ Assert.deepEqual(result, tc.expectedValue);
+ }
+});
--- a/browser/extensions/formautofill/test/unit/xpcshell.ini
+++ b/browser/extensions/formautofill/test/unit/xpcshell.ini
@@ -18,19 +18,21 @@ support-files =
[heuristics/third_party/test_Walmart.js]
[test_activeStatus.js]
[test_addressRecords.js]
[test_autofillFormFields.js]
[test_collectFormFields.js]
[test_creditCardRecords.js]
[test_extractLabelStrings.js]
[test_findLabelElements.js]
+[test_getAdaptedProfiles.js]
[test_getCategoriesFromFieldNames.js]
[test_getFormInputDetails.js]
[test_getInfo.js]
[test_isCJKName.js]
[test_isFieldEligibleForAutofill.js]
[test_markAsAutofillField.js]
[test_nameUtils.js]
[test_onFormSubmitted.js]
[test_profileAutocompleteResult.js]
[test_savedFieldNames.js]
+[test_toOneLineAddress.js]
[test_transformFields.js]