Bug 1419091: Add keybinding tests. r=masayuki
☠☠ backed out by 2efb7ae0315f ☠ ☠
authorDave Townsend <dtownsend@oxymoronical.com>
Thu, 20 Sep 2018 13:10:35 +0000
changeset 493492 d432e26bd0b5ac9e3c3877592616fb7110b52142
parent 493491 575545bc2b10c7e06d89ef687bc2f6c1e7fa2cc5
child 493493 2efb7ae0315fa250f001d82a994e3eae60f36857
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmasayuki
bugs1419091
milestone64.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 1419091: Add keybinding tests. r=masayuki Differential Revision: https://phabricator.services.mozilla.com/D3030
dom/tests/mochitest/keyhandling/browsertest.html
dom/tests/mochitest/keyhandling/chrome.ini
dom/tests/mochitest/keyhandling/mochitest.ini
dom/tests/mochitest/keyhandling/test_browser.xul
dom/tests/mochitest/keyhandling/test_editor.xul
dom/tests/mochitest/keyhandling/test_input.html
dom/tests/mochitest/keyhandling/test_textarea.html
dom/tests/moz.build
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/keyhandling/browsertest.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<p style="white-space: nowrap">
+  A long paragraph to make a horizontal scrollbar.
+  A long paragraph to make a horizontal scrollbar.
+  A long paragraph to make a horizontal scrollbar.
+  A long paragraph to make a horizontal scrollbar.
+  A long paragraph to make a horizontal scrollbar.
+  A long paragraph to make a horizontal scrollbar.
+  A long paragraph to make a horizontal scrollbar.
+  A long paragraph to make a horizontal scrollbar.
+  A long paragraph to make a horizontal scrollbar.
+  A long paragraph to make a horizontal scrollbar.
+</p>
+<p id="paragraph">Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+<p>Lots of paragraphs to make a vertical scrollbar.</p>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/keyhandling/chrome.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+skip-if = os == 'linux' # nsIWidget::SynthesizeNativeKeyEvent() required
+
+[test_browser.xul]
+support-files =
+  browsertest.html
+[test_editor.xul]
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/keyhandling/mochitest.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+skip-if = os == 'linux' # nsIWidget::SynthesizeNativeKeyEvent() required
+
+[test_input.html]
+[test_textarea.html]
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/keyhandling/test_browser.xul
@@ -0,0 +1,255 @@
+<?xml version="1.0"?>
+
+<window title="Browser element keyhandling tests"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        onload="test();">
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js"/>
+
+  <script type="application/javascript">
+  <![CDATA[
+    SimpleTest.waitForExplicitFinish();
+
+    const IS_MAC = navigator.platform.indexOf("Mac") === 0;
+    const VK = {};
+    const CHARS = {};
+
+    // Copied values from NativeKeyCodes.js and EventUtils.js
+    if (IS_MAC) {
+      VK.LEFT = MAC_VK_LeftArrow;
+      CHARS.LEFT = "\uF702";
+      VK.RIGHT = MAC_VK_RightArrow;
+      CHARS.RIGHT = "\uF703";
+      VK.UP = MAC_VK_UpArrow;
+      CHARS.UP = "\uF700";
+      VK.DOWN = MAC_VK_DownArrow;
+      CHARS.DOWN = "\uF701";
+      VK.SPACE = MAC_VK_Space;
+      VK.PGDOWN = MAC_VK_PageDown;
+      CHARS.PGDOWN = "\uF72D";
+      VK.PGUP = MAC_VK_PageUp;
+      CHARS.PGUP = "\uF72C";
+      VK.C = MAC_VK_ANSI_C;
+      VK.HOME = MAC_VK_Home;
+      CHARS.HOME = "\uF729";
+      VK.END = MAC_VK_End;
+      CHARS.END = "\uF72B";
+    } else {
+      VK.LEFT = WIN_VK_LEFT;
+      CHARS.LEFT = "";
+      VK.RIGHT = WIN_VK_RIGHT;
+      CHARS.RIGHT = "";
+      VK.UP = WIN_VK_UP;
+      CHARS.UP = "";
+      VK.DOWN = WIN_VK_DOWN;
+      CHARS.DOWN = "";
+      VK.SPACE = WIN_VK_SPACE;
+      VK.PGDOWN = WIN_VK_NEXT;
+      CHARS.PGDOWN = "";
+      VK.PGUP = WIN_VK_PRIOR;
+      CHARS.PGUP = "";
+      VK.C = WIN_VK_C;
+      VK.HOME = WIN_VK_HOME;
+      CHARS.HOME = "";
+      VK.END = WIN_VK_END;
+      CHARS.END = "";
+    }
+
+    function waitForEvent(target, event) {
+      info(`Waiting for ${event} event.`);
+      return new Promise(resolve => {
+        browser.addEventListener(event, resolve, { once: true });
+      });
+    }
+
+    function synthesizeKey(keyCode, modifiers, chars) {
+      return new Promise((resolve, reject) => {
+        if (!synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, keyCode, modifiers, chars, chars, resolve)) {
+          reject();
+        }
+      });
+    }
+
+    function getWindowProperties(browser, properties) {
+      let results = {};
+      for (let prop of properties) {
+        results[prop] = browser.contentWindow[prop];
+      }
+
+      return results;
+    }
+
+    function getScrollPosition(browser) {
+      return getWindowProperties(browser, ["scrollX", "scrollY"]);
+    }
+
+    async function test() {
+      // Smooth scrolling makes scroll events take time and it's difficult to know
+      // when they've ended, so turn it off for this test.
+      await SpecialPowers.pushPrefEnv({"set": [["general.smoothScroll", false]] });
+
+      let browser = document.getElementById("browser");
+      browser.focus();
+      let { scrollX, scrollY } = await getScrollPosition(browser);
+      is(scrollX, 0, "Should not be scrolled");
+      is(scrollY, 0, "Should not be scrolled");
+
+      info("down");
+      await synthesizeKey(VK.DOWN, {}, CHARS.DOWN);
+      await waitForEvent(browser.contentWindow, "scroll");
+      let { scrollX: lineScrollX, scrollY: lineScrollY } = await getScrollPosition(browser);
+      is(lineScrollX, 0, "Should not be scrolled");
+      ok(lineScrollY > 0, "Should be scrolled");
+
+      info("up");
+      await synthesizeKey(VK.UP, {}, CHARS.UP);
+      await waitForEvent(browser.contentWindow, "scroll");
+      {
+        let { scrollX, scrollY } = await getScrollPosition(browser);
+        is(scrollX, 0, "Should not be scrolled");
+        is(scrollY, 0, "Should not be scrolled");
+      }
+
+      info("right");
+      await synthesizeKey(VK.RIGHT, {}, CHARS.RIGHT);
+      await waitForEvent(browser.contentWindow, "scroll");
+      let { scrollX: rightScrollX, scrollY: rightScrollY } = await getScrollPosition(browser);
+      ok(rightScrollX > 0, "Should be scrolled");
+      is(rightScrollY, 0, "Should not be scrolled");
+
+      info("left");
+      await synthesizeKey(VK.LEFT, {}, CHARS.LEFT);
+      await waitForEvent(browser.contentWindow, "scroll");
+      {
+        let { scrollX, scrollY } = await getScrollPosition(browser);
+        is(scrollX, 0, "Should not be scrolled");
+        is(scrollY, 0, "Should not be scrolled");
+      }
+
+      info("space");
+      await synthesizeKey(VK.SPACE, {}, " ");
+      await waitForEvent(browser.contentWindow, "scroll");
+      let { scrollX: pageScrollX, scrollY: pageScrollY } = await getScrollPosition(browser);
+      is(pageScrollX, 0, "Should not be scrolled");
+      ok(pageScrollY > lineScrollY, "Should be scrolled more than a single line");
+
+      info("shift+space");
+      await synthesizeKey(VK.SPACE, { shiftKey: true }, " ");
+      await waitForEvent(browser.contentWindow, "scroll");
+      {
+        let { scrollX, scrollY } = await getScrollPosition(browser);
+        is(scrollX, 0, "Should not be scrolled");
+        is(scrollY, 0, "Should not be scrolled");
+      }
+
+      info("page down");
+      await synthesizeKey(VK.PGDOWN, {}, CHARS.PGDOWN);
+      await waitForEvent(browser.contentWindow, "scroll");
+      {
+        let { scrollX, scrollY } = await getScrollPosition(browser);
+        is(scrollX, 0, "Should not be scrolled");
+        is(scrollY, pageScrollY, "Should be scrolled a page");
+      }
+
+      info("page up");
+      await synthesizeKey(VK.PGUP, {}, CHARS.PGUP);
+      await waitForEvent(browser.contentWindow, "scroll");
+      {
+        let { scrollX, scrollY } = await getScrollPosition(browser);
+        is(scrollX, 0, "Should not be scrolled");
+        is(scrollY, 0, "Should not be scrolled");
+      }
+
+      info("accel+down");
+      await synthesizeKey(VK.DOWN, { accelKey: true }, CHARS.DOWN);
+      await waitForEvent(browser.contentWindow, "scroll");
+      {
+        let { scrollX, scrollY, innerHeight } = await getWindowProperties(browser, ["scrollX", "scrollY", "innerHeight"]);
+        is(scrollX, 0, "Should not be scrolled");
+        // We can't know the scrollbar height so check that we're scrolled to within 100px of what we expect.
+        isfuzzy(scrollY, browser.contentDocument.body.clientHeight - innerHeight, 100, "Should be scrolled to the end.");
+      }
+
+      info("accel+up");
+      await synthesizeKey(VK.UP, { accelKey: true }, CHARS.UP);
+      await waitForEvent(browser.contentWindow, "scroll");
+      {
+        let { scrollX, scrollY } = await getScrollPosition(browser);
+        is(scrollX, 0, "Should not be scrolled");
+        is(scrollY, 0, "Should not be scrolled");
+      }
+
+      info("end");
+      await synthesizeKey(VK.END, {}, CHARS.END);
+      await waitForEvent(browser.contentWindow, "scroll");
+      {
+        let { scrollX, scrollY, innerHeight } = await getWindowProperties(browser, ["scrollX", "scrollY", "innerHeight"]);
+        is(scrollX, 0, "Should not be scrolled");
+        // We can't know the scrollbar height so check that we're scrolled to within 100px of what we expect.
+        isfuzzy(scrollY, browser.contentDocument.body.clientHeight - innerHeight, 100, "Should be scrolled to the end.");
+      }
+
+      info("home");
+      await synthesizeKey(VK.HOME, {}, CHARS.HOME);
+      await waitForEvent(browser.contentWindow, "scroll");
+      {
+        let { scrollX, scrollY } = await getScrollPosition(browser);
+        is(scrollX, 0, "Should not be scrolled");
+        is(scrollY, 0, "Should not be scrolled");
+      }
+
+      // Select the start of the first paragraph
+      let paragraph = browser.contentDocument.getElementById("paragraph");
+      let selection = browser.contentWindow.getSelection();
+      selection.setBaseAndExtent(paragraph.firstChild, 0, paragraph.firstChild, "Lots of".length);
+
+      info("copy");
+      await SimpleTest.promiseClipboardChange("Lots of", () => {
+        synthesizeKey(VK.C, { accelKey: true }, "c");
+      });
+
+      for (let i = 0; i < " paragraphs".length; i++) {
+        info("select right");
+        await synthesizeKey(VK.RIGHT, { shiftKey: true }, CHARS.RIGHT);
+      }
+
+      info("copy");
+      await SimpleTest.promiseClipboardChange("Lots of paragraphs", () => {
+        synthesizeKey(VK.C, { accelKey: true }, "c");
+      });
+
+      for (let i = 0; i < " paragraphs".length; i++) {
+        info("select left");
+        await synthesizeKey(VK.LEFT, { shiftKey: true }, CHARS.LEFT);
+      }
+
+      info("copy");
+      await SimpleTest.promiseClipboardChange("Lots of", () => {
+        synthesizeKey(VK.C, { accelKey: true }, "c");
+      });
+
+      info("select down");
+      await synthesizeKey(VK.DOWN, { shiftKey: true }, CHARS.DOWN);
+
+      info("copy");
+      await SimpleTest.promiseClipboardChange("Lots of paragraphs to make a vertical scrollbar.\n\nLots of", () => {
+        synthesizeKey(VK.C, { accelKey: true }, "c");
+      });
+
+      SimpleTest.finish();
+    }
+  ]]>
+  </script>
+
+  <body xmlns="http://www.w3.org/1999/xhtml">
+    <p id="display"></p>
+    <div id="content" style="display:none;"></div>
+    <pre id="test"></pre>
+  </body>
+  <browser id="browser" src="browsertest.html" style="height: 500px"/>
+</window>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/keyhandling/test_editor.xul
@@ -0,0 +1,256 @@
+<?xml version="1.0"?>
+
+<window title="Browser element keyhandling tests"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        onload="test();">
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js"/>
+
+  <script type="application/javascript">
+  <![CDATA[
+    SimpleTest.waitForExplicitFinish();
+
+    const IS_MAC = navigator.platform.indexOf("Mac") === 0;
+    const VK = {};
+    const CHARS = {};
+
+    // Copied values from NativeKeyCodes.js and EventUtils.js
+    if (IS_MAC) {
+      VK.LEFT = MAC_VK_LeftArrow;
+      CHARS.LEFT = "\uF702";
+      VK.RIGHT = MAC_VK_RightArrow;
+      CHARS.RIGHT = "\uF703";
+      VK.UP = MAC_VK_UpArrow;
+      CHARS.UP = "\uF700";
+      VK.DOWN = MAC_VK_DownArrow;
+      CHARS.DOWN = "\uF701";
+      VK.SPACE = MAC_VK_Space;
+      VK.X = MAC_VK_ANSI_X;
+      VK.V = MAC_VK_ANSI_V;
+      VK.A = MAC_VK_ANSI_A;
+      VK.Z = MAC_VK_ANSI_Z;
+      VK.BACKSPACE = MAC_VK_PC_Backspace;
+      CHARS.BACKSPACE = "\u007F";
+    } else {
+      VK.LEFT = WIN_VK_LEFT;
+      CHARS.LEFT = "";
+      VK.RIGHT = WIN_VK_RIGHT;
+      CHARS.RIGHT = "";
+      VK.HOME = WIN_VK_HOME;
+      CHARS.HOME = "";
+      VK.END = WIN_VK_END;
+      CHARS.END = "";
+      VK.SPACE = WIN_VK_SPACE;
+      VK.X = WIN_VK_X;
+      VK.V = WIN_VK_V;
+      VK.A = WIN_VK_A;
+      VK.Z = WIN_VK_Z;
+      VK.Y = WIN_VK_Y;
+      VK.BACKSPACE = WIN_VK_BACK;
+      CHARS.BACKSPACE = "";
+    }
+
+    function waitForEvent(target, event) {
+      info(`Waiting for ${event} event.`);
+      return new Promise(resolve => {
+        browser.addEventListener(event, resolve, { once: true });
+      });
+    }
+
+    function synthesizeKey(keyCode, modifiers, chars) {
+      return new Promise((resolve, reject) => {
+        if (!synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, keyCode, modifiers, chars, chars, resolve)) {
+          reject();
+        }
+      });
+    }
+
+    function* nodes(element) {
+      let node = element.firstChild;
+      while (node) {
+        yield node;
+
+        if (node.nodeType === Node.ELEMENT_NODE) {
+          yield* nodes(node);
+        }
+
+        node = node.nextSibling;
+      }
+    }
+
+    async function checkElement(element, start, selectedText, content = "Test text") {
+      selectionPosition = (element, range) => {
+        let pos = 0;
+        for (let node of nodes(element)) {
+          if (node.nodeType === Node.TEXT_NODE) {
+            if (node === range.startContainer) {
+              return pos + range.startOffset;
+            } else {
+              pos += node.nodeValue.length;
+            }
+          } else if (node === range.startContainer) {
+            for (let i = 0; i < range.startOffset; i++) {
+              pos += node.childNodes[i].textContent.length;
+            }
+
+            return pos;
+          }
+        }
+
+        throw new Error("startContainer of range never found.");
+      }
+
+      isReady = () => {
+        let selection = element.contentWindow.getSelection();
+        let range = selection.getRangeAt(0);
+        let pos = selectionPosition(element.contentDocument.documentElement, range);
+
+        if (start != pos) {
+          return false;
+        }
+        if (selectedText != selection.toString()) {
+          return false;
+        }
+        if (content != element.contentDocument.documentElement.textContent) {
+          return false;
+        }
+        return true;
+      };
+
+      for (let i = 0; i < 10; i++) {
+        if (isReady()) {
+          return;
+        }
+
+        SimpleTest.requestFlakyTimeout("Polling for changes to apply");
+        await new Promise(resolve => setTimeout(resolve, 50));
+      }
+      ok(false, `Timed out waiting for state ${start} "${selectedText}" "${content}"`);
+      let selection = element.contentWindow.getSelection();
+      let range = selection.getRangeAt(0);
+      info(`${selectionPosition(element.contentDocument.documentElement, range)} "${selection.toString()}" "${element.contentDocument.documentElement.textContent}"`);
+    }
+
+    async function test() {
+      let editor = document.getElementById("editor");
+      editor.contentDocument.designMode = "on";
+      editor.contentWindow.focus();
+      let edit = editor.getEditor(editor.contentWindow);
+      edit.beginningOfDocument();
+
+      await checkElement(editor, 0, "");
+
+      info("right");
+      await synthesizeKey(VK.RIGHT, {}, CHARS.RIGHT);
+      await checkElement(editor, 1, "");
+
+      info("shift+right");
+      await synthesizeKey(VK.RIGHT, { shiftKey: true }, CHARS.RIGHT);
+      await checkElement(editor, 1, "e");
+
+      info("shift+right");
+      await synthesizeKey(VK.RIGHT, { shiftKey: true }, CHARS.RIGHT);
+      await checkElement(editor, 1, "es");
+
+      info("shift+left");
+      await synthesizeKey(VK.LEFT, { shiftKey: true }, CHARS.LEFT);
+      await checkElement(editor, 1, "e");
+
+      info("shift+left");
+      await synthesizeKey(VK.LEFT, { shiftKey: true }, CHARS.LEFT);
+      await checkElement(editor, 1, "");
+
+      info("shift+left");
+      await synthesizeKey(VK.LEFT, { shiftKey: true }, CHARS.LEFT);
+      await checkElement(editor, 0, "T");
+
+      info("shift+right");
+      await synthesizeKey(VK.RIGHT, { shiftKey: true }, CHARS.RIGHT);
+      await checkElement(editor, 1, "");
+
+      info("left");
+      await synthesizeKey(VK.LEFT, {}, CHARS.LEFT);
+      await checkElement(editor, 0, "");
+
+      if (IS_MAC) {
+        info("down");
+        await synthesizeKey(VK.DOWN, { shiftKey: true }, CHARS.DOWN);
+      } else {
+        info("end");
+        await synthesizeKey(VK.END, { shiftKey: true }, CHARS.END);
+      }
+      await checkElement(editor, 0, "Test text");
+
+      info("cut");
+      await synthesizeKey(VK.X, { accelKey: true }, "x");
+      await checkElement(editor, 0, "", "");
+      let text = SpecialPowers.getClipboardData("text/unicode");
+      is(text, "Test text", "Should have cut to the clipboard");
+      SpecialPowers.clipboardCopyString("New text");
+
+      info("paste");
+      await synthesizeKey(VK.V, { accelKey: true }, "v");
+      await checkElement(editor, 8, "", "New text");
+
+      if (IS_MAC) {
+        info("up");
+        await synthesizeKey(VK.UP, {}, CHARS.UP);
+      } else {
+        info("home");
+        await synthesizeKey(VK.HOME, {}, CHARS.HOME);
+      }
+      await checkElement(editor, 0, "", "New text");
+
+      info("select all");
+      await synthesizeKey(VK.A, { accelKey: true}, "a", "select");
+      await checkElement(editor, 0, "New text", "New text");
+
+      info("right");
+      await synthesizeKey(VK.RIGHT, {}, CHARS.RIGHT);
+      await checkElement(editor, 8, "", "New text");
+
+      info("word left");
+      if (IS_MAC) {
+        await synthesizeKey(VK.LEFT, { altKey: true }, CHARS.LEFT);
+      } else {
+        await synthesizeKey(VK.LEFT, { ctrlKey: true }, CHARS.LEFT);
+      }
+      await checkElement(editor, 4, "", "New text");
+
+      info("delete word left");
+      if (IS_MAC) {
+        await synthesizeKey(VK.BACKSPACE, { altKey: true }, CHARS.BACKSPACE);
+      } else {
+        await synthesizeKey(VK.BACKSPACE, { ctrlKey: true }, CHARS.BACKSPACE);
+      }
+      await checkElement(editor, 0, "", "text");
+
+      info("undo");
+      await synthesizeKey(VK.Z, { accelKey: true }, "z");
+      await checkElement(editor, 4, "", "New text");
+
+      info("redo");
+      if (IS_MAC) {
+        await synthesizeKey(VK.Z, { accelKey: true, shiftKey: true }, "z");
+      } else {
+        await synthesizeKey(VK.Y, { accelKey: true }, "y");
+      }
+      await checkElement(editor, 0, "", "text");
+
+      SimpleTest.finish();
+    }
+  ]]>
+  </script>
+
+  <body xmlns="http://www.w3.org/1999/xhtml">
+    <p id="display"></p>
+    <div id="content" style="display:none;"></div>
+    <pre id="test"></pre>
+  </body>
+  <editor id="editor" editortype="text" src="data:text/plain,Test text" style="height: 500px"/>
+</window>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/keyhandling/test_input.html
@@ -0,0 +1,193 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>input key handling</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="/tests/SimpleTest/NativeKeyCodes.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+<script type="text/javascript">
+const IS_MAC = navigator.platform.indexOf("Mac") == 0;
+const VK = {};
+const CHARS = {};
+
+if (IS_MAC) {
+  VK.LEFT = MAC_VK_LeftArrow;
+  CHARS.LEFT = "\uF702";
+  VK.RIGHT = MAC_VK_RightArrow;
+  CHARS.RIGHT = "\uF703";
+  VK.UP = MAC_VK_UpArrow;
+  CHARS.UP = "\uF700";
+  VK.DOWN = MAC_VK_DownArrow;
+  CHARS.DOWN = "\uF701";
+  VK.X = MAC_VK_ANSI_X;
+  VK.V = MAC_VK_ANSI_V;
+  VK.A = MAC_VK_ANSI_A;
+  VK.BACKSPACE = MAC_VK_PC_Backspace;
+  CHARS.BACKSPACE = "\u007F";
+  VK.Z = MAC_VK_ANSI_Z;
+} else {
+  VK.LEFT = WIN_VK_LEFT;
+  CHARS.LEFT = "";
+  VK.RIGHT = WIN_VK_RIGHT;
+  CHARS.RIGHT = "";
+  VK.UP = WIN_VK_UP;
+  CHARS.UP = "";
+  VK.DOWN = WIN_VK_DOWN;
+  CHARS.DOWN = "";
+  VK.X = WIN_VK_X;
+  VK.V = WIN_VK_V;
+  VK.A = WIN_VK_A;
+  VK.END = WIN_VK_END;
+  CHARS.END = "";
+  VK.HOME = WIN_VK_HOME;
+  CHARS.HOME = "";
+  VK.BACKSPACE = WIN_VK_BACK;
+  CHARS.BACKSPACE = "";
+  VK.Z = WIN_VK_Z;
+}
+
+function synthesizeKey(keyCode, modifiers, chars, event = "keyup") {
+  return new Promise((resolve, reject) => {
+    window.addEventListener(event, resolve, { once: true });
+
+    if (!synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, keyCode, modifiers, chars, chars)) {
+      reject();
+    }
+  });
+}
+
+async function checkElement(element, start, end, content = "Test text") {
+  isReady = () => {
+    if (start != element.selectionStart) {
+      return false;
+    }
+    if (end != element.selectionEnd) {
+      return false;
+    }
+    if (content != element.value) {
+      return false;
+    }
+    return true;
+  };
+
+  for (let i = 0; i < 10; i++) {
+    if (isReady()) {
+      return;
+    }
+    
+    SimpleTest.requestFlakyTimeout("Polling for changes to apply");
+    await new Promise(resolve => setTimeout(resolve, 50));
+  }
+  ok(false, `Timed out waiting for state ${start} ${end} ${content}`);
+}
+
+async function runTest() {
+  let input = document.getElementById("input");
+  input.focus();
+  await checkElement(input, 0, 0);
+
+  info("right");
+  await synthesizeKey(VK.RIGHT, {}, CHARS.RIGHT);
+  await checkElement(input, 1, 1);
+
+  info("shift+right");
+  await synthesizeKey(VK.RIGHT, { shiftKey: true }, CHARS.RIGHT);
+  await checkElement(input, 1, 2);
+
+  info("shift+right");
+  await synthesizeKey(VK.RIGHT, { shiftKey: true }, CHARS.RIGHT);
+  await checkElement(input, 1, 3);
+
+  info("shift+left");
+  await synthesizeKey(VK.LEFT, { shiftKey: true }, CHARS.LEFT);
+  await checkElement(input, 1, 2);
+
+  info("shift+left");
+  await synthesizeKey(VK.LEFT, { shiftKey: true }, CHARS.LEFT);
+  await checkElement(input, 1, 1);
+
+  info("shift+left");
+  await synthesizeKey(VK.LEFT, { shiftKey: true }, CHARS.LEFT);
+  await checkElement(input, 0, 1);
+
+  info("shift+right");
+  await synthesizeKey(VK.RIGHT, { shiftKey: true }, CHARS.RIGHT);
+  await checkElement(input, 1, 1);
+
+  info("left");
+  await synthesizeKey(VK.LEFT, {}, CHARS.LEFT);
+  await checkElement(input, 0, 0);
+
+  if (IS_MAC) {
+    info("down");
+    await synthesizeKey(VK.DOWN, { shiftKey: true }, CHARS.DOWN);
+  } else {
+    info("end");
+    await synthesizeKey(VK.END, { shiftKey: true }, CHARS.END);
+  }
+  await checkElement(input, 0, 9);
+
+  info("cut");
+  await synthesizeKey(VK.X, { accelKey: true }, "x", "input");
+  await checkElement(input, 0, 0, "");
+  let text = SpecialPowers.getClipboardData("text/unicode");
+  is(text, "Test text", "Should have cut to the clipboard");
+  SpecialPowers.clipboardCopyString("New text");
+
+  info("paste");
+  await synthesizeKey(VK.V, { accelKey: true }, "v", "input");
+  await checkElement(input, 8, 8, "New text");
+
+  if (IS_MAC) {
+    info("up");
+    await synthesizeKey(VK.UP, {}, CHARS.UP);
+  } else {
+    info("home");
+    await synthesizeKey(VK.HOME, {}, CHARS.HOME);
+  }
+  await checkElement(input, 0, 0, "New text");
+
+  info("select all");
+  await synthesizeKey(VK.A, { accelKey: true}, "a", "select");
+  await checkElement(input, 0, 8, "New text");
+
+  info("right");
+  await synthesizeKey(VK.RIGHT, {}, CHARS.RIGHT);
+  await checkElement(input, 8, 8, "New text");
+
+  info("word left");
+  if (IS_MAC) {
+    await synthesizeKey(VK.LEFT, { altKey: true }, CHARS.LEFT);
+  } else {
+    await synthesizeKey(VK.LEFT, { ctrlKey: true }, CHARS.LEFT);
+  }
+  await checkElement(input, 4, 4, "New text");
+
+  info("delete word left");
+  if (IS_MAC) {
+    await synthesizeKey(VK.BACKSPACE, { altKey: true }, CHARS.BACKSPACE);
+  } else {
+    await synthesizeKey(VK.BACKSPACE, { ctrlKey: true }, CHARS.BACKSPACE);
+  }
+  await checkElement(input, 0, 0, "text");
+
+  info("undo");
+  await synthesizeKey(VK.Z, { accelKey: true }, "", "input");
+  await checkElement(input, 4, 4, "New text");
+
+  info("redo");
+  await synthesizeKey(VK.Z, { accelKey: true, shiftKey: true }, "", "input");
+  await checkElement(input, 0, 0, "text");
+
+  SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</head>
+<body onload="runTest();">
+<input id=input value="Test text"/>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/keyhandling/test_textarea.html
@@ -0,0 +1,193 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>input key handling</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="/tests/SimpleTest/NativeKeyCodes.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+<script type="text/javascript">
+const IS_MAC = navigator.platform.indexOf("Mac") == 0;
+const VK = {};
+const CHARS = {};
+
+if (IS_MAC) {
+  VK.LEFT = MAC_VK_LeftArrow;
+  CHARS.LEFT = "\uF702";
+  VK.RIGHT = MAC_VK_RightArrow;
+  CHARS.RIGHT = "\uF703";
+  VK.UP = MAC_VK_UpArrow;
+  CHARS.UP = "\uF700";
+  VK.DOWN = MAC_VK_DownArrow;
+  CHARS.DOWN = "\uF701";
+  VK.X = MAC_VK_ANSI_X;
+  VK.V = MAC_VK_ANSI_V;
+  VK.A = MAC_VK_ANSI_A;
+  VK.BACKSPACE = MAC_VK_PC_Backspace;
+  CHARS.BACKSPACE = "\u007F";
+  VK.Z = MAC_VK_ANSI_Z;
+} else {
+  VK.LEFT = WIN_VK_LEFT;
+  CHARS.LEFT = "";
+  VK.RIGHT = WIN_VK_RIGHT;
+  CHARS.RIGHT = "";
+  VK.UP = WIN_VK_UP;
+  CHARS.UP = "";
+  VK.DOWN = WIN_VK_DOWN;
+  CHARS.DOWN = "";
+  VK.X = WIN_VK_X;
+  VK.V = WIN_VK_V;
+  VK.A = WIN_VK_A;
+  VK.END = WIN_VK_END;
+  CHARS.END = "";
+  VK.HOME = WIN_VK_HOME;
+  CHARS.HOME = "";
+  VK.BACKSPACE = WIN_VK_BACK;
+  CHARS.BACKSPACE = "";
+  VK.Z = WIN_VK_Z;
+}
+
+function synthesizeKey(keyCode, modifiers, chars, event = "keyup") {
+  return new Promise((resolve, reject) => {
+    window.addEventListener(event, resolve, { once: true });
+
+    if (!synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, keyCode, modifiers, chars, chars)) {
+      reject();
+    }
+  });
+}
+
+async function checkElement(element, start, end, content = "Test text") {
+  isReady = () => {
+    if (start != element.selectionStart) {
+      return false;
+    }
+    if (end != element.selectionEnd) {
+      return false;
+    }
+    if (content != element.value) {
+      return false;
+    }
+    return true;
+  };
+
+  for (let i = 0; i < 10; i++) {
+    if (isReady()) {
+      return;
+    }
+    
+    SimpleTest.requestFlakyTimeout("Polling for changes to apply");
+    await new Promise(resolve => setTimeout(resolve, 50));
+  }
+  ok(false, `Timed out waiting for state ${start} ${end} ${content}`);
+}
+
+async function runTest() {
+  let input = document.getElementById("input");
+  input.focus();
+  await checkElement(input, 0, 0);
+
+  info("right");
+  await synthesizeKey(VK.RIGHT, {}, CHARS.RIGHT);
+  await checkElement(input, 1, 1);
+
+  info("shift+right");
+  await synthesizeKey(VK.RIGHT, { shiftKey: true }, CHARS.RIGHT);
+  await checkElement(input, 1, 2);
+
+  info("shift+right");
+  await synthesizeKey(VK.RIGHT, { shiftKey: true }, CHARS.RIGHT);
+  await checkElement(input, 1, 3);
+
+  info("shift+left");
+  await synthesizeKey(VK.LEFT, { shiftKey: true }, CHARS.LEFT);
+  await checkElement(input, 1, 2);
+
+  info("shift+left");
+  await synthesizeKey(VK.LEFT, { shiftKey: true }, CHARS.LEFT);
+  await checkElement(input, 1, 1);
+
+  info("shift+left");
+  await synthesizeKey(VK.LEFT, { shiftKey: true }, CHARS.LEFT);
+  await checkElement(input, 0, 1);
+
+  info("shift+right");
+  await synthesizeKey(VK.RIGHT, { shiftKey: true }, CHARS.RIGHT);
+  await checkElement(input, 1, 1);
+
+  info("left");
+  await synthesizeKey(VK.LEFT, {}, CHARS.LEFT);
+  await checkElement(input, 0, 0);
+
+  if (IS_MAC) {
+    info("down");
+    await synthesizeKey(VK.DOWN, { shiftKey: true }, CHARS.DOWN);
+  } else {
+    info("end");
+    await synthesizeKey(VK.END, { shiftKey: true }, CHARS.END);
+  }
+  await checkElement(input, 0, 9);
+
+  info("cut");
+  await synthesizeKey(VK.X, { accelKey: true }, "x", "input");
+  await checkElement(input, 0, 0, "");
+  let text = SpecialPowers.getClipboardData("text/unicode");
+  is(text, "Test text", "Should have cut to the clipboard");
+  SpecialPowers.clipboardCopyString("New text");
+
+  info("paste");
+  await synthesizeKey(VK.V, { accelKey: true }, "v", "input");
+  await checkElement(input, 8, 8, "New text");
+
+  if (IS_MAC) {
+    info("up");
+    await synthesizeKey(VK.UP, {}, CHARS.UP);
+  } else {
+    info("home");
+    await synthesizeKey(VK.HOME, {}, CHARS.HOME);
+  }
+  await checkElement(input, 0, 0, "New text");
+
+  info("select all");
+  await synthesizeKey(VK.A, { accelKey: true}, "a", "select");
+  await checkElement(input, 0, 8, "New text");
+
+  info("right");
+  await synthesizeKey(VK.RIGHT, {}, CHARS.RIGHT);
+  await checkElement(input, 8, 8, "New text");
+
+  info("word left");
+  if (IS_MAC) {
+    await synthesizeKey(VK.LEFT, { altKey: true }, CHARS.LEFT);
+  } else {
+    await synthesizeKey(VK.LEFT, { ctrlKey: true }, CHARS.LEFT);
+  }
+  await checkElement(input, 4, 4, "New text");
+
+  info("delete word left");
+  if (IS_MAC) {
+    await synthesizeKey(VK.BACKSPACE, { altKey: true }, CHARS.BACKSPACE);
+  } else {
+    await synthesizeKey(VK.BACKSPACE, { ctrlKey: true }, CHARS.BACKSPACE);
+  }
+  await checkElement(input, 0, 0, "text");
+
+  info("undo");
+  await synthesizeKey(VK.Z, { accelKey: true }, "", "input");
+  await checkElement(input, 4, 4, "New text");
+
+  info("redo");
+  await synthesizeKey(VK.Z, { accelKey: true, shiftKey: true }, "", "input");
+  await checkElement(input, 0, 0, "text");
+
+  SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</head>
+<body onload="runTest();">
+<textarea id=input>Test text</textarea>
+</body>
+</html>
--- a/dom/tests/moz.build
+++ b/dom/tests/moz.build
@@ -157,31 +157,33 @@ MOCHITEST_MANIFESTS += [
     'mochitest/dom-level0/mochitest.ini',
     'mochitest/dom-level1-core/mochitest.ini',
     'mochitest/dom-level2-core/mochitest.ini',
     'mochitest/dom-level2-html/mochitest.ini',
     'mochitest/fetch/mochitest.ini',
     'mochitest/gamepad/mochitest.ini',
     'mochitest/general/mochitest.ini',
     'mochitest/geolocation/mochitest.ini',
+    'mochitest/keyhandling/mochitest.ini',
     'mochitest/localstorage/mochitest.ini',
     'mochitest/orientation/mochitest.ini',
     'mochitest/pointerlock/mochitest.ini',
     'mochitest/script/mochitest.ini',
     'mochitest/sessionstorage/mochitest.ini',
     'mochitest/storageevent/mochitest.ini',
     'mochitest/webcomponents/mochitest.ini',
     'mochitest/whatwg/mochitest.ini',
 ]
 
 MOCHITEST_CHROME_MANIFESTS += [
     'mochitest/beacon/chrome.ini',
     'mochitest/chrome/chrome.ini',
     'mochitest/general/chrome.ini',
     'mochitest/geolocation/chrome.ini',
+    'mochitest/keyhandling/chrome.ini',
     'mochitest/localstorage/chrome.ini',
     'mochitest/sessionstorage/chrome.ini',
     'mochitest/webcomponents/chrome.ini',
     'mochitest/webcomponents/chrome_disabled.ini',
     'mochitest/whatwg/chrome.ini',
 ]
 
 XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']