Bug 1124900 - Add tests for keyboard navigation in the search panel. r=Mossop, a=test-only
authorFlorian Quèze <florian@queze.net>
Thu, 05 Feb 2015 00:08:19 +0100
changeset 249775 86222ca4b30912a4069265812cc80ecc2c32bfb0
parent 249774 e62dd2ded2fc5aa2c2ec1028defc85abc9e54807
child 249776 9ae4b7412921b5b60e01955365cdfd5e96cc0bd9
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMossop, test-only
bugs1124900
milestone37.0a2
Bug 1124900 - Add tests for keyboard navigation in the search panel. r=Mossop, a=test-only
browser/components/search/test/browser.ini
browser/components/search/test/browser_searchbar_keyboard_navigation.js
browser/components/search/test/browser_searchbar_openpopup.js
browser/components/search/test/head.js
--- a/browser/components/search/test/browser.ini
+++ b/browser/components/search/test/browser.ini
@@ -34,8 +34,9 @@ skip-if = e10s # Bug ?????? - some issue
 skip-if = e10s # Bug ?????? - Test uses load event and checks event.target.
 [browser_yahoo.js]
 [browser_yahoo_behavior.js]
 skip-if = e10s # Bug ?????? - some issue with progress listeners [JavaScript Error: "req.originalURI is null" {file: "chrome://mochitests/content/browser/browser/components/search/test/browser_bing_behavior.js" line: 127}]
 [browser_abouthome_behavior.js]
 skip-if = e10s || true # Bug ??????, Bug 1100301 - leaks windows until shutdown when --run-by-dir
 [browser_searchbar_openpopup.js]
 skip-if = os == "linux" || e10s || (os == "win" && os_version == "5.1" && debug) # Linux has different focus behaviours, e10s seems to have timing issues, Windows XP debug fails inexplicably for the test added in bug 1111947.
