Bug 433238 - Tests for the password manager contextual menu password fill. r=MattN

This commit is contained in:
Bernardo P. Rittmeyer 2015-08-06 18:45:44 -07:00
parent 845e03690d
commit eee2e6e0b2
9 changed files with 517 additions and 4 deletions

View File

@ -61,7 +61,9 @@ function getVisibleMenuItems(aMenu, aData) {
items.push("+" + label);
} else if (item.id.indexOf("spell-check-dictionary-") != 0 &&
item.id != "spell-no-suggestions" &&
item.id != "spell-add-dictionaries-main") {
item.id != "spell-add-dictionaries-main" &&
item.id != "fill-login-saved-passwords" &&
item.id != "fill-login-no-logins") {
ok(key, "menuitem " + item.id + " has an access key");
if (accessKeys[key])
ok(false, "menuitem " + item.id + " has same accesskey as " + accessKeys[key]);

View File

@ -80,6 +80,16 @@ function runTest(testNum) {
"context-inspect", true];
}
var passwordFillItems = [
"---", null,
"fill-login", null,
[
"fill-login-no-logins", false,
"---", null,
"fill-login-saved-passwords", true
], null,
];
var tests = [
function () {
// Invoke context menu for next test.
@ -665,7 +675,8 @@ function runTest(testNum) {
["spell-check-dictionary-en-US", true,
"---", null,
"spell-add-dictionaries", true], null
].concat(inspectItems));
].concat(passwordFillItems)
.concat(inspectItems));
closeContextMenu();
subwindow.getSelection().removeAllRanges();
openContextMenuFor(plugin);

View File

@ -167,6 +167,11 @@ function runTest(testNum) {
"---", null,
"context-selectall", false,
"---", null,
"fill-login", null,
["fill-login-no-logins", false,
"---", null,
"fill-login-saved-passwords", true], null,
"---", null,
"context-inspect", true]);
closeContextMenu();

View File

@ -2,6 +2,7 @@
support-files =
authenticate.sjs
form_basic.html
multiple_forms.html
[browser_DOMFormHasPassword.js]
[browser_DOMInputPasswordAdded.js]
@ -10,6 +11,7 @@ support-files =
skip-if = true # Intermittent failures: Bug 1182296, bug 1148771
[browser_passwordmgr_editing.js]
skip-if = os == "linux"
[browser_context_menu.js]
[browser_passwordmgr_fields.js]
[browser_passwordmgr_observers.js]
[browser_passwordmgr_sort.js]

View File

