Bug 1563206 - Test top key combinations that affect gutenberg; r=ato,remote-protocol-reviewers,jdescottes
☠☠ backed out by b92c29aab7c6 ☠ ☠
authorMaja Frydrychowicz <mjzffr@gmail.com>
Fri, 18 Oct 2019 15:07:30 +0000
changeset 559514 1ce1b20dcc2015fb6a4a244bb851cafec84bb184
parent 559513 27ba497f76d15ef43d02d6c159b278ffc1879460
child 559515 4ad0eb343fac8037f7226c04ad7f1f8a76147426
push id12177
push usercsabou@mozilla.com
push dateMon, 21 Oct 2019 14:52:16 +0000
treeherdermozilla-beta@1918a9cd33bc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersato, remote-protocol-reviewers, jdescottes
bugs1563206
milestone71.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 1563206 - Test top key combinations that affect gutenberg; r=ato,remote-protocol-reviewers,jdescottes Differential Revision: https://phabricator.services.mozilla.com/D47032
remote/doc/Testing.md
remote/domains/parent/Input.jsm
remote/test/browser/browser.ini
remote/test/browser/browser_input_dispatchKeyEvent.js
remote/test/browser/browser_input_dispatchMouseEvent.js
remote/test/browser/doc_input_events.html
remote/test/browser/head.js
--- a/remote/doc/Testing.md
+++ b/remote/doc/Testing.md
@@ -27,17 +27,17 @@ Browser chrome tests
 --------------------
 
 We also have a set of functional [browser chrome] tests located
 under _remote/test/browser_:
 
 	% ./mach mochitest remote/test/browser/browser_cdp.js
 
 The functional tests will appear under the `M` (for _mochitest_)
-category in the `bc` (_browser-chrome_) jobs on Treeherder.
+category in the `remote` jobs on Treeherder.
 
 As the functional tests will sporadically pop up new Firefox
 application windows, a helpful tip is to run them in [headless
 mode]:
 
 	% ./mach mochitest --headless remote/test/browser
 
 The `--headless` flag is equivalent to setting the `MOZ_HEADLESS`
