Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
- Manifest: editor/libeditor/tests/mochitest.toml
 
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Test for contenteditable focus</title>
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<div id="display">
  First text in this document.<br>
  <input id="inputText" type="text"><br>
  <input id="inputTextReadonly" type="text" readonly><br>
  <input id="inputButton" type="button" value="input[type=button]"><br>
  <button id="button">button</button><br>
  <div id="primaryEditor" contenteditable="true">
    editable contents.<br>
    <input id="inputTextInEditor" type="text"><br>
    <input id="inputTextReadonlyInEditor" type="text" readonly><br>
    <input id="inputButtonInEditor" type="button" value="input[type=button]"><br>
    <button id="buttonInEditor">button</button><br>
    <div id="noeditableInEditor" contenteditable="false">
      <span id="spanInNoneditableInEditor">span element in noneditable in editor</span><br>
      <input id="inputTextInNoneditableInEditor" type="text"><br>
      <input id="inputTextReadonlyInNoneditableInEditor" type="text" readonly><br>
      <input id="inputButtonInNoneditableInEditor" type="button" value="input[type=button]"><br>
      <button id="buttonInNoneditableInEditor">button</button><br>
    </div>
    <span id="spanInEditor">span element in editor</span><br>
  </div>
  <div id="otherEditor" contenteditable="true">
    other editor.
  </div>