@ -0,0 +1,270 @@
/*
* Test the password manager context menu.
*/
"use strict";
Cu.import("resource://testing-common/LoginTestUtils.jsm", this);
// The hostname for the test URIs.
const TEST_HOSTNAME = "https://example.com";
/**
* Initialize logins needed for the tests and disable autofill
* for login forms for easier testing of manual fill.
*/
add_task(function* test_initialize() {
Services.prefs.setBoolPref("signon.autofillForms", false);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("signon.autofillForms");
Services.logins.removeAllLogins();
});
for (let login of loginList()) {
Services.logins.addLogin(login);
}
});
/**
* Check if the context menu is populated with the right
* menuitems for the target password input field.
*/
add_task(function* test_context_menu_populate() {
yield BrowserTestUtils.withNewTab({
gBrowser,
url: TEST_HOSTNAME + "/browser/toolkit/components/" +
"passwordmgr/test/browser/multiple_forms.html",
}, function* (browser) {
let passwordInput = browser.contentWindow.document.getElementById("test-password-1");
yield openPasswordContextMenu(browser, passwordInput);
// Check the content of the password manager popup
let popupMenu = document.getElementById("fill-login-popup");
checkMenu(popupMenu);
let contextMenu = document.getElementById("contentAreaContextMenu");
contextMenu.hidePopup();
});
});
/**
* Check if the password field is correctly filled when one
* login menuitem is clicked.
*/
add_task(function* test_context_menu_password_fill() {
// Set of element ids to check.
let testSet = [
{
passwordInput: "test-password-1",
unchangedFields: null,
},
{
passwordInput: "test-password-2",
unchangedFields: ["test-username-2"],
},
{
passwordInput: "test-password-3",
unchangedFields: ["test-username-3"],
},
{
passwordInput: "test-password-4",
unchangedFields: ["test-username-4"],
},
{
passwordInput: "test-password-5",
unchangedFields: ["test-username-5", "test-password2-5"],
},
{
passwordInput: "test-password2-5",
unchangedFields: ["test-username-5", "test-password-5"],
},
{
passwordInput: "test-password-6",
unchangedFields: ["test-username-6", "test-password2-6"],
},
{
passwordInput: "test-password2-6",
unchangedFields: ["test-username-6", "test-password-6"],
},
{
passwordInput: "test-password-7",
unchangedFields: null,
},
];
yield BrowserTestUtils.withNewTab({
gBrowser,
url: TEST_HOSTNAME + "/browser/toolkit/components/" +
"passwordmgr/test/browser/multiple_forms.html",
}, function* (browser) {
for (let testCase of testSet) {
let passwordInput = browser.contentWindow.document.getElementById(testCase.passwordInput);
yield openPasswordContextMenu(browser, passwordInput);
let popupMenu = document.getElementById("fill-login-popup");
// Store the values of fields that should remain unchanged.
let unchangedFieldsValues = null;
if (testCase.unchangedFields) {
unchangedFieldsValues = [];
for (let fieldId of testCase.unchangedFields) {
unchangedFieldsValues[fieldId] = browser.contentWindow.document.getElementById(fieldId).value;
}
}
// Execute the default command of the first login menuitem found at the context menu.
let firstLoginItem = popupMenu.getElementsByClassName("context-login-item")[0];
firstLoginItem.doCommand();
yield BrowserTestUtils.waitForEvent(passwordInput, "input", "Password input value changed");
// Find the used login by it's username (Use only unique usernames in this test).
let login = getLoginFromUsername(firstLoginItem.label);
Assert.equal(login.password, passwordInput.value, "Password filled and correct.");
// Check that the fields that should remain unchanged didn't got modified.
if (testCase.unchangedFields) {
Assert.ok(testCase.unchangedFields.every(fieldId => {
return unchangedFieldsValues[fieldId] == browser.contentWindow.document.getElementById(fieldId).value;
}), "Other fields were not changed.");
}
let contextMenu = document.getElementById("contentAreaContextMenu");
contextMenu.hidePopup();
}
});
});
/**
* Check if the password field is correctly filled when it's in an iframe.
*/
add_task(function* test_context_menu_iframe_fill() {
yield BrowserTestUtils.withNewTab({
gBrowser,
url: TEST_HOSTNAME + "/browser/toolkit/components/" +
"passwordmgr/test/browser/multiple_forms.html",
}, function* (browser) {
let iframe = browser.contentWindow.document.getElementById("test-iframe");
let passwordInput = iframe.contentDocument.getElementById("form-basic-password");
let contextMenuShownPromise = BrowserTestUtils.waitForEvent(window, "popupshown");
let eventDetails = {type: "contextmenu", button: 2};
// To click at the right point we have to take into account the iframe offset.
let iframeRect = iframe.getBoundingClientRect();
let inputRect = passwordInput.getBoundingClientRect();
let clickPos = {
offsetX: iframeRect.left + inputRect.width / 2,
offsetY: iframeRect.top + inputRect.height / 2,
};
// Synthesize a right mouse click over the password input element.
BrowserTestUtils.synthesizeMouse(passwordInput, clickPos.offsetX, clickPos.offsetY, eventDetails, browser);
yield contextMenuShownPromise;
// Synthesize a mouse click over the fill login menu header.
let popupHeader = document.getElementById("fill-login");
let popupShownPromise = BrowserTestUtils.waitForEvent(popupHeader, "popupshown");
EventUtils.synthesizeMouseAtCenter(popupHeader, {});
yield popupShownPromise;
let popupMenu = document.getElementById("fill-login-popup");
// Stores the original value of username
let usernameInput = iframe.contentDocument.getElementById("form-basic-username");
let usernameOriginalValue = usernameInput.value;
// Execute the command of the first login menuitem found at the context menu.
let firstLoginItem = popupMenu.getElementsByClassName("context-login-item")[0];
firstLoginItem.doCommand();
yield BrowserTestUtils.waitForEvent(passwordInput, "input", "Password input value changed");
// Find the used login by it's username.
let login = getLoginFromUsername(firstLoginItem.label);
Assert.equal(login.password, passwordInput.value, "Password filled and correct.");
Assert.equal(usernameOriginalValue,
usernameInput.value,
"Username value was not changed.");
let contextMenu = document.getElementById("contentAreaContextMenu");
contextMenu.hidePopup();
});
});
/**
* Synthesize mouse clicks to open the password manager context menu popup
* for a target password input element.
*/
function* openPasswordContextMenu(browser, passwordInput) {
// Synthesize a right mouse click over the password input element.
let contextMenuShownPromise = BrowserTestUtils.waitForEvent(window, "popupshown");
let eventDetails = {type: "contextmenu", button: 2};
BrowserTestUtils.synthesizeMouseAtCenter(passwordInput, eventDetails, browser);
yield contextMenuShownPromise;
// Synthesize a mouse click over the fill login menu header.
let popupHeader = document.getElementById("fill-login");
let popupShownPromise = BrowserTestUtils.waitForEvent(popupHeader, "popupshown");
EventUtils.synthesizeMouseAtCenter(popupHeader, {});
yield popupShownPromise;
}
/**
* Check if every login that matches the page hostname are available at the context menu.
*/
function checkMenu(contextMenu) {
let logins = loginList().filter(login => login.hostname == TEST_HOSTNAME);
// Make an array of menuitems for easier comparison.
let menuitems = [...contextMenu.getElementsByClassName("context-login-item")];
Assert.equal(menuitems.length, logins.length, "Same amount of menu items and expected logins.");
Assert.ok(logins.every(l => menuitems.some(m => l.username == m.label)), "Every login have an item at the menu.");
}
/**
* Search for a login by it's username.
*
* Only unique login/hostname combinations should be used at this test.
*/
function getLoginFromUsername(username) {
return loginList().find(login => login.username == username);
}
/**
* List of logins used for the test.
*
* We should only use unique usernames in this test,
* because we need to search logins by username.
*/
function loginList() {
return [
LoginTestUtils.testData.formLogin({
hostname: "https://example.com",
formSubmitURL: "https://example.com",
username: "username",
password: "password",
}),
LoginTestUtils.testData.formLogin({
hostname: "http://example.com",
formSubmitURL: "http://example.com",
username: "username1",
password: "password1",
}),
LoginTestUtils.testData.formLogin({
hostname: "https://example.com",
formSubmitURL: "https://example.com",
username: "username2",
password: "password2",
}),
LoginTestUtils.testData.formLogin({
hostname: "https://example.org",
formSubmitURL: "https://example.org",
username: "username-cross-origin",
password: "password-cross-origin",
}),
];
}