--- a/remote/domains/parent/Input.jsm
+++ b/remote/domains/parent/Input.jsm
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["Input"];
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
 const { Domain } = ChromeUtils.import(
   "chrome://remote/content/domains/Domain.jsm"
 );
 
 class Input extends Domain {
   // commands
 
   /**
@@ -21,21 +22,26 @@ class Input extends Domain {
    *        - autoRepeat (not supported)
    *        - code (not supported)
    *        - key
    *        - isKeypad (not supported)
    *        - location (not supported)
    *        - modifiers
    *        - text (not supported)
    *        - type
-   *        - unmodifiedTest (not supported)
+   *        - unmodifiedText (not supported)
    *        - windowsVirtualKeyCode
+   *        - nativeVirtualKeyCode (not supported)
+   *        - keyIdentifier (not supported)
+   *        - isSystemKey (not supported)
    */
   async dispatchKeyEvent(options) {
+    // missing code, text, unmodifiedText, autorepeat, location, iskeypad
     const { key, modifiers, type, windowsVirtualKeyCode } = options;
+    const { alt, ctrl, meta, shift } = Input.Modifier;
 
     let domType;
     if (type == "keyDown" || type == "rawKeyDown") {
       // 'rawKeyDown' is passed as type by puppeteer for all non-text keydown events:
       // See https://github.com/GoogleChrome/puppeteer/blob/2d99d85976dcb28cc6e3bad4b6a00cd61a67a2cf/lib/Input.js#L52
       // For now we simply map rawKeyDown to keydown.
       domType = "keydown";
     } else if (type == "keyUp" || type == "char") {
@@ -57,75 +63,58 @@ class Input extends Domain {
 
     if (type == "char") {
       // type == "char" is used when doing `await page.keyboard.type( 'I’m a list' );`
       // the ’ character will be calling dispatchKeyEvent only once with type=char.
       EventUtils.synthesizeKey(key, {}, browserWindow);
     } else {
       // Non printable keys should be prefixed with `KEY_`
       const eventUtilsKey = key.length == 1 ? key : "KEY_" + key;
-      EventUtils.synthesizeKey(
-        eventUtilsKey,
-        {
-          keyCode: windowsVirtualKeyCode,
-          type: domType,
-          altKey: !!(modifiers & 1),
-          ctrlKey: !!(modifiers & 2),
-          metaKey: !!(modifiers & 4),
-          shiftKey: !!(modifiers & 8),
-        },
-        browserWindow
-      );
+      const eventInfo = {
+        keyCode: windowsVirtualKeyCode,
+        type: domType,
+        altKey: !!(modifiers & alt),
+        ctrlKey: !!(modifiers & ctrl),
+        metaKey: !!(modifiers & meta),
+        shiftKey: !!(modifiers & shift),
+      };
+      EventUtils.synthesizeKey(eventUtilsKey, eventInfo, browserWindow);
     }
 
     await this.executeInChild("waitForContentEvent", eventId);
   }
 
   async dispatchMouseEvent({ type, button, x, y, modifiers, clickCount }) {
+    const { alt, ctrl, meta, shift } = Input.Modifier;
+
     if (type == "mousePressed") {
       type = "mousedown";
     } else if (type == "mouseReleased") {
       type = "mouseup";
     } else if (type == "mouseMoved") {
       type = "mousemove";
     } else {
       throw new Error(`Mouse type is not supported: ${type}`);
     }
 
     if (type === "mousedown" && button === "right") {
       type = "contextmenu";
     }
-    if (button == undefined || button == "none" || button == "left") {
-      button = 0;
-    } else if (button == "middle") {
-      button = 1;
-    } else if (button == "right") {
-      button = 2;
-    } else if (button == "back") {
-      button = 3;
-    } else if (button == "forward") {
-      button = 4;
-    } else {
-      throw new Error(`Mouse button is not supported: ${button}`);
-    }
-
-    // Gutenberg test packages/e2e-tests/specs/blocks/list.test.js:
-    // "can be created by converting multiple paragraphs"
-    // Works better with EventUtils, in order to make the Shift+Click to work
+    const buttonID = Input.Button[button] || Input.Button.left;
     const { browser } = this.session.target;
     const currentWindow = browser.ownerGlobal;
     const EventUtils = this._getEventUtils(currentWindow);
     EventUtils.synthesizeMouse(browser, x, y, {
       type,
-      button,
+      button: buttonID,
       clickCount: clickCount || 1,
-      altKey: !!(modifiers & 1),
-      ctrlKey: !!(modifiers & 2),
-      metaKey: !!(modifiers & 4),
-      shiftKey: !!(modifiers & 8),
+      altKey: !!(modifiers & alt),
+      ctrlKey: !!(modifiers & ctrl),
+      metaKey: !!(modifiers & meta),
+      shiftKey: !!(modifiers & shift),
     });
   }
 
   /**
    * Memoized EventUtils getter.
    */
   _getEventUtils(win) {
     if (!this._eventUtils) {
@@ -138,8 +127,23 @@ class Input extends Domain {
       Services.scriptloader.loadSubScript(
         "chrome://remote/content/external/EventUtils.js",
         this._eventUtils
       );
     }
     return this._eventUtils;
   }
 }
+
+Input.Button = {
+  left: 0,
+  middle: 1,
+  right: 2,
+  back: 3,
+  forward: 4,
+};
+
+Input.Modifier = {
+  alt: 1,
+  ctrl: 2,
+  meta: 4,
+  shift: 8,
+};
--- a/remote/test/browser/browser.ini
+++ b/remote/test/browser/browser.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 tags = remote
 subsuite = remote
 prefs = remote.enabled=true
 support-files =
   chrome-remote-interface.js
   doc_input_dispatchKeyEvent_race.html
+  doc_input_events.html
   doc_network_requestWillBeSent.html
   file_network_requestWillBeSent.js
   head.js
 
 [browser_cdp.js]
 [browser_input_dispatchKeyEvent.js]
 [browser_input_dispatchKeyEvent_race.js]
 [browser_input_dispatchMouseEvent.js]
--- a/remote/test/browser/browser_input_dispatchKeyEvent.js
+++ b/remote/test/browser/browser_input_dispatchKeyEvent.js
@@ -1,154 +1,431 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Basic test for dispatch key event API with input type text
+const { Input: I } = ChromeUtils.import(
+  "chrome://remote/content/domains/parent/Input.jsm"
+);
+
+const { alt, ctrl, meta, shift } = I.Modifier;
 
 // Map of key codes used in this test.
 const KEYCODES = {
-  a: 65,
-  Backspace: 8,
-  h: 72,
-  H: 72,
-  AltLeft: 18,
-  ArrowLeft: 37,
-  ArrowRight: 39,
+  a: KeyboardEvent.DOM_VK_A,
+  A: KeyboardEvent.DOM_VK_A,
+  b: KeyboardEvent.DOM_VK_B,
+  B: KeyboardEvent.DOM_VK_B,
+  c: KeyboardEvent.DOM_VK_C,
+  C: KeyboardEvent.DOM_VK_C,
+  h: KeyboardEvent.DOM_VK_H,
+  H: KeyboardEvent.DOM_VK_H,
+  Alt: KeyboardEvent.DOM_VK_ALT,
+  ArrowLeft: KeyboardEvent.DOM_VK_LEFT,
+  ArrowRight: KeyboardEvent.DOM_VK_RIGHT,
+  ArrowDown: KeyboardEvent.DOM_VK_DOWN,
+  Backspace: KeyboardEvent.DOM_VK_BACK_SPACE,
+  Control: KeyboardEvent.DOM_VK_CONTROL,
+  Meta: KeyboardEvent.DM_VK_META,
+  Shift: KeyboardEvent.DOM_VK_SHIFT,
+  Tab: KeyboardEvent.DOM_VK_TAB,
 };
 
-// Modifier for move forward shortcut is CTRL+RightArrow on Linux/Windows, ALT+RightArrow
-// on Mac.
 const isMac = Services.appinfo.OS === "Darwin";
-const ALT_MODIFIER = 1;
-const CTRL_MODIFIER = 2;
-const ARROW_MODIFIER = isMac ? ALT_MODIFIER : CTRL_MODIFIER;
 
-add_task(async function() {
-  // The selectionchange event was flagged behind dom.select_events.textcontrols.enabled,
-  // which is only true on Nightly and Local builds. Force the pref to true so that
-  // the test passes on all channels. See Bug 1309628 for more details.
-  info("Enable selectionchange events on input elements");
-  await new Promise(resolve => {
-    const options = {
-      set: [["dom.select_events.textcontrols.enabled", true]],
-    };
-    SpecialPowers.pushPrefEnv(options, resolve);
-  });
+const PAGE_URL =
+  "http://example.com/browser/remote/test/browser/doc_input_events.html";
 
-  const { client, tab } = await setupForURL(toDataURL("<input>"));
-  is(gBrowser.selectedTab, tab, "Selected tab is the target tab");
-
+add_task(async function testTypingPrintableCharacters() {
+  const { client } = await setupForInput(toDataURL("<input>"));
   const { Input } = client;
 
-  info("Focus the input on the page");
-  await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
-    const input = content.document.querySelector("input");
-    input.focus();
-    is(input, content.document.activeElement, "Input should be focused");
-  });
-  await checkInputContent("", 0);
-
   info("Write 'h'");
   await sendTextKey(Input, "h");
   await checkInputContent("h", 1);
 
   info("Write 'H'");
   await sendTextKey(Input, "H");
   await checkInputContent("hH", 2);
 
   info("Send char type event for char [’]");
   await Input.dispatchKeyEvent({
     type: "char",
     modifiers: 0,
     key: "’",
   });
   await checkInputContent("hH’", 3);
 
+  await teardown(client);
+});
+
+add_task(async function testArrowKeys() {
+  const { client } = await setupForInput(toDataURL("<input>"));
+  const { Input } = client;
+
+  await sendText(Input, "hH’");
   info("Send Left");
-  await sendArrowKey(Input, "ArrowLeft");
+  await sendRawKey(Input, "ArrowLeft");
   await checkInputContent("hH’", 2);
 
   info("Write 'a'");
   await sendTextKey(Input, "a");
   await checkInputContent("hHa’", 3);
 
   info("Send Left");
-  await sendArrowKey(Input, "ArrowLeft");
+  await sendRawKey(Input, "ArrowLeft");
   await checkInputContent("hHa’", 2);
 
   info("Send Left");
-  await sendArrowKey(Input, "ArrowLeft");
+  await sendRawKey(Input, "ArrowLeft");
   await checkInputContent("hHa’", 1);
 
   info("Write 'a'");
   await sendTextKey(Input, "a");
   await checkInputContent("haHa’", 2);
 
-  info("Send ALT/CTRL + Right");
-  await sendArrowKey(Input, "ArrowRight", ARROW_MODIFIER);
+  info("Send ALT/CONTROL + Right");
+  let modCode = isMac ? alt : ctrl;
+  let modKey = isMac ? "Alt" : "Control";
+  await dispatchKeyEvent(Input, modKey, "rawKeyDown", modCode);
+  await dispatchKeyEvent(Input, "ArrowRight", "rawKeyDown", modCode);
+  await dispatchKeyEvent(Input, "ArrowRight", "keyUp");
+  await dispatchKeyEvent(Input, modKey, "keyUp");
   await checkInputContent("haHa’", 5);
 
+  await teardown(client);
+});
+
+add_task(async function testBackspace() {
+  const { client } = await setupForInput(toDataURL("<input>"));
+  const { Input } = client;
+
+  await sendText(Input, "haHa’");
+
   info("Delete every character in the input");
-  await sendBackspace(Input, "haHa");
-  await sendBackspace(Input, "haH");
-  await sendBackspace(Input, "ha");
-  await sendBackspace(Input, "h");
-  await sendBackspace(Input, "");
+  await checkBackspace(Input, "haHa");
+  await checkBackspace(Input, "haH");
+  await checkBackspace(Input, "ha");
+  await checkBackspace(Input, "h");
+  await checkBackspace(Input, "");
+
+  await teardown(client);
+});
+
+add_task(async function testShiftSelect() {
+  const { client } = await setupForInput(toDataURL("<input>"));
+  const { Input } = client;
+  await resetInput("word 2 word3");
+
+  info("Send Shift + Left (select one char to the left)");
+  await dispatchKeyEvent(Input, "Shift", "rawKeyDown", shift);
+  await sendRawKey(Input, "ArrowLeft", shift);
+  await sendRawKey(Input, "ArrowLeft", shift);
+  await sendRawKey(Input, "ArrowLeft", shift);
+  info("(deleteContentBackward)");
+  await checkBackspace(Input, "word 2 wo");
+  await dispatchKeyEvent(Input, "Shift", "keyUp");
+
+  await resetInput("word 2 wo");
+  info("Send Shift + Left (select one char to the left)");
+  await dispatchKeyEvent(Input, "Shift", "rawKeyDown", shift);
+  await sendRawKey(Input, "ArrowLeft", shift);
+  await sendRawKey(Input, "ArrowLeft", shift);
+  await sendTextKey(Input, "H");
+  await checkInputContent("word 2 H", 8);
+  await dispatchKeyEvent(Input, "Shift", "keyUp");
+
+  await teardown(client);
+});
+
+add_task(async function testSelectWord() {
+  const { client } = await setupForInput(toDataURL("<input>"));
+  const { Input } = client;
+  await resetInput("word 2 word3");
+
+  info("Send Shift + Ctrl/Alt + Left (select one word to the left)");
+  const { primary, primaryKey } = keyForPlatform();
+  const combined = shift | primary;
+  await dispatchKeyEvent(Input, "Shift", "rawKeyDown", shift);
+  await dispatchKeyEvent(Input, primaryKey, "rawKeyDown", combined);
+  await sendRawKey(Input, "ArrowLeft", combined);
+  await sendRawKey(Input, "ArrowLeft", combined);
+  await dispatchKeyEvent(Input, "Shift", "keyUp", primary);
+  await dispatchKeyEvent(Input, primaryKey, "keyUp");
+  info("(deleteContentBackward)");
+  await checkBackspace(Input, "word ");
+
+  await teardown(client);
+});
+
+add_task(async function testSelectDelete() {
+  const { client } = await setupForInput(toDataURL("<input>"));
+  const { Input } = client;
+  await resetInput("word 2 word3");
+
+  info("Send Ctrl/Alt + Backspace (deleteWordBackward)");
+  const { primary, primaryKey } = keyForPlatform();
+  await dispatchKeyEvent(Input, primaryKey, "rawKeyDown", primary);
+  await checkBackspace(Input, "word 2 ", primary);
+  await dispatchKeyEvent(Input, primaryKey, "keyUp");
 
-  await client.close();
-  ok(true, "The client is closed");
+  await resetInput("word 2 ");
+  await sendText(Input, "word4");
+  await sendRawKey(Input, "ArrowLeft");
+  await sendRawKey(Input, "ArrowLeft");
+  await checkInputContent("word 2 word4", 10);
+
+  if (isMac) {
+    info("Send Meta + Backspace (deleteSoftLineBackward)");
+    await dispatchKeyEvent(Input, "Meta", "rawKeyDown", meta);
+    await sendRawKey(Input, "Backspace", meta);
+    const { value, caret } = await getInputContent();
+    await dispatchKeyEvent(Input, "Meta", "keyUp");
+    todo_is(value, "d4", "Meta + Backspace should delete line backward");
+    todo_is(caret, 0, "Check position after Meta + Backspace");
+  }
+
+  await teardown(client);
+});
+
+add_task(async function testShiftEvents() {
+  const { client } = await setupForInput(PAGE_URL);
+  const { Input } = client;
+  await resetEvents();
 
-  BrowserTestUtils.removeTab(tab);
+  await withModifier(Input, "Shift", "shift", "A");
+  await checkInputContent("A", 1);
+  let events = await getEvents();
+  checkEvent(events[0], "keydown", "Shift", "shift", true);
+  checkEvent(events[1], "keydown", "A", "shift", true);
+  checkEvent(events[2], "keypress", "A", "shift", true);
+  checkProperties({ data: "A", inputType: "insertText" }, events[3]);
+  checkEvent(events[4], "keyup", "A", "shift", true);
+  checkEvent(events[5], "keyup", "Shift", "shift", false);
+  await resetEvents();
+
+  await withModifier(Input, "Shift", "shift", "Enter");
+  events = await getEvents();
+  checkEvent(events[2], "keypress", "Enter", "shift", true);
+  await resetEvents();
 
-  await RemoteAgent.close();
+  await withModifier(Input, "Shift", "shift", "Tab");
+  events = await getEvents();
+  checkEvent(events[1], "keydown", "Tab", "shift", true);
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    const input = content.document.querySelector("input");
+    isnot(input, content.document.activeElement, "input should lose focus");
+  });
+
+  await teardown(client);
+});
+
+add_task(async function testAltEvents() {
+  const { client } = await setupForInput(PAGE_URL);
+  const { Input } = client;
+
+  await withModifier(Input, "Alt", "alt", "a");
+  if (isMac) {
+    await checkInputContent("a", 1);
+  } else {
+    await checkInputContent("", 0);
+  }
+  let events = await getEvents();
+  checkEvent(events[1], "keydown", "a", "alt", true);
+  checkEvent(events[events.length - 1], "keyup", "Alt", "alt", false);
+  await teardown(client);
 });
 
-async function sendTextKey(Input, key) {
-  await dispatchKeyEvent(Input, key, "keyDown");
-  await dispatchKeyEvent(Input, key, "keyUp");
+add_task(async function testControlEvents() {
+  const { client } = await setupForInput(PAGE_URL);
+  const { Input } = client;
+
+  await withModifier(Input, "Control", "ctrl", "`");
+  let events = await getEvents();
+  // no keypress or input event
+  checkEvent(events[1], "keydown", "`", "ctrl", true);
+  checkEvent(events[events.length - 1], "keyup", "Control", "ctrl", false);
+  await teardown(client);
+});
+
+add_task(async function testMetaEvents() {
+  if (!isMac) {
+    return;
+  }
+  const { client } = await setupForInput(PAGE_URL);
+  const { Input } = client;
+
+  await withModifier(Input, "Meta", "meta", "a");
+  let events = await getEvents();
+  // no keypress or input event
+  checkEvent(events[1], "keydown", "a", "meta", true);
+  checkEvent(events[events.length - 1], "keyup", "Meta", "meta", false);
+
+  await teardown(client);
+});
+
+add_task(async function testCtrlShiftArrows() {
+  const { client } = await setupForURL(
+    toDataURL('<select multiple size="3"><option>a<option>b<option>c</select>')
+  );
+  const { Input } = client;
+
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    const select = content.document.querySelector("select");
+    select.selectedIndex = 0;
+    select.focus();
+  });
+
+  const combined = shift | ctrl;
+  await dispatchKeyEvent(Input, "Control", "rawKeyDown", shift);
+  await dispatchKeyEvent(Input, "Shift", "rawKeyDown", combined);
+  await sendRawKey(Input, "ArrowDown", combined);
+  await dispatchKeyEvent(Input, "Control", "keyUp", shift);
+  await dispatchKeyEvent(Input, "Shift", "keyUp");
+
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    const select = content.document.querySelector("select");
+    ok(select[0].selected, "First option should be selected");
+    ok(select[1].selected, "Second option should be selected");
+    ok(!select[2].selected, "Third option should not be selected");
+  });
+  await teardown(client);
+});
+
+add_task(async function testShiftClick() {
+  const { client } = await setupForURL(PAGE_URL);
+  const { Input } = client;
+  await resetEvents();
+
+  await dispatchKeyEvent(Input, "Shift", "rawKeyDown", shift);
+  info("Click the 'pointers' div.");
+  await Input.dispatchMouseEvent({
+    type: "mousePressed",
+    x: 80,
+    y: 180,
+    modifiers: shift,
+  });
+  await Input.dispatchMouseEvent({
+    type: "mouseReleased",
+    x: 80,
+    y: 180,
+    modifiers: shift,
+  });
+  await dispatchKeyEvent(Input, "Shift", "keyUp", shift);
+  let events = await getEvents();
+  checkProperties({ type: "click", shiftKey: true, button: 0 }, events[2]);
+
+  await teardown(client);
+});
+
+function keyForPlatform() {
+  // TODO add cases for other key-combinations as the need arises
+  let primary = ctrl;
+  let primaryKey = "Control";
+  if (isMac) {
+    primary = alt;
+    primaryKey = "Alt";
+  }
+  return { primary, primaryKey };
 }
 