</div>
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
<script>
"use strict";
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(() => {
  function getNodeDescription(aNode) {
    if (aNode === undefined) {
      return "undefined";
    }
    if (aNode === null) {
      return "null";
    }
    switch (aNode.nodeType) {
      case Node.TEXT_NODE:
        return `${aNode.nodeName}, "${aNode.data.replace(/\n/g, "\\n")}"`;
      case Node.ELEMENT_NODE:
        return `<${aNode.tagName.toLowerCase()}${
          aNode.getAttribute("id") !== null
            ? ` id="${aNode.getAttribute("id")}"`
            : ""
        }${
          aNode.getAttribute("readonly") !== null
            ? " readonly"
            : ""
        }${
          aNode.getAttribute("type") !== null
            ? ` type="${aNode.getAttribute("type")}"`
            : ""
        }>`;
    }
    return aNode.nodeName;
  }
  const fm = SpecialPowers.Services.focus;
  // XXX using selCon for checking the visibility of the caret, however,
  // selCon is shared in document, cannot get the element of owner of the
  // caret from javascript?
  const selCon = SpecialPowers.wrap(window).docShell.
        QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
        getInterface(SpecialPowers.Ci.nsISelectionDisplay).
        QueryInterface(SpecialPowers.Ci.nsISelectionController);
  const primaryEditor = document.getElementById("primaryEditor");
  const spanInEditor = document.getElementById("spanInEditor");
  const otherEditor = document.getElementById("otherEditor");
  (function test_initial_state_on_load() {
    is(
      getSelection().rangeCount,
      0,
      "There should be no selection range at start"
    );
    ok(!selCon.caretVisible, "The caret should not be visible in the document");
    // Move focus to <input type="text"> in the primary editor
    primaryEditor.querySelector("input[type=text]").focus();
    is(
      SpecialPowers.unwrap(fm.focusedElement),
      primaryEditor.querySelector("input[type=text]"),
      '<input type="text"> in the primary editor should get focus'
    );
    todo_is(
      getSelection().rangeCount,
      0,
      'There should be no selection range after calling focus() of <input type="text"> in the primary editor'
    );
    ok(
      selCon.caretVisible,
      'The caret should not be visible in the <input type="text"> in the primary editor'
    );
  })();
  // Move focus to the editor
  (function test_move_focus_from_child_input_to_parent_editor() {
    primaryEditor.focus();
    is(
      SpecialPowers.unwrap(fm.focusedElement),
      primaryEditor,
      `The editor should steal focus from <input type="text"> in the primary editor with calling its focus() (got ${
        getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
      }`
    );
    is(
      getSelection().rangeCount,
      1,
      "There should be one range after focus() of the editor is called"
    );
    const range = getSelection().getRangeAt(0);
    ok(
      range.collapsed,
      "The selection range should be collapsed (immediately after calling focus() of the editor)"
    );
    is(
      range.startContainer,
      primaryEditor.firstChild,
      `The selection range should be in the first text node of the editor (immediately after calling focus() of the editor, got ${
        getNodeDescription(range.startContainer)
      })`
    );
    ok(
      selCon.caretVisible,
      "The caret should be visible in the primary editor (immediately after calling focus() of the editor)"
    );
  })();
  // Move focus to other editor
  (function test_move_focus_from_editor_to_the_other_editor() {
    otherEditor.focus();
    is(
      SpecialPowers.unwrap(fm.focusedElement),
      otherEditor,
      `The other editor should steal focus from the editor (got ${
        getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
      }`
    );
    is(
      getSelection().rangeCount,
      1,
      "There should be one range after focus() of the other editor is called"
    );
    const range = getSelection().getRangeAt(0);
    ok(
      range.collapsed,
      "The selection range should be collapsed (immediately after calling focus() of the other editor)"
    );
    is(
      range.startContainer,
      otherEditor.firstChild,
      `The selection range should be in the first text node of the editor (immediately after calling focus() of the other editor, got ${
        getNodeDescription(range.startContainer)
      })`
    );
    ok(
      selCon.caretVisible,
      "The caret should be visible in the primary editor (immediately after calling focus() of the other editor)"
    );
  })();
  // Move focus to <input type="text"> in the primary editor
  (function test_move_focus_from_the_other_editor_to_input_in_the_editor() {
    primaryEditor.querySelector("input[type=text]").focus();
    is(
      SpecialPowers.unwrap(fm.focusedElement),
      primaryEditor.querySelector("input[type=text]"),
      `<input type="text"> in the primary editor should steal focus from the other editor (got ${
        getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
      }`);
    is(
      getSelection().rangeCount,
      1,
      'There should be one range after focus() of the <input type="text"> in the primary editor is called'
    );
    const range = getSelection().getRangeAt(0);
    ok(
      range.collapsed,
      'The selection range should be collapsed (immediately after calling focus() of the <input type="text"> in the primary editor)'
    );
    // XXX maybe, the caret can stay on the other editor if it's better.
    is(
      range.startContainer,
      primaryEditor.firstChild,
      `The selection range should be in the first text node of the editor (immediately after calling focus() of the <input type="text"> in the primary editor, got ${
        getNodeDescription(range.startContainer)
      })`
    );
    ok(
      selCon.caretVisible,
      'The caret should be visible in the <input type="text"> (immediately after calling focus() of the <input type="text"> in the primary editor)'
    );
  })();
  // Move focus to the other editor again
  (function test_move_focus_from_the_other_editor_to_the_editor_with_Selection_API() {
    otherEditor.focus();
    is(
      SpecialPowers.unwrap(fm.focusedElement),
      otherEditor,
      `The other editor should steal focus from the <input type="text"> in the primary editor with its focus() (got ${
        getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
      })`
    );
    // Set selection to the span element in the primary editor.
    getSelection().collapse(spanInEditor.firstChild, 5);
    is(
      getSelection().rangeCount,
      1,
      "There should be one selection range after collapsing selection into the <span> in the primary editor when the other editor has focus"
    );
    is(
      SpecialPowers.unwrap(fm.focusedElement),
      primaryEditor,
      `The editor should steal focus from the other editor with Selection API (got ${
        getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
      }`
    );
    ok(
      selCon.caretVisible,
      "The caret should be visible in the primary editor (immediately after moving focus with Selection API)"
    );
  })();
  // Move focus to the editor
  (function test_move_focus() {
    primaryEditor.focus();
    is(
      SpecialPowers.unwrap(fm.focusedElement),
      primaryEditor,
      "The editor should keep having focus (immediately after calling focus() of the editor when it has focus)"
    );
    is(
      getSelection().rangeCount,
      1,
      "There should be one selection range in the primary editor (immediately after calling focus() of the editor when it has focus)"
    );
    const range = getSelection().getRangeAt(0);
    ok(
      range.collapsed,
      "The selection range should be collapsed in the primary editor (immediately after calling focus() of the editor when it has focus)"
    );
    is(
      range.startOffset,
      5,
      "The startOffset of the selection range shouldn't be changed (immediately after calling focus() of the editor when it has focus)"
    );
    is(
      range.startContainer.parentNode,
      spanInEditor,
      `The startContainer of the selection range shouldn't be changed (immediately after calling focus() of the editor when it has focus, got ${
        getNodeDescription(range.startContainer)
      })`);
    ok(
      selCon.caretVisible,
      "The caret should be visible in the primary editor (immediately after calling focus() of the editor when it has focus)"
    );
  })();
  // Move focus to each focusable element in the primary editor.
  function test_move_focus_from_the_editor(aTargetElement, aFocusable, aCaretVisible) {
    primaryEditor.focus();
    is(
      SpecialPowers.unwrap(fm.focusedElement),
      primaryEditor,
      `The editor should have focus at preparing to move focus to ${
        getNodeDescription(aTargetElement)
      } (got ${
        getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
      }`);
    aTargetElement.focus();
    if (aFocusable) {
      is(
        SpecialPowers.unwrap(fm.focusedElement),
        aTargetElement,
        `${
          getNodeDescription(aTargetElement)
        } should get focus with calling its focus() (got ${
          getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
        }`
      );
    } else {
      is(
        SpecialPowers.unwrap(fm.focusedElement),
        primaryEditor,
        `${
          getNodeDescription(aTargetElement)
        } should not take focus with calling its focus() (got ${
          getNodeDescription(SpecialPowers.unwrap(fm.focusedElement))
        }`
      );
    }
    is(
      selCon.caretVisible,
      aCaretVisible,
      `The caret ${
        aCaretVisible ? "should" : "should not"
      } visible after calling focus() of ${
          getNodeDescription(aTargetElement)
      }`
    );
  }
  test_move_focus_from_the_editor(primaryEditor.querySelector("input[type=text]"), true, true);
  test_move_focus_from_the_editor(primaryEditor.querySelector("input[type=text][readonly]"), true, true);
  // XXX shouldn't the caret become invisible?
  test_move_focus_from_the_editor(primaryEditor.querySelector("input[type=button]"), true, true);
  test_move_focus_from_the_editor(primaryEditor.querySelector("button"), true, true);
  test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false]"), false, true);
  test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > span"), false, true);
  test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > input[type=text]"), true, true);
  test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > input[type=text][readonly]"), true, true);
  test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > input[type=button]"), true, false);
  test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > button"), true, false);
  test_move_focus_from_the_editor(spanInEditor, false, true);
  test_move_focus_from_the_editor(document.querySelector("input[type=text]"), true, true);
  test_move_focus_from_the_editor(document.querySelector("input[type=text][readonly]"), true, true);
  test_move_focus_from_the_editor(document.querySelector("input[type=button]"), true, false);
  test_move_focus_from_the_editor(document.querySelector("button"), true, false);
  SimpleTest.finish();
});
</script>
</body>
</html>