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>