зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1556953 - Only fill with an exact match when tabbing between fields in a login form. r=MattN
Differential Revision: https://phabricator.services.mozilla.com/D47191 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
23f72bc330
Коммит
13f15730a5
|
@ -1077,6 +1077,20 @@ this.LoginManagerContent = {
|
|||
showMasterPassword: false,
|
||||
})
|
||||
.then(({ form, loginsFound, recipes }) => {
|
||||
if (!loginGUID) {
|
||||
// not an explicit autocomplete menu selection, filter for exact matches only
|
||||
loginsFound = this._filterForExactFormOriginLogins(
|
||||
loginsFound,
|
||||
acForm
|
||||
);
|
||||
// filter the list for exact matches with the username
|
||||
// NOTE: this could be an empty string which is a valid username
|
||||
let searchString = usernameField.value.toLowerCase();
|
||||
loginsFound = loginsFound.filter(
|
||||
l => l.username.toLowerCase() == searchString
|
||||
);
|
||||
}
|
||||
|
||||
this._fillForm(form, loginsFound, recipes, {
|
||||
autofillForm: true,
|
||||
clobberPassword: true,
|
||||
|
@ -1709,6 +1723,53 @@ this.LoginManagerContent = {
|
|||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter logins for exact origin/formActionOrigin and dedupe on usernamematche
|
||||
* @param {nsILoginInfo[]} logins an array of nsILoginInfo that could be
|
||||
* used for the form, including ones with a different form action origin
|
||||
* which are only used when the fill is userTriggered
|
||||
* @param {LoginForm} form
|
||||
*/
|
||||
_filterForExactFormOriginLogins(logins, form) {
|
||||
let loginOrigin = LoginHelper.getLoginOrigin(
|
||||
form.ownerDocument.documentURI
|
||||
);
|
||||
let formActionOrigin = LoginHelper.getFormActionOrigin(form);
|
||||
|
||||
logins = logins.filter(l => {
|
||||
let formActionMatches = LoginHelper.isOriginMatching(
|
||||
l.formActionOrigin,
|
||||
formActionOrigin,
|
||||
{
|
||||
schemeUpgrades: LoginHelper.schemeUpgrades,
|
||||
acceptWildcardMatch: true,
|
||||
acceptDifferentSubdomains: false,
|
||||
}
|
||||
);
|
||||
let formOriginMatches = LoginHelper.isOriginMatching(
|
||||
l.origin,
|
||||
loginOrigin,
|
||||
{
|
||||
schemeUpgrades: LoginHelper.schemeUpgrades,
|
||||
acceptWildcardMatch: true,
|
||||
acceptDifferentSubdomains: false,
|
||||
}
|
||||
);
|
||||
return formActionMatches && formOriginMatches;
|
||||
});
|
||||
|
||||
// Since the logins are already filtered now to only match the origin and formAction,
|
||||
// dedupe to just the username since remaining logins may have different schemes.
|
||||
logins = LoginHelper.dedupeLogins(
|
||||
logins,
|
||||
["username"],
|
||||
["scheme", "timePasswordChanged"],
|
||||
loginOrigin,
|
||||
formActionOrigin
|
||||
);
|
||||
return logins;
|
||||
},
|
||||
|
||||
/**
|
||||
* Attempt to find the username and password fields in a form, and fill them
|
||||
* in using the provided logins and recipes.
|
||||
|
@ -1839,41 +1900,7 @@ this.LoginManagerContent = {
|
|||
if (!userTriggered) {
|
||||
// Only autofill logins that match the form's action and origin. In the above code
|
||||
// we have attached autocomplete for logins that don't match the form action.
|
||||
let loginOrigin = LoginHelper.getLoginOrigin(
|
||||
form.ownerDocument.documentURI
|
||||
);
|
||||
let formActionOrigin = LoginHelper.getFormActionOrigin(form);
|
||||
foundLogins = foundLogins.filter(l => {
|
||||
let formActionMatches = LoginHelper.isOriginMatching(
|
||||
l.formActionOrigin,
|
||||
formActionOrigin,
|
||||
{
|
||||
schemeUpgrades: LoginHelper.schemeUpgrades,
|
||||
acceptWildcardMatch: true,
|
||||
acceptDifferentSubdomains: false,
|
||||
}
|
||||
);
|
||||
let formOriginMatches = LoginHelper.isOriginMatching(
|
||||
l.origin,
|
||||
loginOrigin,
|
||||
{
|
||||
schemeUpgrades: LoginHelper.schemeUpgrades,
|
||||
acceptWildcardMatch: true,
|
||||
acceptDifferentSubdomains: false,
|
||||
}
|
||||
);
|
||||
return formActionMatches && formOriginMatches;
|
||||
});
|
||||
|
||||
// Since the logins are already filtered now to only match the origin and formAction,
|
||||
// dedupe to just the username since remaining logins may have different schemes.
|
||||
foundLogins = LoginHelper.dedupeLogins(
|
||||
foundLogins,
|
||||
["username"],
|
||||
["scheme", "timePasswordChanged"],
|
||||
loginOrigin,
|
||||
formActionOrigin
|
||||
);
|
||||
foundLogins = this._filterForExactFormOriginLogins(foundLogins, form);
|
||||
}
|
||||
|
||||
// Nothing to do if we have no matching logins available.
|
||||
|
|
|
@ -43,6 +43,9 @@ skip-if = toolkit == 'android' # autocomplete
|
|||
[test_autocomplete_sandboxed.html]
|
||||
scheme = https
|
||||
skip-if = toolkit == 'android' # autocomplete
|
||||
[test_autocomplete_tab_between_fields.html]
|
||||
scheme = https
|
||||
skip-if = toolkit == 'android' # autocomplete
|
||||
[test_autofill_autocomplete_types.html]
|
||||
scheme = https
|
||||
skip-if = toolkit == 'android' # bug 1533965
|
||||
|
|
|
@ -15,6 +15,11 @@ function initLogins() {
|
|||
let login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
|
||||
login1.init("https://example.com", "https://autofill", null, "user1", "pass1");
|
||||
Services.logins.addLogin(login1);
|
||||
|
||||
// add a 2nd matching user to prevent autofill
|
||||
let login2 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
|
||||
login2.init("https://example.com", "https://autofill", null, "user2", "pass2");
|
||||
Services.logins.addLogin(login2);
|
||||
}
|
||||
|
||||
runInParent(initLogins);
|
||||
|
@ -27,7 +32,7 @@ function preventDefaultAndStopProgagation(event) {
|
|||
}
|
||||
</script>
|
||||
<p id="display">
|
||||
<form id="form1" action="https://no-autofill" onsubmit="return false;">
|
||||
<form id="form1" action="https://autofill" onsubmit="return false;">
|
||||
<input type="text" name="uname">
|
||||
<input type="password" name="pword">
|
||||
<button type="submit">Submit</button>
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test autocomplete behavior when tabbing between form fields</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<script type="text/javascript" src="pwmgr_common.js"></script>
|
||||
<script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
|
||||
SpecialPowers.Ci.nsILoginInfo,
|
||||
"init");
|
||||
let readyPromise = registerRunTests();
|
||||
</script>
|
||||
<p id="display"></p>
|
||||
|
||||
<!-- we presumably can't hide the content for this test. -->
|
||||
<div id="content">
|
||||
<form id="form1" action="https://autofill" onsubmit="return false;">
|
||||
<input type="text" name="uname">
|
||||
<input type="password" name="pword">
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript">
|
||||
const { TestUtils } = SpecialPowers.Cu.import("resource://testing-common/TestUtils.jsm");
|
||||
|
||||
async function prepareLogins(logins = []) {
|
||||
await LoginManager.removeAllLogins();
|
||||
|
||||
for (let login of logins) {
|
||||
let storageAddPromise = promiseStorageChanged(["addLogin"]);
|
||||
await LoginManager.addLogin(login);
|
||||
await storageAddPromise;
|
||||
}
|
||||
let count = (await LoginManager.getAllLogins()).length;
|
||||
is(count, logins.length, "All logins were added");
|
||||
}
|
||||
|
||||
const availableLogins = {
|
||||
"exampleUser1": new nsLoginInfo("https://example.com", "https://autofill", null,
|
||||
"user1", "pass1", "uname", "pword"),
|
||||
"subdomainUser1": new nsLoginInfo("https://sub.example.com", "https://autofill", null,
|
||||
"user1", "pass1", "uname", "pword"),
|
||||
}
|
||||
|
||||
const tests = [
|
||||
{
|
||||
name: "single_login_exact_origin_no_inputs",
|
||||
logins: ["exampleUser1"],
|
||||
expectedAutofillUsername: "user1",
|
||||
expectedAutofillPassword: "pass1",
|
||||
expectedACLabels: ["user1"],
|
||||
typeUsername: null,
|
||||
expectedTabbedUsername: "",
|
||||
expectedTabbedPassword: "",
|
||||
},
|
||||
{
|
||||
name: "single_login_exact_origin_initial_letter",
|
||||
logins: ["exampleUser1"],
|
||||
expectedAutofillUsername: "user1",
|
||||
expectedAutofillPassword: "pass1",
|
||||
expectedACLabels: ["user1"],
|
||||
typeUsername: "u",
|
||||
expectedTabbedUsername: "u",
|
||||
expectedTabbedPassword: "",
|
||||
},
|
||||
{
|
||||
name: "single_login_exact_origin_type_username",
|
||||
logins: ["exampleUser1"],
|
||||
expectedAutofillUsername: "user1",
|
||||
expectedAutofillPassword: "pass1",
|
||||
expectedACLabels: ["user1"],
|
||||
typeUsername: "user1",
|
||||
expectedTabbedUsername: "user1",
|
||||
expectedTabbedPassword: "pass1",
|
||||
},
|
||||
{
|
||||
name: "single_login_subdomain_no_inputs",
|
||||
logins: ["subdomainUser1"],
|
||||
expectedAutofillUsername: "",
|
||||
expectedAutofillPassword: "",
|
||||
expectedACLabels: ["user1"],
|
||||
typeUsername: null,
|
||||
expectedTabbedUsername: "",
|
||||
expectedTabbedPassword: "",
|
||||
},
|
||||
{
|
||||
name: "single_login_subdomain_type_username",
|
||||
logins: ["subdomainUser1"],
|
||||
expectedAutofillUsername: "",
|
||||
expectedAutofillPassword: "",
|
||||
expectedACLabels: ["user1"],
|
||||
typeUsername: "user1",
|
||||
expectedTabbedUsername: "user1",
|
||||
expectedTabbedPassword: "",
|
||||
},
|
||||
];
|
||||
|
||||
add_task(async function setup() {
|
||||
ok(readyPromise, "check promise is available");
|
||||
await readyPromise;
|
||||
});
|
||||
|
||||
async function testResultOfTabInteractions(testData) {
|
||||
await SimpleTest.promiseFocus(window);
|
||||
let logins = testData.logins.map(name => availableLogins[name]);
|
||||
await prepareLogins(logins);
|
||||
|
||||
info("recreating form");
|
||||
let processed = promiseFormsProcessed();
|
||||
recreateTree(document.getElementById("form1"));
|
||||
info("waiting for form processed");
|
||||
await processed;
|
||||
// check autofill results
|
||||
checkForm(1, testData.expectedAutofillUsername, testData.expectedAutofillPassword);
|
||||
let pword = $_(1, "pword");
|
||||
let uname = $_(1, "uname");
|
||||
|
||||
SpecialPowers.wrap(pword).setUserInput("");
|
||||
SpecialPowers.wrap(uname).setUserInput("");
|
||||
|
||||
info("Placing focus in the password field");
|
||||
const shownPromise = promiseACShown();
|
||||
pword.focus();
|
||||
await synthesizeKey("KEY_Tab", { shiftKey: true }); // blur pw, focus un
|
||||
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
||||
|
||||
// moving focus shouldn't change anything
|
||||
checkForm(1, "", "");
|
||||
|
||||
await synthesizeKey("KEY_ArrowDown");
|
||||
info("waiting for AC results");
|
||||
let results = await shownPromise;
|
||||
info("checking results");
|
||||
checkAutoCompleteResults(results, testData.expectedACLabels, "example.com", "Check all rows are correct");
|
||||
|
||||
if (testData.typeUsername) {
|
||||
await sendString(testData.typeUsername);
|
||||
}
|
||||
|
||||
// don't select anything from the AC menu
|
||||
await synthesizeKey("KEY_Escape");
|
||||
await TestUtils.waitForCondition(async () => {
|
||||
let popupState = await getPopupState();
|
||||
return !popupState.open;
|
||||
}, "AutoComplete popup should have closed");
|
||||
|
||||
await synthesizeKey("KEY_Tab");
|
||||
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
||||
|
||||
ok($_(1, "pword").matches("input:focus"), "pword field is focused");
|
||||
checkForm(1, testData.expectedTabbedUsername, testData.expectedTabbedPassword);
|
||||
|
||||
recreateTree(document.getElementById("form1"));
|
||||
await promiseFormsProcessed();
|
||||
// tidy up by closing any open AC popup
|
||||
await synthesizeKey("KEY_Escape");
|
||||
}
|
||||
|
||||
for (let testData of tests) {
|
||||
let tmp = {
|
||||
async [testData.name]() {
|
||||
await testResultOfTabInteractions(testData);
|
||||
},
|
||||
};
|
||||
add_task(tmp[testData.name]);
|
||||
}
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
Загрузка…
Ссылка в новой задаче