-async function sendArrowKey(Input, arrowKey, modifiers = 0) {
-  await dispatchKeyEvent(Input, arrowKey, "rawKeyDown", modifiers);
-  await dispatchKeyEvent(Input, arrowKey, "keyUp", modifiers);
+async function setupForInput(url) {
+  const { client, tab } = await setupForURL(url);
+  info("Focus the input on the page");
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    const input = content.document.querySelector("input");
+    input.focus();
+    is(input, content.document.activeElement, "Input should be focused");
+  });
+  await checkInputContent("", 0);
+  return { client, tab };
+}
+
+async function sendTextKey(Input, key, modifiers = 0) {
+  await dispatchKeyEvent(Input, key, "keyDown", modifiers);
+  await dispatchKeyEvent(Input, key, "keyUp", modifiers);
+}
+
+async function sendText(Input, text) {
+  for (const sym of text) {
+    await sendTextKey(Input, sym);
+  }
+}
+
+async function sendRawKey(Input, key, modifiers = 0) {
+  await dispatchKeyEvent(Input, key, "rawKeyDown", modifiers);
+  await dispatchKeyEvent(Input, key, "keyUp", modifiers);
+}
+
+async function checkBackspace(Input, expected, modifiers = 0) {
+  info("Send Backspace");
+  await sendRawKey(Input, "Backspace", modifiers);
+  await checkInputContent(expected, expected.length);
+}
+
+async function withModifier(Input, modKey, mod, key) {
+  await dispatchKeyEvent(Input, modKey, "rawKeyDown", I.Modifier[mod]);
+  await dispatchKeyEvent(Input, key, "keyDown", I.Modifier[mod]);
+  await dispatchKeyEvent(Input, key, "keyUp", I.Modifier[mod]);
+  await dispatchKeyEvent(Input, modKey, "keyUp");
 }
 
 function dispatchKeyEvent(Input, key, type, modifiers = 0) {
   info(`Send ${type} for key ${key}`);
   return Input.dispatchKeyEvent({
     type,
     modifiers,
     windowsVirtualKeyCode: KEYCODES[key],
     key,
   });
 }
 