+[browser_searchbar_keyboard_navigation.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/search/test/browser_searchbar_keyboard_navigation.js
@@ -0,0 +1,332 @@
+// Tests that keyboard navigation in the search panel works as designed.
+
+const searchbar = document.getElementById("searchbar");
+const textbox = searchbar._textbox;
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+
+const kValues = ["foo1", "foo2", "foo3"];
+const kUserValue = "foo";
+
+// Get an array of the one-off buttons.
+function getOneOffs() {
+  let oneOffs = [];
+  let oneOff = document.getAnonymousElementByAttribute(searchPopup, "anonid",
+                                                       "search-panel-one-offs");
+  for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
+    if (oneOff.classList.contains("dummy"))
+      break;
+    oneOffs.push(oneOff);
+  }
+
+  return oneOffs;
+}
+
+add_task(function* init() {
+  yield promiseNewEngine("testEngine.xml");
+
+  // First cleanup the form history in case other tests left things there.
+  yield new Promise((resolve, reject) => {
+    info("cleanup the search history");
+    searchbar.FormHistory.update({op: "remove", fieldname: "searchbar-history"},
+                                 {handleCompletion: resolve,
+                                  handleError: reject});
+  });
+
+  yield new Promise((resolve, reject) => {
+    info("adding search history values: " + kValues);
+    let ops = kValues.map(value => { return {op: "add",
+                                             fieldname: "searchbar-history",
+                                             value: value}
+                                   });
+    searchbar.FormHistory.update(ops, {
+      handleCompletion: function() {
+        registerCleanupFunction(() => {
+          info("removing search history values: " + kValues);
+          let ops =
+            kValues.map(value => { return {op: "remove",
+                                           fieldname: "searchbar-history",
+                                           value: value}
+                                 });
+          searchbar.FormHistory.update(ops);
+        });
+        resolve();
+      },
+      handleError: reject
+    });
+  });
+
+  textbox.value = kUserValue;
+  registerCleanupFunction(() => { textbox.value = ""; });
+});
+
+
+add_task(function* test_arrows() {
+  let promise = promiseEvent(searchPopup, "popupshown");
+  info("Opening search panel");
+  searchbar.focus();
+  yield promise;
+  is(textbox.mController.searchString, kUserValue, "The search string should be 'foo'");
+
+  // Check the initial state of the panel before sending keyboard events.
+  is(searchPopup.view.rowCount, kValues.length, "There should be 3 suggestions");
+  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+  // The tests will be less meaningful if the first, second, last, and
+  // before-last one-off buttons aren't different. We should always have more
+  // than 4 default engines, but it's safer to check this assumption.
+  let oneOffs = getOneOffs();
+  ok(oneOffs.length >= 4, "we have at least 4 one-off buttons displayed")
+
+  ok(!textbox.getSelectedOneOff(), "no one-off button should be selected");
+
+  // The down arrow should first go through the suggestions.
+  for (let i = 0; i < kValues.length; ++i) {
+    EventUtils.synthesizeKey("VK_DOWN", {});
+    is(searchPopup.selectedIndex, i,
+       "the suggestion at index " + i + " should be selected");
+    is(textbox.value, kValues[i],
+       "the textfield value should be " + kValues[i]);
+  }
+
+  // Pressing down again should remove suggestion selection and change the text
+  // field value back to what the user typed, and select the first one-off.
+  EventUtils.synthesizeKey("VK_DOWN", {});
+  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+  is(textbox.value, kUserValue,
+     "the textfield value should be back to initial value");
+
+  // now cycle through the one-off items, the first one is already selected.
+  for (let i = 0; i < oneOffs.length; ++i) {
+    is(textbox.getSelectedOneOff(), oneOffs[i],
+       "the one-off button #" + (i + 1) + " should be selected");
+    EventUtils.synthesizeKey("VK_DOWN", {});
+  }
+
+  // We should now be back to the initial situation.
+  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+  ok(!textbox.getSelectedOneOff(), "no one-off button should be selected");
+
+  info("now test the up arrow key");
+  // cycle through the one-off items, the first one is already selected.
+  for (let i = oneOffs.length; i; --i) {
+    EventUtils.synthesizeKey("VK_UP", {});
+    is(textbox.getSelectedOneOff(), oneOffs[i - 1],
+       "the one-off button #" + i + " should be selected");
+  }
+
+  // Another press on up should clear the one-off selection and select the
+  // last suggestion.
+  EventUtils.synthesizeKey("VK_UP", {});
+  ok(!textbox.getSelectedOneOff(), "no one-off button should be selected");
+
+  for (let i = kValues.length - 1; i >= 0; --i) {
+    is(searchPopup.selectedIndex, i,
+       "the suggestion at index " + i + " should be selected");
+    is(textbox.value, kValues[i],
+       "the textfield value should be " + kValues[i]);
+    EventUtils.synthesizeKey("VK_UP", {});
+  }
+
+  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+  is(textbox.value, kUserValue,
+     "the textfield value should be back to initial value");
+});
+
+add_task(function* test_tab() {
+  is(Services.focus.focusedElement, textbox.inputField,
+     "the search bar should be focused"); // from the previous test.
+
+  let oneOffs = getOneOffs();
+  ok(!textbox.getSelectedOneOff(), "no one-off button should be selected");
+
+  // Pressing tab should select the first one-off without selecting suggestions.
+  // now cycle through the one-off items, the first one is already selected.
+  for (let i = 0; i < oneOffs.length; ++i) {
+    EventUtils.synthesizeKey("VK_TAB", {});
+    is(textbox.getSelectedOneOff(), oneOffs[i],
+       "the one-off button #" + (i + 1) + " should be selected");
+  }
+  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+  is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+
+  // Pressing tab again should close the panel...
+  let promise = promiseEvent(searchPopup, "popuphidden");
+  EventUtils.synthesizeKey("VK_TAB", {});
+  yield promise;
+
+  // ... and move the focus out of the searchbox.
+  isnot(Services.focus.focusedElement, textbox.inputField,
+        "the search bar no longer be focused");
+});
+
+add_task(function* test_shift_tab() {
+  // First reopen the panel.
+  let promise = promiseEvent(searchPopup, "popupshown");
+  info("Opening search panel");
+  searchbar.focus();
+  yield promise;
+
+  let oneOffs = getOneOffs();
+  ok(!textbox.getSelectedOneOff(), "no one-off button should be selected");
+
+  // Press up once to select the last one-off button.
+  EventUtils.synthesizeKey("VK_UP", {});
+
+  // Pressing shift+tab should cycle through the one-off items.
+  for (let i = oneOffs.length - 1; i >= 0; --i) {
+    is(textbox.getSelectedOneOff(), oneOffs[i],
+       "the one-off button #" + (i + 1) + " should be selected");
+    if (i)
+      EventUtils.synthesizeKey("VK_TAB", {shiftKey: true});
+  }
+  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+  is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+  // Pressing shift+tab again should close the panel...
+  promise = promiseEvent(searchPopup, "popuphidden");
+  EventUtils.synthesizeKey("VK_TAB", {shiftKey: true});
+  yield promise;
+
+  // ... and move the focus out of the searchbox.
+  isnot(Services.focus.focusedElement, textbox.inputField,
+        "the search bar no longer be focused");
+});
+
+add_task(function* test_alt_down() {
+  // First refocus the panel.
+  let promise = promiseEvent(searchPopup, "popupshown");
+  info("Opening search panel");
+  searchbar.focus();
+  yield promise;
+
+  // close the panel using the escape key.
+  promise = promiseEvent(searchPopup, "popuphidden");
+  EventUtils.synthesizeKey("VK_ESCAPE", {});
+  yield promise;
+
+  // check that alt+down opens the panel...
+  promise = promiseEvent(searchPopup, "popupshown");
+  EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
+  yield promise;
+
+  // ... and does nothing else.
+  ok(!textbox.getSelectedOneOff(), "no one-off button should be selected");
+  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+  is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+  // Pressing alt+down should select the first one-off without selecting suggestions
+  // and cycle through the one-off items.
+  let oneOffs = getOneOffs();
+  for (let i = 0; i < oneOffs.length; ++i) {
+    EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
+    is(textbox.getSelectedOneOff(), oneOffs[i],
+       "the one-off button #" + (i + 1) + " should be selected");
+    is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+  }
+
+  // One more alt+down keypress and nothing should be selected.
+  EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
+  ok(!textbox.getSelectedOneOff(), "no one-off button should be selected");
+
+  // another one and the first one-off should be selected.
+  EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
+  is(textbox.getSelectedOneOff(), oneOffs[0],
+     "the first one-off button should be selected");
+});
+
+add_task(function* test_alt_up() {
+  // close the panel using the escape key.
+  let promise = promiseEvent(searchPopup, "popuphidden");
+  EventUtils.synthesizeKey("VK_ESCAPE", {});
+  yield promise;
+
+  // check that alt+up opens the panel...
+  promise = promiseEvent(searchPopup, "popupshown");
+  EventUtils.synthesizeKey("VK_UP", {altKey: true});
+  yield promise;
+
+  // ... and does nothing else.
+  ok(!textbox.getSelectedOneOff(), "no one-off button should be selected");
+  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+  is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+  // Pressing alt+up should select the last one-off without selecting suggestions
+  // and cycle up through the one-off items.
+  let oneOffs = getOneOffs();
+  for (let i = oneOffs.length - 1; i >= 0; --i) {
+    EventUtils.synthesizeKey("VK_UP", {altKey: true});
+    is(textbox.getSelectedOneOff(), oneOffs[i],
+       "the one-off button #" + (i + 1) + " should be selected");
+    is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+  }
+
+  // One more alt+down keypress and nothing should be selected.
+  EventUtils.synthesizeKey("VK_UP", {altKey: true});
+  ok(!textbox.getSelectedOneOff(), "no one-off button should be selected");
+
+  // another one and the last one-off should be selected.
+  EventUtils.synthesizeKey("VK_UP", {altKey: true});
+  is(textbox.getSelectedOneOff(), oneOffs[oneOffs.length - 1],
+     "the last one-off button should be selected");
+
+  // Cleanup for the next test.
+  EventUtils.synthesizeKey("VK_DOWN", {});
+  ok(!textbox.getSelectedOneOff(), "no one-off should be selected anymore");
+});
+
+add_task(function* test_tab_and_arrows() {
+  // Check the initial state is as expected.
+  ok(!textbox.getSelectedOneOff(), "no one-off button should be selected");
+  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+  is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+  // After pressing down, the first sugggestion should be selected.
+  EventUtils.synthesizeKey("VK_DOWN", {});
+  is(searchPopup.selectedIndex, 0, "first suggestion should be selected");
+  is(textbox.value, kValues[0], "the textfield value should have changed");
+  ok(!textbox.getSelectedOneOff(), "no one-off button should be selected");
+
+  // After pressing tab, the first one-off should be selected,
+  // and the first suggestion still selected.
+  let oneOffs = getOneOffs();
+  EventUtils.synthesizeKey("VK_TAB", {});
+  is(textbox.getSelectedOneOff(), oneOffs[0],
+     "the first one-off button should be selected");
+  is(searchPopup.selectedIndex, 0, "first suggestion should still be selected");
+
+  // After pressing down, the second suggestion should be selected,
+  // and the first one-off still selected.
+  EventUtils.synthesizeKey("VK_DOWN", {});
+  is(textbox.getSelectedOneOff(), oneOffs[0],
+     "the first one-off button should still be selected");
+  is(searchPopup.selectedIndex, 1, "second suggestion should be selected");
+
+  // After pressing up, the first suggestion should be selected again,
+  // and the first one-off still selected.
+  EventUtils.synthesizeKey("VK_UP", {});
+  is(textbox.getSelectedOneOff(), oneOffs[0],
+     "the first one-off button should still be selected");
+  is(searchPopup.selectedIndex, 0, "second suggestion should be selected again");
+
+  // After pressing up again, we should have no suggestion selected anymore,
+  // the textfield value back to the user-typed value, and still the first one-off
+  // selected.
+  EventUtils.synthesizeKey("VK_UP", {});
+  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+  is(textbox.value, kUserValue,
+     "the textfield value should be back to user typed value");
+  is(textbox.getSelectedOneOff(), oneOffs[0],
+     "the first one-off button should still be selected");
+
+  // Now pressing down should select the second one-off.
+  EventUtils.synthesizeKey("VK_DOWN", {});
+  is(textbox.getSelectedOneOff(), oneOffs[1],
+     "the second one-off button should be selected");
+  is(searchPopup.selectedIndex, -1, "there should still be no selected suggestion");
+
+  // Finally close the panel.
+  let promise = promiseEvent(searchPopup, "popuphidden");
+  searchPopup.hidePopup();
+  yield promise;
+});
--- a/browser/components/search/test/browser_searchbar_openpopup.js
+++ b/browser/components/search/test/browser_searchbar_openpopup.js
@@ -26,44 +26,16 @@ function synthesizeNativeMouseClick(aEle
   let win = aElement.ownerDocument.defaultView;
   let x = win.mozInnerScreenX + (rect.left + rect.right) / 2;
   let y = win.mozInnerScreenY + (rect.top + rect.bottom) / 2;
 
   utils.sendNativeMouseEvent(x * scale, y * scale, mouseDown, 0, null);
   utils.sendNativeMouseEvent(x * scale, y * scale, mouseUp, 0, null);
 }
 