View File

@ -0,0 +1,57 @@
<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!-- Password only form -->
<form id='form-1'>
<input id='test-password-1' type='password' name='pname' value=''>
<input type='submit'>Submit</input>
</form>
<!-- Simple username and password blank form -->
<form id='form-2'>
<input id='test-username-2' type='text' name='uname' value=''>
<input id='test-password-2' type='password' name='pname' value=''>
<button type='submit'>Submit</button>
</form>
<!-- Simple username and password form, prefilled username -->
<form id='form-3'>
<input id='test-username-3' type='text' name='uname' value='testuser'>
<input id='test-password-3' type='password' name='pname' value=''>
<button type='submit'>Submit</button>
</form>
<!-- Simple username and password form, prefilled username and password -->
<form id='form-4'>
<input id='test-username-4' type='text' name='uname' value='testuser'>
<input id='test-password-4' type='password' name='pname' value='testpass'>
<button type='submit'>Submit</button>
</form>
<!-- One username and two passwords empty form -->
<form id='form-5'>
<input id='test-username-5' type='text' name='uname'>
<input id='test-password-5' type='password' name='pname'>
<input id='test-password2-5' type='password' name='pname2'>
<button type='submit'>Submit</button>
</form>
<!-- One username and two passwords form, fields prefiled -->
<form id='form-6'>
<input id='test-username-6' type='text' name='uname' value="testuser">
<input id='test-password-6' type='password' name='pname' value="testpass">
<input id='test-password2-6' type='password' name='pname2' value="testpass">
<button type='submit'>Submit</button>
</form>
<!-- Password field with no form -->
<div id='form-7'>
<input id='test-password-7' type='password' name='pname' value="testpass">
</div>
<!-- Form in an iframe -->
<iframe src="https://example.org/browser/toolkit/components/passwordmgr/test/browser/form_basic.html" id="test-iframe"></iframe>
</body>
</html>

View File

