Bug 433238 - Tests for the password manager contextual menu password fill. r=MattN
authorBernardo P. Rittmeyer <bernardo@rittme.com>
Thu, 06 Aug 2015 18:45:44 -0700
changeset 256860 96f128a9b4656aee393a3d022bb9d40a2a8c0384
parent 256859 02ff02df924ad184ec843ac0d9677cb38a95cba8
child 256861 d0a1ddd01c1e2b6e67bce8272b0a512d43667293
push id29191
push userkwierso@gmail.com
push dateSat, 08 Aug 2015 00:10:29 +0000
treeherdermozilla-central@a5bde89f6829 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs433238
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 433238 - Tests for the password manager contextual menu password fill. r=MattN
browser/base/content/test/general/contextmenu_common.js
browser/base/content/test/general/test_contextmenu.html
browser/base/content/test/general/test_contextmenu_input.html
toolkit/components/passwordmgr/test/browser/browser.ini
toolkit/components/passwordmgr/test/browser/browser_context_menu.js
toolkit/components/passwordmgr/test/browser/multiple_forms.html
toolkit/components/passwordmgr/test/unit/head.js
toolkit/components/passwordmgr/test/unit/test_context_menu.js
toolkit/components/passwordmgr/test/unit/xpcshell.ini
--- a/browser/base/content/test/general/contextmenu_common.js
+++ b/browser/base/content/test/general/contextmenu_common.js
@@ -56,17 +56,19 @@ function getVisibleMenuItems(aMenu, aDat
             ok(label.length, "menuitem " + item.id + " has a label");
             if (isSpellSuggestion) {
               is(key, "", "Spell suggestions shouldn't have an access key");
               items.push("*" + label);
             } else if (isGenerated) {
               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]);
               else
                   accessKeys[key] = item.id;
             }
             if (!isSpellSuggestion && !isGenerated) {
               items.push(item.id);
--- a/browser/base/content/test/general/test_contextmenu.html
+++ b/browser/base/content/test/general/test_contextmenu.html
@@ -75,16 +75,26 @@ function runTest(testNum) {
   ok(true, "Starting test #" + testNum);
 
   var inspectItems = [];
   if (SpecialPowers.getBoolPref("devtools.inspector.enabled")) {
     inspectItems = ["---", null,
                     "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.
         openContextMenuFor(text);
     },
 
     function () {
         // Context menu for plain text
@@ -660,17 +670,18 @@ function runTest(testNum) {
                           "context-selectall",   true,
                           "---",                 null,
                           "spell-check-enabled", true,
                           //spell checker is shown on input[type="password"] on this testcase
                           "spell-dictionaries",  true,
                               ["spell-check-dictionary-en-US", true,
                                "---",                          null,
                                "spell-add-dictionaries",       true], null
-                         ].concat(inspectItems));
+                         ].concat(passwordFillItems)
+                          .concat(inspectItems));
         closeContextMenu();
         subwindow.getSelection().removeAllRanges();
         openContextMenuFor(plugin);
     },
 
     function () {
         // Context menu for click-to-play blocked plugin
         checkContextMenu(["context-navigation", null,
--- a/browser/base/content/test/general/test_contextmenu_input.html
+++ b/browser/base/content/test/general/test_contextmenu_input.html
@@ -162,16 +162,21 @@ function runTest(testNum) {
                           "---",                 null,
                           "context-cut",         true,
                           "context-copy",        true,
                           "context-paste",       null, // ignore clipboard state
                           "context-delete",      false,
                           "---",                 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();
 
         input.type = 'email';
 
         openContextMenuFor(input); // Invoke context menu for next test.
         break;
--- a/toolkit/components/passwordmgr/test/browser/browser.ini
+++ b/toolkit/components/passwordmgr/test/browser/browser.ini
@@ -1,18 +1,20 @@
 [DEFAULT]
 support-files =
   authenticate.sjs
   form_basic.html
+  multiple_forms.html
 
 [browser_DOMFormHasPassword.js]
 [browser_DOMInputPasswordAdded.js]
 [browser_filldoorhanger.js]
 [browser_notifications.js]
 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]
 [browser_passwordmgr_switchtab.js]
 [browser_passwordmgrcopypwd.js]
 [browser_passwordmgrdlg.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu.js
@@ -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",
+    }),
+  ];
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/multiple_forms.html
@@ -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>
--- a/toolkit/components/passwordmgr/test/unit/head.js
+++ b/toolkit/components/passwordmgr/test/unit/head.js
@@ -124,21 +124,21 @@ const RecipeHelpers = {
     return (new LoginRecipesParent({ defaults: null })).initializationPromise;
   },
 };
 
 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);
     }
     return parsedDoc;
   },
 
   mockOwnerDocumentProperty(aElement, aDoc, aURL) {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_context_menu.js
@@ -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"),
+  ];
+}
\ No newline at end of file
--- a/toolkit/components/passwordmgr/test/unit/xpcshell.ini
+++ b/toolkit/components/passwordmgr/test/unit/xpcshell.ini
@@ -10,16 +10,17 @@ skip-if = os == "android"
 [test_module_LoginStore.js]
 skip-if = os == "android"
 
 # Test SQLite database backup and migration, applicable to Android only.
 [test_storage_mozStorage.js]
 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]
 [test_legacy_empty_formSubmitURL.js]
 [test_legacy_validation.js]
 [test_logins_change.js]
 [test_logins_decrypt_failure.js]
 skip-if = os == "android" # Bug 1171687: Needs fixing on Android