-function promiseNewEngine(basename) {
-  return new Promise((resolve, reject) => {
-    info("Waiting for engine to be added: " + basename);
-    Services.search.init({
-      onInitComplete: function() {
-        let url = getRootDirectory(gTestPath) + basename;
-        let current = Services.search.currentEngine;
-        Services.search.addEngine(url, Ci.nsISearchEngine.TYPE_MOZSEARCH, "", false, {
-          onSuccess: function (engine) {
-            info("Search engine added: " + basename);
-            Services.search.currentEngine = engine;
-            registerCleanupFunction(() => {
-              Services.search.currentEngine = current;
-              Services.search.removeEngine(engine);
-              info("Search engine removed: " + basename);
-            });
-            resolve(engine);
-          },
-          onError: function (errCode) {
-            ok(false, "addEngine failed with error code " + errCode);
-            reject();
-          }
-        });
-      }
-    });
-  });
-}
-
 add_task(function* init() {
   yield promiseNewEngine("testEngine.xml");
 });
 
 // Adds a task that shouldn't show the search suggestions popup.
 function add_no_popup_task(task) {
   add_task(function*() {
     let sawPopup = false;
--- a/browser/components/search/test/head.js
+++ b/browser/components/search/test/head.js
@@ -131,8 +131,35 @@ function* promiseOnLoad() {
         info("onLoadListener: " + aEvent.originalTarget.location);
         gBrowser.removeEventListener("load", onLoadListener, true);
         resolve(aEvent);
       }
     }, true);
   });
 }
 
+function promiseNewEngine(basename) {
+  return new Promise((resolve, reject) => {
+    info("Waiting for engine to be added: " + basename);
+    Services.search.init({
+      onInitComplete: function() {
+        let url = getRootDirectory(gTestPath) + basename;
+        let current = Services.search.currentEngine;
+        Services.search.addEngine(url, Ci.nsISearchEngine.TYPE_MOZSEARCH, "", false, {
+          onSuccess: function (engine) {
+            info("Search engine added: " + basename);
+            Services.search.currentEngine = engine;
+            registerCleanupFunction(() => {
+              Services.search.currentEngine = current;
+              Services.search.removeEngine(engine);
+              info("Search engine removed: " + basename);
+            });
+            resolve(engine);
+          },
+          onError: function (errCode) {
+            ok(false, "addEngine failed with error code " + errCode);
+            reject();
+          }
+        });
+      }
+    });
+  });
+}