Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Testing editable state and focus in shadow DOM in design mode</title>
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="../include/editor-test-utils.js"></script>
</head>
<body>
<h3>open</h3>
<my-shadow data-mode="open"></my-shadow>
<h3>closed</h3>
<my-shadow data-mode="closed"></my-shadow>
<script>
"use strict";
document.designMode = "on";
const utils = new EditorTestUtils(document.body);
class MyShadow extends HTMLElement {
#defaultInnerHTML =
"<style>:focus { outline: 3px red solid; }</style>" +
"<div>text" +
"<div contenteditable=\"\">editable</div>" +
"<object tabindex=\"0\">object</object>" +
"<p tabindex=\"0\">paragraph</p>" +
"</div>";
#shadowRoot;
constructor() {
super();
this.#shadowRoot = this.attachShadow({mode: this.getAttribute("data-mode")});
this.#shadowRoot.innerHTML = this.#defaultInnerHTML;
}
reset() {
this.#shadowRoot.innerHTML = this.#defaultInnerHTML;
this.#shadowRoot.querySelector("div").getBoundingClientRect();
}
focusText() {
this.focus();
const div = this.#shadowRoot.querySelector("div");
getSelection().collapse(div.firstChild || div, 0);
}
focusContentEditable() {
this.focus();
const contenteditable = this.#shadowRoot.querySelector("div[contenteditable]");
contenteditable.focus();
getSelection().collapse(contenteditable.firstChild || contenteditable, 0);
}
focusObject() {
this.focus();
this.#shadowRoot.querySelector("object[tabindex]").focus();
}
focusParagraph() {
this.focus();
const tabbableP = this.#shadowRoot.querySelector("p[tabindex]");
tabbableP.focus();
getSelection().collapse(tabbableP.firstChild || tabbableP, 0);
}
getInnerHTML() {
return this.#shadowRoot.innerHTML;
}
getDefaultInnerHTML() {
return this.#defaultInnerHTML;
}
getFocusedElementName() {
return this.#shadowRoot.querySelector(":focus")?.tagName.toLocaleLowerCase() || "";
}
getSelectedRange() {
// XXX There is no standardized way to retrieve selected ranges in
// shadow trees, therefore, we use non-standardized API for now
// since the main purpose of this test is checking the behavior of
// selection changes in shadow trees, not checking the selection API.
const selection =
this.#shadowRoot.getSelection !== undefined
? this.#shadowRoot.getSelection()
: getSelection();
return selection.getRangeAt(0);
}
}
customElements.define("my-shadow", MyShadow);
function getRangeDescription(range) {
function getNodeDescription(node) {
if (!node) {
return "null";
}
switch (node.nodeType) {
case Node.TEXT_NODE:
case Node.COMMENT_NODE:
case Node.CDATA_SECTION_NODE:
return `${node.nodeName} "${node.data}"`;
case Node.ELEMENT_NODE:
return `<${node.nodeName.toLowerCase()}>`;
default:
return `${node.nodeName}`;
}
}
if (range === null) {
return "null";
}
if (range === undefined) {
return "undefined";
}
return range.startContainer == range.endContainer &&
range.startOffset == range.endOffset
? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})`
: `(${getNodeDescription(range.startContainer)}, ${
range.startOffset
}) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`;
}
promise_test(async () => {
await new Promise(resolve => addEventListener("load", resolve, {once: true}));
assert_true(true, "Load event is fired");
}, "Waiting for load");
/**
* The expected result of this test is based on Blink and Gecko's behavior.
*/
for (const mode of ["open", "closed"]) {
const host = document.querySelector(`my-shadow[data-mode=${mode}]`);
promise_test(async (t) => {
host.reset();
host.focusText();
test(() => {
assert_equals(
host.getFocusedElementName(),
"",
`No element should have focus after ${t.name}`
);
}, `Focus after ${t.name}`);
await utils.sendKey("A");
test(() => {
assert_equals(
host.getInnerHTML(),
host.getDefaultInnerHTML(),
`The shadow DOM shouldn't be modified after ${t.name}`
);
}, `Typing "A" after ${t.name}`);
}, `Collapse selection into text in the ${mode} shadow DOM`);
promise_test(async (t) => {
host.reset();
host.focusContentEditable();
test(() => {
assert_equals(
host.getFocusedElementName(),
"div",
`<div contenteditable> should have focus after ${t.name}`
);
}, `Focus after ${t.name}`);
await utils.sendKey("A");
test(() => {
assert_equals(
host.getInnerHTML(),
host.getDefaultInnerHTML().replace("<div contenteditable=\"\">", "<div contenteditable=\"\">A"),
`The shadow DOM shouldn't be modified after ${t.name}`
);
}, `Typing "A" after ${t.name}`);
}, `Collapse selection into text in <div contenteditable> in the ${mode} shadow DOM`);
promise_test(async (t) => {
host.reset();
host.focusObject();
test(() => {
assert_equals(
host.getFocusedElementName(),
"object",
`The <object> element should have focus after ${t.name}`
);
}, `Focus after ${t.name}`);
await utils.sendKey("A");
test(() => {
assert_equals(
host.getInnerHTML(),
host.getDefaultInnerHTML(),
`The shadow DOM shouldn't be modified after ${t.name}`
);
}, `Typing "A" after ${t.name}`);
}, `Set focus to <object> in the ${mode} shadow DOM`);
promise_test(async (t) => {
host.reset();
host.focusParagraph();
test(() => {
assert_equals(
host.getFocusedElementName(),
"p",
`The <p tabindex="0"> element should have focus after ${t.name}`
);
}, `Focus after ${t.name}`);
await utils.sendKey("A");
test(() => {
assert_equals(
host.getInnerHTML(),
host.getDefaultInnerHTML(),
`The shadow DOM shouldn't be modified after ${t.name}`
);
}, `Typing "A" after ${t.name}`);
}, `Set focus to <p tabindex="0"> in the ${mode} shadow DOM`);
promise_test(async (t) => {
host.reset();
host.focusParagraph();
await utils.sendSelectAllShortcutKey();
assert_in_array(
getRangeDescription(host.getSelectedRange()),
[
// Feel free to add reasonable select all result in the <my-shadow>.
"(#document-fragment, 0) - (#document-fragment, 2)",
"(#text \"text\", 0) - (#text \"paragraph\", 9)",
],
`Only all children of the ${mode} shadow DOM should be selected`
);
getSelection().collapse(document.body, 0);
}, `SelectAll in the ${mode} shadow DOM`);
promise_test(async (t) => {
host.reset();
host.focusContentEditable();
await utils.sendSelectAllShortcutKey();
assert_in_array(
getRangeDescription(host.getSelectedRange()),
[
// Feel free to add reasonable select all result in the <div contenteditable>.
"(<div>, 0) - (<div>, 1)",
"(#text \"editable\", 0) - (#text \"editable\", 8)",
]
);
getSelection().collapse(document.body, 0);
}, `SelectAll in the <div contenteditable> in the ${mode} shadow DOM`);
}
</script>
</body>
</html>