Bug 1625028 - Add plain mochitest coverage for new-password heuristics r=MattN

* Updated the toolkit/components/passwordmgr/test/mochitest/test_autocomplete_new_password.html test to include a third form that has enough signal for the new-password heuristics to detect its password field as a new password field.
* Factored out most of the test_autofillAutocompletePassword_withGeneration task to be rechecked for this new form.
* Created some new helper functions and promoted them to the global scope in the test, so they can be used by other tasks as well.

Differential Revision: https://phabricator.services.mozilla.com/D68499

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Bianca Danforth 2020-04-01 19:47:12 +00:00
Родитель 21171604ac
Коммит d3709f2977
1 изменённых файлов: 220 добавлений и 208 удалений

Просмотреть файл

@ -46,7 +46,19 @@ let readyPromise = registerRunTests();
<!-- form2 uses a new-password type=password field -->
<form id="form2" action="https://autofill" onsubmit="return false;">
<input type="text" name="uname">
<input type="password" name="pword" autocomplete="new-password">
<input type="password" name="password" autocomplete="new-password">
<button type="submit">Submit</button>
</form>
<!-- form3 has enough signal to make new-password heuristics (Bug 1595244) detect
a new password field, even though there is no autocomplete="new-password"
attribute on the <input type="password"> element-->
<form id="form3" action="https://autofill" onsubmit="return false;">
<input type="text" name="username">
<label>
New password
<input type="password" name="password">
</label>
<button type="submit">Submit</button>
</form>
</div>
@ -96,6 +108,31 @@ async function promiseACPopupClosed() {
}, "Wait for AC popup to be closed");
}
async function showACPopup(formNumber, expectedACLabels) {
let shownPromise = promiseACShown();
synthesizeKey("KEY_ArrowDown");
let results = await shownPromise;
checkAutoCompleteResults(results, expectedACLabels, "example.com", "Check all rows are correct");
}
async function checkTelemetryEventsPWGenShown(expectedPWGenTelemetryEvents) {
info(`showed generated password option, check there are now ${expectedPWGenTelemetryEvents} generatedpassword telemetry events`);
await waitForTelemetryEventsCondition(events => {
return events.length == expectedPWGenTelemetryEvents;
}, { process: "parent", filterProps: TelemetryFilterPropsShown }, `Wait for there to be ${expectedPWGenTelemetryEvents} shown telemetry events`);
}
async function checkTelemetryEventsPWGenUsed(expectedPWGenTelemetryEvents) {
info("filled generated password again, ensure we don't record another generatedpassword autocomplete telemetry event");
let telemetryEvents;
try {
telemetryEvents = await waitForTelemetryEventsCondition(events => events.length == expectedPWGenTelemetryEvents + 1,
{ process: "parent", filterProps: TelemetryFilterPropsUsed },
`Wait for there to be ${expectedPWGenTelemetryEvents + 1} used events`, 50);
} catch (ex) {}
ok(!telemetryEvents, `Expected to timeout waiting for there to be ${expectedPWGenTelemetryEvents + 1} events`);
}
add_task(async function setup() {
await SpecialPowers.pushPrefEnv({"set": [
["signon.generation.available", false],
@ -109,6 +146,9 @@ add_task(async function setup() {
is(showEvents.length, 0, "Expect 0 show events");
let acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true });
is(acEvents.length, 0, "Expect 0 autocomplete events");
// This can be removed once Bug 1623431 is resolved.
document.getElementById("form3").reset();
});
add_task(async function test_autofillAutocompleteUsername_noGeneration() {
@ -119,11 +159,7 @@ add_task(async function test_autofillAutocompleteUsername_noGeneration() {
checkForm(2, "", "");
$_(2, "uname").focus();
const shownPromise = promiseACShown();
synthesizeKey("KEY_ArrowDown");
let results = await shownPromise;
let expectedACLabels = ["user1"];
checkAutoCompleteResults(results, expectedACLabels, "example.com", "Check all rows are correct");
await showACPopup(2, ["user1"]);
let acEvents = await waitForTelemetryEventsCondition(events => {
return events.length == 1;
@ -146,13 +182,9 @@ add_task(async function test_autofillAutocompletePassword_noGeneration() {
// 2nd form should not be filled
checkForm(2, "", "");
let pword = $_(2, "pword");
let pword = $_(2, "password");
pword.focus();
const shownPromise = promiseACShown();
synthesizeKey("KEY_ArrowDown");
let results = await shownPromise;
let expectedACLabels = ["user1"];
checkAutoCompleteResults(results, expectedACLabels, "example.com", "Check all rows are correct");
await showACPopup(2, ["user1"]);
let acEvents = await waitForTelemetryEventsCondition(events => {
return events.length == 1;
}, { process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }, `Wait for there to be 1 autocomplete telemetry event`);
@ -188,12 +220,8 @@ add_task(async function test_autofillAutocompleteUsername_noGeneration2() {
checkForm(2, "", "");
$_(2, "uname").focus();
const shownPromise = promiseACShown();
synthesizeKey("KEY_ArrowDown");
let results = await shownPromise;
// No generation option on username fields.
let expectedACLabels = ["user1"];
checkAutoCompleteResults(results, expectedACLabels, "example.com", "Check all rows are correct");
await showACPopup(2, ["user1"]);
let acEvents = await waitForTelemetryEventsCondition(events => {
return events.length == 1;
}, { process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }, `Wait for there to be 1 autocomplete telemetry event`);
@ -212,192 +240,183 @@ add_task(async function test_autofillAutocompleteUsername_noGeneration2() {
});
add_task(async function test_autofillAutocompletePassword_withGeneration() {
// 2nd form should not be filled
checkForm(2, "", "");
const formNumbersToTest = [2, 3];
// Bug 1616356 and Bug 1548878: Recorded once per origin
let expectedPWGenTelemetryEvents = 0;
// Bug 1619498: Recorded once every time the autocomplete popup is shown
let expectedACShownTelemetryEvents = 0;
for (let formNumber of formNumbersToTest) {
// This form should not be filled
checkForm(formNumber, "", "");
let pword = $_(2, "pword");
pword.focus();
let shownPromise = promiseACShown();
synthesizeKey("KEY_ArrowDown");
let results = await shownPromise;
let expectedACLabels = [
"user1",
"Use a Securely Generated Password",
];
checkAutoCompleteResults(results, expectedACLabels, "example.com", "Check all rows are correct");
let pword = $_(formNumber, "password");
pword.focus();
let expectedCount = 1;
info(`showed generated password option, check there are now ${expectedCount} generatedpassword telemetry events`);
await waitForTelemetryEventsCondition(events => {
return events.length == expectedCount;
}, { process: "parent", filterProps: TelemetryFilterPropsShown }, `Wait for there to be ${expectedCount} shown telemetry events`);
await showACPopup(formNumber, [
"user1",
"Use a Securely Generated Password",
]);
expectedPWGenTelemetryEvents++;
expectedACShownTelemetryEvents++;
let acEvents = await waitForTelemetryEventsCondition(events => {
return events.length == 1;
}, { process: "parent", filterProps: TelemetryFilterPropsAC }, `Wait for there to be 1 autocomplete telemetry event`);
checkACTelemetryEvent(acEvents[0], pword, {
"generatedPasswo": "1",
"hadPrevious": "0",
"loginWithOrigin": "1",
"loginsFooter": "1"
});
await checkTelemetryEventsPWGenShown(expectedPWGenTelemetryEvents);
let acEvents = await waitForTelemetryEventsCondition(events => {
return events.length == expectedACShownTelemetryEvents;
}, { process: "parent", filterProps: TelemetryFilterPropsAC }, `Wait for there to be ${expectedACShownTelemetryEvents} autocomplete telemetry event(s)`);
checkACTelemetryEvent(acEvents[expectedACShownTelemetryEvents - 1], pword, {
"generatedPasswo": "1",
"hadPrevious": "0",
"loginWithOrigin": "1",
"loginsFooter": "1"
});
synthesizeKey("KEY_ArrowDown");
synthesizeKey("KEY_Enter");
// Can't use promiseFormsProcessed() when autocomplete fills the field directly.
await SimpleTest.promiseWaitForCondition(() => pword.value == "pass1", "Check pw filled");
checkForm(2, "", "pass1");
synthesizeKey("KEY_ArrowDown");
synthesizeKey("KEY_Enter");
// Can't use promiseFormsProcessed() when autocomplete fills the field directly.
await SimpleTest.promiseWaitForCondition(() => pword.value == "pass1", "Check pw filled");
checkForm(formNumber, "", "pass1");
// No autocomplete results should appear for non-empty pw fields.
synthesizeKey("KEY_ArrowDown");
await promiseNoUnexpectedPopupShown();
// No autocomplete results should appear for non-empty pw fields.
synthesizeKey("KEY_ArrowDown");
await promiseNoUnexpectedPopupShown();
info("Removing all logins to test auto-saving of generated passwords");
await LoginManager.removeAllLogins();
info("Removing all logins to test auto-saving of generated passwords");
await LoginManager.removeAllLogins();
while (pword.value) {
while (pword.value) {
synthesizeKey("KEY_Backspace");
}
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blanked field");
info("This time select the generated password");
await showACPopup(formNumber, [
"Use a Securely Generated Password",
]);
expectedACShownTelemetryEvents++;
await checkTelemetryEventsPWGenShown(expectedPWGenTelemetryEvents);
acEvents = await waitForTelemetryEventsCondition(events => {
return events.length == expectedACShownTelemetryEvents;
}, { process: "parent", filterProps: TelemetryFilterPropsAC }, `Wait for there to be ${expectedACShownTelemetryEvents} autocomplete telemetry event(s)`);
checkACTelemetryEvent(acEvents[expectedACShownTelemetryEvents - 1], pword, {
"generatedPasswo": "1",
"hadPrevious": "0",
"loginsFooter": "1"
});
synthesizeKey("KEY_ArrowDown");
let storageAddPromise = promiseStorageChanged(["addLogin"]);
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Before first fill of generated pw");
synthesizeKey("KEY_Enter");
info("waiting for the password field to be filled with the generated password");
await SimpleTest.promiseWaitForCondition(() => !!pword.value, "Check generated pw filled");
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, "After first fill of generated pw");
info("Wait for generated password to be added to storage");
await storageAddPromise;
let logins = await LoginManager.getAllLogins();
let timePasswordChanged = logins[logins.length - 1].timePasswordChanged;
let time = dateAndTimeFormatter.format(new Date(timePasswordChanged));
const LABEL_NO_USERNAME = "No username (" + time + ")";
let generatedPW = pword.value;
is(generatedPW.length, GENERATED_PASSWORD_LENGTH, "Check generated password length");
ok(generatedPW.match(GENERATED_PASSWORD_REGEX), "Check generated password format");
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, "After fill");
info("Check field is masked upon blurring");
synthesizeKey("KEY_Tab"); // blur
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "After blur");
synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, "After shift-tab to focus again");
// Remove selection for OS where the whole value is selected upon focus.
synthesizeKey("KEY_ArrowRight");
while (pword.value) {
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, pword.value);
synthesizeKey("KEY_Backspace");
}
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blanked field");
info("Blur the empty field to trigger a 'change' event");
synthesizeKey("KEY_Tab"); // blur
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blur after blanking");
synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Focus again after blanking");
info("Type a single character after blanking");
synthesizeKey("@");
info("Blur the single-character field to trigger a 'change' event");
synthesizeKey("KEY_Tab"); // blur
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blur after backspacing");
synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Focus again after backspacing");
synthesizeKey("KEY_Backspace"); // Blank the field again
await showACPopup(formNumber, [
LABEL_NO_USERNAME,
"Use a Securely Generated Password",
]);
expectedACShownTelemetryEvents++;
synthesizeKey("KEY_ArrowDown");
synthesizeKey("KEY_ArrowDown");
synthesizeKey("KEY_Enter");
await SimpleTest.promiseWaitForCondition(() => !!pword.value, "Check generated pw filled");
// Same generated password should be used, even despite the 'change' to @ earlier.
checkForm(formNumber, "", generatedPW);
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, "Second fill of the generated pw");
await checkTelemetryEventsPWGenUsed(expectedPWGenTelemetryEvents);
logins = await LoginManager.getAllLogins();
is(logins.length, 1, "Still 1 login after filling the generated password a 2nd time");
is(logins[0].timePasswordChanged, timePasswordChanged, "Saved login wasn't changed");
is(logins[0].password, generatedPW, "Password is the same");
info("filling the saved login to ensure the field is masked again");
while (pword.value) {
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, pword.value);
synthesizeKey("KEY_Backspace");
}
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blanked field again");
info("Blur the field to trigger a 'change' event again");
synthesizeKey("KEY_Tab"); // blur
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blur after blanking again");
synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Focus again after blanking again");
// Remove selection for OS where the whole value is selected upon focus.
synthesizeKey("KEY_ArrowRight");
await showACPopup(formNumber, [
LABEL_NO_USERNAME,
"Use a Securely Generated Password",
]);
expectedACShownTelemetryEvents++;
synthesizeKey("KEY_ArrowDown");
synthesizeKey("KEY_Enter");
await SimpleTest.promiseWaitForCondition(() => !!pword.value, "Check saved generated pw filled");
// Same generated password should be used but from storage
checkForm(formNumber, "", generatedPW);
// Passwords from storage should always be masked.
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "after fill from storage");
synthesizeKey("KEY_Tab"); // blur
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "after blur");
synthesizeKey("KEY_Tab", { shiftKey: true }); // focus
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "after shift-tab to focus again");
info("reset initial login state with login1");
runInParent(initLogins);
info("invalidate the autocomplete cache after updating storage above");
synthesizeKey("KEY_Backspace");
recreateTree(document.getElementById(`form${formNumber}`));
}
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blanked field");
info("This time select the generated password");
shownPromise = promiseACShown();
synthesizeKey("KEY_ArrowDown");
results = await shownPromise;
expectedACLabels = [
"Use a Securely Generated Password",
];
checkAutoCompleteResults(results, expectedACLabels, "example.com", "Check all rows are correct");
synthesizeKey("KEY_ArrowDown");
let storageAddPromise = promiseStorageChanged(["addLogin"]);
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Before first fill of generated pw");
synthesizeKey("KEY_Enter");
info("waiting for the password field to be filled with the generated password");
await SimpleTest.promiseWaitForCondition(() => !!pword.value, "Check generated pw filled");
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, "After first fill of generated pw");
info("Wait for generated password to be added to storage");
await storageAddPromise;
expectedCount = 1;
info(`filled generated password, check there are now ${expectedCount} generatedpassword telemetry events`);
await waitForTelemetryEventsCondition(events => {
return events.length == expectedCount;
}, { process: "parent", filterProps: TelemetryFilterPropsUsed }, `Wait for there to be ${expectedCount} used telemetry events`);
let logins = await LoginManager.getAllLogins();
let timePasswordChanged = logins[logins.length - 1].timePasswordChanged;
let time = dateAndTimeFormatter.format(new Date(timePasswordChanged));
const LABEL_NO_USERNAME = "No username (" + time + ")";
let generatedPW = pword.value;
is(generatedPW.length, GENERATED_PASSWORD_LENGTH, "Check generated password length");
ok(generatedPW.match(GENERATED_PASSWORD_REGEX), "Check generated password format");
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, "After fill");
info("Check field is masked upon blurring");
synthesizeKey("KEY_Tab"); // blur
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "After blur");
synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, "After shift-tab to focus again");
// Remove selection for OS where the whole value is selected upon focus.
synthesizeKey("KEY_ArrowRight");
while (pword.value) {
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, pword.value);
synthesizeKey("KEY_Backspace");
}
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blanked field");
info("Blur the empty field to trigger a 'change' event");
synthesizeKey("KEY_Tab"); // blur
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blur after blanking");
synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Focus again after blanking");
info("Type a single character after blanking");
synthesizeKey("@");
info("Blur the single-character field to trigger a 'change' event");
synthesizeKey("KEY_Tab"); // blur
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blur after backspacing");
synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Focus again after backspacing");
synthesizeKey("KEY_Backspace"); // Blank the field again
shownPromise = promiseACShown();
synthesizeKey("KEY_ArrowDown");
results = await shownPromise;
expectedACLabels = [
LABEL_NO_USERNAME,
"Use a Securely Generated Password",
];
checkAutoCompleteResults(results, expectedACLabels, "example.com", "Check all rows are correct");
synthesizeKey("KEY_ArrowDown");
synthesizeKey("KEY_ArrowDown");
synthesizeKey("KEY_Enter");
await SimpleTest.promiseWaitForCondition(() => !!pword.value, "Check generated pw filled");
// Same generated password should be used, even despite the 'change' to @ earlier.
checkForm(2, "", generatedPW);
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, "Second fill of the generated pw");
info("filled generated password again, ensure we don't record another generatedpassword autocomplete telemetry event");
let telemetryEvents;
expectedCount = 2;
try {
telemetryEvents = await waitForTelemetryEventsCondition(events => events.length == expectedCount,
{ process: "parent", filterProps: TelemetryFilterPropsUsed },
`Wait for there to be ${expectedCount} used events`, 50);
} catch (ex) {}
ok(!telemetryEvents, "Expected to timeout waiting for there to be 2 events");
logins = await LoginManager.getAllLogins();
is(logins.length, 1, "Still 1 login after filling the generated password a 2nd time");
is(logins[0].timePasswordChanged, timePasswordChanged, "Saved login wasn't changed");
is(logins[0].password, generatedPW, "Password is the same");
info("filling the saved login to ensure the field is masked again");
while (pword.value) {
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, pword.value);
synthesizeKey("KEY_Backspace");
}
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blanked field again");
info("Blur the field to trigger a 'change' event again");
synthesizeKey("KEY_Tab"); // blur
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blur after blanking again");
synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Focus again after blanking again");
// Remove selection for OS where the whole value is selected upon focus.
synthesizeKey("KEY_ArrowRight");
shownPromise = promiseACShown();
synthesizeKey("KEY_ArrowDown");
results = await shownPromise;
expectedACLabels = [
LABEL_NO_USERNAME,
"Use a Securely Generated Password",
];
checkAutoCompleteResults(results, expectedACLabels, "example.com", "Check all rows are correct");
synthesizeKey("KEY_ArrowDown");
synthesizeKey("KEY_Enter");
await SimpleTest.promiseWaitForCondition(() => !!pword.value, "Check saved generated pw filled");
// Same generated password should be used but from storage
checkForm(2, "", generatedPW);
// Passwords from storage should always be masked.
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "after fill from storage");
synthesizeKey("KEY_Tab"); // blur
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "after blur");
synthesizeKey("KEY_Tab", { shiftKey: true }); // focus
LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "after shift-tab to focus again");
info("reset initial login state with login1");
runInParent(initLogins);
info("invalidate the autocomplete cache after updating storage above");
synthesizeKey("KEY_Backspace");
recreateTree(document.getElementById("form2"));
});
add_task(async function test_autofillAutocompletePassword_saveLoginDisabled() {
@ -408,17 +427,10 @@ add_task(async function test_autofillAutocompletePassword_saveLoginDisabled() {
await LoginManager.setLoginSavingEnabled("https://example.com", false);
let pword = $_(2, "pword");
let pword = $_(2, "password");
pword.focus();
let shownPromise = promiseACShown();
synthesizeKey("KEY_ArrowDown");
let results = await shownPromise;
let expectedACLabels = [
// when login-saving is disabled for an origin, we expect no generated password row here
"user1",
];
info("ac results: " + results.join(", "));
checkAutoCompleteResults(results, expectedACLabels, "example.com", "Check all rows are correct");
// when login-saving is disabled for an origin, we expect no generated password row here
await showACPopup(2, ["user1"]);
// close any open menu
synthesizeKey("KEY_Escape");
@ -434,7 +446,7 @@ add_task(async function test_deleteAndReselectGeneratedPassword() {
// form should not be filled
checkForm(2, "", "");
let pword = $_(2, "pword");
let pword = $_(2, "password");
let uname = $_(2, "uname");
async function showAndSelectACPopupItem(index) {
@ -512,10 +524,10 @@ add_task(async function test_deleteAndReselectGeneratedPassword() {
});
add_task(async function test_passwordGenerationShownTelemetry() {
// Should only be recorded once per principal origin per session but the cache is cleared once via initLogins.
// Should only be recorded once per principal origin per session, but the cache is cleared each time ``initLogins`` is called.
await waitForTelemetryEventsCondition(events => {
return events.length == 2;
}, { process: "parent", filterProps: TelemetryFilterPropsShown }, "Expect two shown telemetry events");
return events.length == 3;
}, { process: "parent", filterProps: TelemetryFilterPropsShown }, "Expect 3 shown telemetry events");
});
</script>
</pre>