@ -129,11 +129,11 @@ const MockDocument = {
/**
* Create a document for the given URL containing the given HTML with the ownerDocument of all <form>s having a mocked location.
*/
createTestDocument(aDocumentURL, aHTML = "<form>") {
createTestDocument(aDocumentURL, aContent = "<form>", aType = "text/html") {
let parser = Cc["@mozilla.org/xmlextras/domparser;1"].
createInstance(Ci.nsIDOMParser);
parser.init();
let parsedDoc = parser.parseFromString(aHTML, "text/html");
let parsedDoc = parser.parseFromString(aContent, aType);
for (let element of parsedDoc.forms) {
this.mockOwnerDocumentProperty(element, parsedDoc, aDocumentURL);

View File

@ -0,0 +1,165 @@
/*
* Test the password manager context menu.
*/
"use strict";
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/LoginManagerContextMenu.jsm");
XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() {
return Services.strings.
createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
});
/**
* Prepare data for the following tests.
*/
add_task(function* test_initialize() {
for (let login of loginList()) {
Services.logins.addLogin(login);
}
});
/**
* Tests if the LoginManagerContextMenu returns the correct login items.
*/
add_task(function* test_contextMenuAddAndRemoveLogins() {
const DOCUMENT_CONTENT = "<form><input id='pw' type=password></form>";
const INPUT_QUERY = "input[type='password']";
let testHostnames = [
"http://www.example.com",
"http://www2.example.com",
"http://www3.example.com",
"http://empty.example.com",
];
for (let hostname of testHostnames) {
do_print("test for hostname: " + hostname);
// Get expected logins for this test.
let logins = getExpectedLogins(hostname);
// Create the logins menuitems fragment.
let {fragment, document} = createLoginsFragment(hostname, DOCUMENT_CONTENT, INPUT_QUERY);
if (!logins.length) {
Assert.ok(fragment === null, "Null returned. No logins where found.");
continue;
}
let items = [...fragment.querySelectorAll("menuitem")];
// Check if the items are those expected to be listed.
Assert.ok(checkLoginItems(logins, items), "All expected logins found.");
document.body.appendChild(fragment);
// Try to clear the fragment.
LoginManagerContextMenu.clearLoginsFromMenu(document);
Assert.equal(fragment.querySelectorAll("menuitem").length, 0, "All items correctly cleared.");
}
Services.logins.removeAllLogins();
});
/**
* Create a fragment with a menuitem for each login.
*/
function createLoginsFragment(url, content, elementQuery) {
const CHROME_URL = "chrome://mock-chrome";
// Create a mock document.
let document = MockDocument.createTestDocument(CHROME_URL, content);
let inputElement = document.querySelector(elementQuery);
MockDocument.mockOwnerDocumentProperty(inputElement, document, url);
// We also need a simple mock Browser object for this test.
let browser = {
ownerDocument: document
};
let URI = Services.io.newURI(url, null, null);
return {
document,
fragment: LoginManagerContextMenu.addLoginsToMenu(inputElement, browser, URI),
}
}
/**
* Check if every login have it's corresponding menuitem.
* Duplicates and empty usernames have a date appended.
*/
function checkLoginItems(logins, items) {
function findDuplicates(loginList) {
var seen = new Set();
var duplicates = new Set();
for (let login of loginList) {
if (seen.has(login.username)) {
duplicates.add(login.username);
}
seen.add(login.username);
}
return duplicates;
}
let duplicates = findDuplicates(logins);
let dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
{ day: "numeric", month: "short", year: "numeric" });
for (let login of logins) {
if (login.username && !duplicates.has(login.username)) {
// If login is not duplicate and we can't find an item for it, fail.
if (!items.find(item => item.label == login.username)) {
return false;
}
continue;
}
let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
let time = dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
// If login is duplicate, check if we have a login item with appended date.
if (login.username && !items.find(item => item.label == login.username + " (" + time + ")")) {
return false;
}
// If login is empty, check if we have a login item with appended date.
if (!login.username &&
!items.find(item => item.label == _stringBundle.GetStringFromName("noUsername") + " (" + time + ")")) {
return false;
}
}
return true;
}
/**
* Gets the list of expected logins for a hostname.
*/
function getExpectedLogins(hostname) {
return Services.logins.getAllLogins().filter(entry => entry["hostname"] === hostname);
}
function loginList() {
return [
new LoginInfo("http://www.example.com", "http://www.example.com", null,
"username1", "password",
"form_field_username", "form_field_password"),
new LoginInfo("http://www.example.com", "http://www.example.com", null,
"username2", "password",
"form_field_username", "form_field_password"),
new LoginInfo("http://www2.example.com", "http://www.example.com", null,
"username", "password",
"form_field_username", "form_field_password"),
new LoginInfo("http://www2.example.com", "http://www2.example.com", null,
"username", "password2",
"form_field_username", "form_field_password"),
new LoginInfo("http://www2.example.com", "http://www2.example.com", null,
"username2", "password2",
"form_field_username", "form_field_password"),
new LoginInfo("http://www3.example.com", "http://www.example.com", null,
"", "password",
"form_field_username", "form_field_password"),
new LoginInfo("http://www3.example.com", "http://www3.example.com", null,
"", "password2",
"form_field_username", "form_field_password"),
];
}

View File

@ -15,6 +15,7 @@ skip-if = os == "android"
skip-if = true || os != "android" # Bug 1171687: Needs fixing on Android
# The following tests apply to any storage back-end.
[test_context_menu.js]
[test_disabled_hosts.js]
[test_getFormFields.js]
[test_getPasswordFields.js]