-async function checkInputContent(expectedValue, expectedCaret) {
-  const { value, caret } = await ContentTask.spawn(
-    gBrowser.selectedBrowser,
-    null,
-    function() {
-      const input = content.document.querySelector("input");
-      return { value: input.value, caret: input.selectionStart };
-    }
-  );
+function getInputContent() {
+  return ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    const input = content.document.querySelector("input");
+    return { value: input.value, caret: input.selectionStart };
+  });
+}
 
-  is(
-    value,
-    expectedValue,
-    `The input value is correct ("${value}"="${expectedValue}")`
-  );
-  is(
-    caret,
-    expectedCaret,
-    `The input caret has the correct index ("${caret}"="${expectedCaret}")`
-  );
+async function checkInputContent(expectedValue, expectedCaret) {
+  const { value, caret } = await getInputContent();
+  is(value, expectedValue, "Check input content");
+  is(caret, expectedCaret, "Check position of input caret");
+}
+
+// assuming doc_input_events.html
+async function getEvents() {
+  const events = await ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+    return content.eval("allEvents");
+  });
+  info(`Events: ${JSON.stringify(events)}`);
+  return events;
 }
 
-async function sendBackspace(Input, expected) {
-  info("Send Backspace");
-  await dispatchKeyEvent(Input, "Backspace", "rawKeyDown");
-  await dispatchKeyEvent(Input, "Backspace", "keyUp");
+// assuming doc_input_events.html
+async function resetEvents() {
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+    content.eval("resetEvents()");
+    const events = content.eval("allEvents");
+    is(events.length, 0, "List of events should be empty");
+  });
+}
 
