tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

editable-state-and-focus-in-shadow-dom-in-designMode.tentative.html (7585B)


      1 <!doctype html>
      2 <html>
      3 <head>
      4 <meta charset="utf-8">
      5 <title>Testing editable state and focus in shadow DOM in design mode</title>
      6 <script src=/resources/testharness.js></script>
      7 <script src=/resources/testharnessreport.js></script>
      8 <script src="/resources/testdriver.js"></script>
      9 <script src="/resources/testdriver-vendor.js"></script>
     10 <script src="/resources/testdriver-actions.js"></script>
     11 <script src="../include/editor-test-utils.js"></script>
     12 </head>
     13 <body>
     14 <h3>open</h3>
     15 <my-shadow data-mode="open"></my-shadow>
     16 <h3>closed</h3>
     17 <my-shadow data-mode="closed"></my-shadow>
     18 
     19 <script>
     20 "use strict";
     21 
     22 document.designMode = "on";
     23 const utils = new EditorTestUtils(document.body);
     24 
     25 class MyShadow extends HTMLElement {
     26  #defaultInnerHTML =
     27    "<style>:focus { outline: 3px red solid; }</style>" +
     28    "<div>text" +
     29      "<div contenteditable=\"\">editable</div>" +
     30      "<object tabindex=\"0\">object</object>" +
     31      "<p tabindex=\"0\">paragraph</p>" +
     32    "</div>";
     33  #shadowRoot;
     34 
     35  constructor() {
     36    super();
     37    this.#shadowRoot = this.attachShadow({mode: this.getAttribute("data-mode")});
     38    this.#shadowRoot.innerHTML = this.#defaultInnerHTML;
     39  }
     40 
     41  reset() {
     42    this.#shadowRoot.innerHTML = this.#defaultInnerHTML;
     43    this.#shadowRoot.querySelector("div").getBoundingClientRect();
     44  }
     45 
     46  focusText() {
     47    this.focus();
     48    const div = this.#shadowRoot.querySelector("div");
     49    getSelection().collapse(div.firstChild || div, 0);
     50  }
     51 
     52  focusContentEditable() {
     53    this.focus();
     54    const contenteditable = this.#shadowRoot.querySelector("div[contenteditable]");
     55    contenteditable.focus();
     56    getSelection().collapse(contenteditable.firstChild || contenteditable, 0);
     57  }
     58 
     59  focusObject() {
     60    this.focus();
     61    this.#shadowRoot.querySelector("object[tabindex]").focus();
     62  }
     63 
     64  focusParagraph() {
     65    this.focus();
     66    const tabbableP = this.#shadowRoot.querySelector("p[tabindex]");
     67    tabbableP.focus();
     68    getSelection().collapse(tabbableP.firstChild || tabbableP, 0);
     69  }
     70 
     71  getInnerHTML() {
     72    return this.#shadowRoot.innerHTML;
     73  }
     74 
     75  getDefaultInnerHTML() {
     76    return this.#defaultInnerHTML;
     77  }
     78 
     79  getFocusedElementName() {
     80    return this.#shadowRoot.querySelector(":focus")?.tagName.toLocaleLowerCase() || "";
     81  }
     82 
     83  getSelectedRange() {
     84    // XXX There is no standardized way to retrieve selected ranges in
     85    //     shadow trees, therefore, we use non-standardized API for now
     86    //     since the main purpose of this test is checking the behavior of
     87    //     selection changes in shadow trees, not checking the selection API.
     88    const selection =
     89      this.#shadowRoot.getSelection !== undefined
     90        ? this.#shadowRoot.getSelection()
     91        : getSelection();
     92    return selection.getRangeAt(0);
     93  }
     94 }
     95 
     96 customElements.define("my-shadow", MyShadow);
     97 
     98 function getRangeDescription(range) {
     99  function getNodeDescription(node) {
    100    if (!node) {
    101      return "null";
    102    }
    103    switch (node.nodeType) {
    104      case Node.TEXT_NODE:
    105      case Node.COMMENT_NODE:
    106      case Node.CDATA_SECTION_NODE:
    107        return `${node.nodeName} "${node.data}"`;
    108      case Node.ELEMENT_NODE:
    109        return `<${node.nodeName.toLowerCase()}>`;
    110      default:
    111        return `${node.nodeName}`;
    112    }
    113  }
    114  if (range === null) {
    115    return "null";
    116  }
    117  if (range === undefined) {
    118    return "undefined";
    119  }
    120  return range.startContainer == range.endContainer &&
    121    range.startOffset == range.endOffset
    122    ? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})`
    123    : `(${getNodeDescription(range.startContainer)}, ${
    124        range.startOffset
    125      }) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`;
    126 }
    127 
    128 promise_test(async () => {
    129  await new Promise(resolve => addEventListener("load", resolve, {once: true}));
    130  assert_true(true, "Load event is fired");
    131 }, "Waiting for load");
    132 
    133 /**
    134 * The expected result of this test is based on Blink and Gecko's behavior.
    135 */
    136 
    137 for (const mode of ["open", "closed"]) {
    138  const host = document.querySelector(`my-shadow[data-mode=${mode}]`);
    139  promise_test(async (t) => {
    140    host.reset();
    141    host.focusText();
    142    test(() => {
    143      assert_equals(
    144        host.getFocusedElementName(),
    145        "",
    146        `No element should have focus after ${t.name}`
    147      );
    148    }, `Focus after ${t.name}`);
    149    await utils.sendKey("A");
    150    test(() => {
    151      assert_equals(
    152        host.getInnerHTML(),
    153        host.getDefaultInnerHTML(),
    154        `The shadow DOM shouldn't be modified after ${t.name}`
    155      );
    156    }, `Typing "A" after ${t.name}`);
    157  }, `Collapse selection into text in the ${mode} shadow DOM`);
    158 
    159  promise_test(async (t) => {
    160    host.reset();
    161    host.focusContentEditable();
    162    test(() => {
    163      assert_equals(
    164        host.getFocusedElementName(),
    165        "div",
    166        `<div contenteditable> should have focus after ${t.name}`
    167      );
    168    }, `Focus after ${t.name}`);
    169    await utils.sendKey("A");
    170    test(() => {
    171      assert_equals(
    172        host.getInnerHTML(),
    173        host.getDefaultInnerHTML().replace("<div contenteditable=\"\">", "<div contenteditable=\"\">A"),
    174        `The shadow DOM shouldn't be modified after ${t.name}`
    175      );
    176    }, `Typing "A" after ${t.name}`);
    177  }, `Collapse selection into text in <div contenteditable> in the ${mode} shadow DOM`);
    178 
    179  promise_test(async (t) => {
    180    host.reset();
    181    host.focusObject();
    182    test(() => {
    183      assert_equals(
    184        host.getFocusedElementName(),
    185        "object",
    186        `The <object> element should have focus after ${t.name}`
    187      );
    188    }, `Focus after ${t.name}`);
    189    await utils.sendKey("A");
    190    test(() => {
    191      assert_equals(
    192        host.getInnerHTML(),
    193        host.getDefaultInnerHTML(),
    194        `The shadow DOM shouldn't be modified after ${t.name}`
    195      );
    196    }, `Typing "A" after ${t.name}`);
    197  }, `Set focus to <object> in the ${mode} shadow DOM`);
    198 
    199  promise_test(async (t) => {
    200    host.reset();
    201    host.focusParagraph();
    202    test(() => {
    203      assert_equals(
    204        host.getFocusedElementName(),
    205        "p",
    206        `The <p tabindex="0"> element should have focus after ${t.name}`
    207      );
    208    }, `Focus after ${t.name}`);
    209    await utils.sendKey("A");
    210    test(() => {
    211      assert_equals(
    212        host.getInnerHTML(),
    213        host.getDefaultInnerHTML(),
    214        `The shadow DOM shouldn't be modified after ${t.name}`
    215      );
    216    }, `Typing "A" after ${t.name}`);
    217  }, `Set focus to <p tabindex="0"> in the ${mode} shadow DOM`);
    218 
    219  promise_test(async (t) => {
    220    host.reset();
    221    host.focusParagraph();
    222    await utils.sendSelectAllShortcutKey();
    223    assert_in_array(
    224      getRangeDescription(host.getSelectedRange()),
    225      [
    226        // Feel free to add reasonable select all result in the <my-shadow>.
    227        "(#document-fragment, 0) - (#document-fragment, 2)",
    228        "(#text \"text\", 0) - (#text \"paragraph\", 9)",
    229      ],
    230      `Only all children of the ${mode} shadow DOM should be selected`
    231    );
    232    getSelection().collapse(document.body, 0);
    233  }, `SelectAll in the ${mode} shadow DOM`);
    234 
    235  promise_test(async (t) => {
    236    host.reset();
    237    host.focusContentEditable();
    238    await utils.sendSelectAllShortcutKey();
    239    assert_in_array(
    240      getRangeDescription(host.getSelectedRange()),
    241      [
    242        // Feel free to add reasonable select all result in the <div contenteditable>.
    243        "(<div>, 0) - (<div>, 1)",
    244        "(#text \"editable\", 0) - (#text \"editable\", 8)",
    245      ]
    246    );
    247    getSelection().collapse(document.body, 0);
    248  }, `SelectAll in the <div contenteditable> in the ${mode} shadow DOM`);
    249 }
    250 </script>
    251 </body>
    252 </html>