-  await checkInputContent(expected, expected.length);
+function resetInput(value = "") {
+  return ContentTask.spawn(gBrowser.selectedBrowser, value, function(arg) {
+    const input = content.document.querySelector("input");
+    input.value = arg;
+    input.focus();
+  });
 }
+
+function checkEvent(event, type, key, property, expectedValue) {
+  let expected = { type, key };
+  expected[property] = expectedValue;
+  checkProperties(expected, event, "Event");
+}
+
+function checkProperties(expectedObj, targetObj, message = "Compare objects") {
+  for (const prop in expectedObj) {
+    is(targetObj[prop], expectedObj[prop], message + `: check ${prop}`);
+  }
+}
--- a/remote/test/browser/browser_input_dispatchMouseEvent.js
+++ b/remote/test/browser/browser_input_dispatchMouseEvent.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 add_task(async function testDispatchMouseEvent() {
-  const { client, tab } = await setupForURL(toDataURL("<div>foo</div>"));
+  const { client } = await setupForURL(toDataURL("<div>foo</div>"));
 
   const { Input } = client;
 
   await ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
     const div = content.document.querySelector("div");
     this.mouseDownPromise = new Promise(resolve => {
       div.addEventListener("mousedown", resolve, { once: true });
     });
@@ -47,15 +47,10 @@ add_task(async function testDispatchMous
     return this.mouseUpPromise;
   });
 
   info("Waiting for DOM click event on the div");
   await ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
     return this.clickPromise;
   });
 
-  await client.close();
-  ok(true, "The client is closed");
-
-  BrowserTestUtils.removeTab(tab);
-
-  await RemoteAgent.close();
+  await teardown(client);
 });
new file mode 100644
--- /dev/null
+++ b/remote/test/browser/doc_input_events.html
@@ -0,0 +1,148 @@
+<!doctype html>
+<meta charset=utf-8>
+<html>
+<head>
+    <title>Input Events</title>
+    <style>
+      div { padding:0px; margin: 0px; }
+      #trackPointer { position: fixed; }
+      #resultContainer { width: 600px; height: 60px; }
+      .area { width: 100px; height: 50px; background-color: #ccc; }
+      .block { width: 5px; height: 5px; border: solid 1px red; }
+      #dragArea { position: relative; }
+      #dragTarget { position: absolute; top:22px; left:47px;}
+    </style>
+    <script>
+        "use strict";
+        var allEvents = [];
+        function makeParagraph(message) {
+            let paragraph = document.createElement("p");
+            paragraph.textContent = message;
+            return paragraph;
+        }
+        function displayMessage(message) {
+            let eventNode = document.getElementById("events");
+            eventNode.innerHTML = ""
+            eventNode.appendChild(makeParagraph(message));
+        }
+
+        function appendMessage(message) {
+            document.getElementById("events").appendChild(makeParagraph(message));
+        }
+
+        /**
+         * Escape |key| if it's in a surrogate-half character range.
+         *
+         * Example: given "\ud83d" return "U+d83d".
+         *
+         * Otherwise JSON.stringify will convert it to U+FFFD (REPLACEMENT CHARACTER)
+         * when returning a value from executeScript, for example.
+         */
+        function escapeSurrogateHalf(key) {
+          if (typeof key !== "undefined" && key.length === 1) {
+            var charCode = key.charCodeAt(0);
+            var highSurrogate = charCode >= 0xD800 && charCode <= 0xDBFF;
+            var surrogate = highSurrogate || (charCode >= 0xDC00 && charCode <= 0xDFFF);
+            if (surrogate) {
+              key = "U+" + charCode.toString(16);
+            }
+          }
+          return key;
+        }
+
+        function recordKeyboardEvent(event) {
+          var key = escapeSurrogateHalf(event.key);
+          allEvents.push({
+            "code": event.code,
+            "key": key,
+            "which": event.which,
+            "location": event.location,
+            "alt": event.altKey,
+            "ctrl": event.ctrlKey,
+            "meta": event.metaKey,
+            "shift": event.shiftKey,
+            "repeat": event.repeat,
+            "type": event.type
+          });
+          appendMessage(event.type + " " +
+              "code: " + event.code + ", " +
+              "key: " + key + ", " +
+              "which: " + event.which + ", " +
+              "keyCode: " + event.keyCode);
+        }
+        function recordInputEvent(event) {
+          allEvents.push({
+            "data": event.data,
+            "inputType": event.inputType,
+            "isComposing": event.isComposing,
+          });
+          appendMessage("InputEvent " +
+              "data: " + event.data + ", " +
+              "inputType: " + event.inputType + ", " +
+              "isComposing: " + event.isComposing);
+        }
+        function recordPointerEvent(event) {
+          if (event.type === "contextmenu") {
+            event.preventDefault();
+          }
+          allEvents.push({
+            "type": event.type,
+            "button": event.button,
+            "buttons": event.buttons,
+            "pageX": event.pageX,
+            "pageY": event.pageY,
+            "ctrlKey": event.ctrlKey,
+            "metaKey": event.metaKey,
+            "altKey": event.altKey,
+            "shiftKey": event.shiftKey,
+            "target": event.target.id
+          });
+          appendMessage(event.type + " " +
+              "pageX: " + event.pageX + ", " +
+              "pageY: " + event.pageY + ", " +
+              "button: " + event.button + ", " +
+              "buttons: " + event.buttons + ", " +
+              "ctrlKey: " + event.ctrlKey + ", " +
+              "altKey: " + event.altKey + ", " +
+              "metaKey: " + event.metaKey + ", " +
+              "shiftKey: " + event.shiftKey + ", " +
+              "target id: " + event.target.id);
+        }
+        function resetEvents() {
+          allEvents.length = 0;
+          displayMessage("");
+        }
+
+        document.addEventListener("DOMContentLoaded", function() {
+          let keyReporter = document.getElementById("keys");
+          keyReporter.addEventListener("keyup", recordKeyboardEvent);
+          keyReporter.addEventListener("keypress", recordKeyboardEvent);
+          keyReporter.addEventListener("keydown", recordKeyboardEvent);
+          keyReporter.addEventListener("input", recordInputEvent);
+
+          let mouseReporter = document.getElementById("pointers");
+          mouseReporter.addEventListener("click", recordPointerEvent);
+          mouseReporter.addEventListener("dblclick", recordPointerEvent);
+          mouseReporter.addEventListener("mousedown", recordPointerEvent);
+          mouseReporter.addEventListener("mouseup", recordPointerEvent);
+          mouseReporter.addEventListener("contextmenu", recordPointerEvent);
+        });
+    </script>
+</head>
+<body>
+  <div id="trackPointer" class="block"></div>
+  <div>
+    <h2>KeyReporter</h2>
+    <input type="text" id="keys" size="80">
+  </div>
+  <div>
+    <h2>ClickReporter</h2>
+    <div id="pointers" class="area">
+    </div>
+  </div>
+  <div id="resultContainer">
+    <h2>Events</h2>
+    <div id="events"></div>
+  </div>
+</body>
+</html>
--- a/remote/test/browser/head.js
+++ b/remote/test/browser/head.js
@@ -111,16 +111,17 @@ async function setup() {
 
 /**
  * Set up test environment by starting the remote agent, connecting
  * the CDP client over loopback, and creating a fresh tab to avoid
  * state bleedover from previous test.
  */
 async function setupForURL(url) {
   const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+  is(gBrowser.selectedTab, tab, "Selected tab is the target tab");
 
   await RemoteAgent.listen(Services.io.newURI("http://localhost:9222"));
   const CDP = await getCDP();
 
   const client = await CDP({
     target(list) {
       // ensure we are debugging the right target, i.e. the requested URL
       return list.find(target => target.url == url);
@@ -152,8 +153,20 @@ function toDataURL(src, doctype = "html"
 function getContentProperty(prop) {
   info(`Retrieve ${prop} on the content window`);
   return ContentTask.spawn(
     gBrowser.selectedBrowser,
     prop,
     _prop => content[_prop]
   );
 }
+
+/**
+ * Close tabs, client, remote agent.
+ */
+async function teardown(client) {
+  await client.close();
+  ok(true, "The client is closed");
+  while (gBrowser.tabs.length > 1) {
+    gBrowser.removeCurrentTab();
+  }
+  await RemoteAgent.close();
+}