summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/editing/edit-context
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/editing/edit-context
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/editing/edit-context')
-rw-r--r--testing/web-platform/tests/editing/edit-context/edit-context-basics.tentative.html196
-rw-r--r--testing/web-platform/tests/editing/edit-context/edit-context-execCommand.tentative.https.html111
-rw-r--r--testing/web-platform/tests/editing/edit-context/edit-context-inheritability.tentative.html182
-rw-r--r--testing/web-platform/tests/editing/edit-context/edit-context-input.tentative.html249
-rw-r--r--testing/web-platform/tests/editing/edit-context/edit-context-property.tentative.html90
5 files changed, 828 insertions, 0 deletions
diff --git a/testing/web-platform/tests/editing/edit-context/edit-context-basics.tentative.html b/testing/web-platform/tests/editing/edit-context/edit-context-basics.tentative.html
new file mode 100644
index 0000000000..78b6824921
--- /dev/null
+++ b/testing/web-platform/tests/editing/edit-context/edit-context-basics.tentative.html
@@ -0,0 +1,196 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>EditContext: The HTMLElement.editContext property</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src='../../html/resources/common.js'></script>
+</head>
+<body>
+ <div id="test"></div>
+ <div id="contenteditableDiv" contenteditable></div>
+ <script>
+ test(function() {
+ const editContextDict = {
+ text: "Hello world",
+ selectionStart: 11,
+ selectionEnd: 11
+ };
+ const editContext = new EditContext(editContextDict);
+ assert_not_equals(editContext, null);
+ // Verify all the members of the EditContext
+ assert_equals(editContext.text, "Hello world");
+ assert_equals(editContext.selectionStart, 11);
+ assert_equals(editContext.selectionEnd, 11);
+ }, 'Testing EditContext Dictionary Init');
+
+ test(function() {
+ contenteditableDiv.editContext = new EditContext();
+ contenteditableDiv.editContext = null;
+ contenteditableDiv.focus();
+ assert_equals(document.activeElement, contenteditableDiv);
+ }, 'A contenteditable element should remain editable after attaching and detaching EditContext.');
+
+ test(function() {
+ const editContext = new EditContext();
+ assert_not_equals(editContext, null);
+
+ const disconnected_div = document.createElement("DIV");
+ assert_equals(disconnected_div.editContext, null);
+
+ disconnected_div.editContext = editContext;
+ assert_equals(disconnected_div.editContext, editContext);
+ assert_equals(editContext.attachedElements().length, 1);
+ assert_equals(editContext.attachedElements()[0], disconnected_div);
+ }, 'EditContext can be associated with an element that is not in the tree.');
+
+ test(function() {
+ const editContext = new EditContext();
+ assert_not_equals(editContext, null);
+
+ const div = document.createElement("DIV");
+ assert_equals(div.editContext, null);
+
+ document.body.appendChild(div);
+ div.editContext = editContext;
+ assert_equals(div.editContext, editContext);
+ assert_equals(editContext.attachedElements().length, 1);
+ assert_equals(editContext.attachedElements()[0], div);
+
+ document.body.removeChild(div);
+ assert_equals(div.editContext, editContext);
+ assert_equals(editContext.attachedElements().length, 1);
+ assert_equals(editContext.attachedElements()[0], div);
+ }, 'If an element is removed from the tree, the associated EditContext remains connected to the element.');
+
+ test(function() {
+ const editContext = new EditContext();
+
+ const div_parent = document.createElement("DIV");
+ const div_child = document.createElement("DIV");
+ document.body.appendChild(div_parent);
+ div_parent.appendChild(div_child);
+
+ div_child.editContext = editContext;
+ assert_equals(div_child.editContext, editContext);
+ assert_equals(div_parent.editContext, null);
+ assert_equals(editContext.attachedElements().length, 1);
+ assert_equals(editContext.attachedElements()[0], div_child);
+
+ document.body.removeChild(div_parent);
+ assert_equals(div_child.editContext, editContext);
+ assert_equals(editContext.attachedElements().length, 1);
+ assert_equals(editContext.attachedElements()[0], div_child);
+ }, 'If an element\'s ancestor is removed from tree, the associated EditContext remains connected to the element.');
+
+ test(function() {
+ const editContext = new EditContext();
+ const test = document.getElementById("test");
+
+ test.editContext = editContext;
+
+ assert_equals(test.editContext, editContext);
+ assert_equals(editContext.attachedElements().length, 1);
+ assert_equals(editContext.attachedElements()[0], test);
+
+ test.editContext = null;
+
+ assert_equals(editContext.attachedElements().length, 0);
+ }, '.attachedElements() should return associated element');
+
+ test(function() {
+ const editContext = new EditContext();
+ assert_not_equals(editContext, null);
+ editContext.updateText(0, 3, "foo");
+ assert_equals(editContext.text, "foo");
+ const test = document.getElementById('test');
+ // Update the layout of the |EditContext|
+ var viewRect = test.getBoundingClientRect();
+ viewRect.x = viewRect.left;
+ viewRect.y = viewRect.top;
+ var caretRect = test.getBoundingClientRect();
+ caretRect.x = caretRect.left;
+ caretRect.y = 2.2 * caretRect.top;
+ caretRect.width = 1;
+ editContext.updateSelection(0, 0);
+ assert_equals(editContext.selectionStart, 0);
+ assert_equals(editContext.selectionEnd, 0);
+ editContext.updateSelection(1, 0);
+ assert_equals(editContext.selectionStart, 1);
+ assert_equals(editContext.selectionEnd, 0);
+ editContext.updateSelection(0, 1);
+ assert_equals(editContext.selectionStart, 0);
+ assert_equals(editContext.selectionEnd, 1);
+ editContext.updateSelection(1, 1);
+ assert_equals(editContext.selectionStart, 1);
+ assert_equals(editContext.selectionEnd, 1);
+ editContext.updateControlBounds(viewRect);
+ editContext.updateSelectionBounds(caretRect);
+ editContext.updateCharacterBounds(0, [caretRect]);
+
+ assert_throws_js(TypeError, function() { editContext.updateControlBounds(42); });
+ assert_throws_js(TypeError, function() { editContext.updateSelectionBounds(42); });
+ assert_throws_js(TypeError, function() { editContext.updateControlBounds(undefined); });
+ assert_throws_js(TypeError, function() { editContext.updateSelectionBounds(undefined); });
+ assert_throws_js(TypeError, function() { editContext.updateCharacterBounds(0); });
+ assert_throws_js(TypeError, function() { editContext.updateCharacterBounds([caretRect]); });
+ assert_throws_js(TypeError, function() { editContext.updateCharacterBounds(0, caretRect); });
+ assert_throws_js(TypeError, function() { editContext.updateCharacterBounds(0, 42); });
+ assert_throws_js(TypeError, function() { editContext.updateCharacterBounds(0, undefined); });
+ assert_throws_js(TypeError, function() { editContext.updateCharacterBounds(0, [undefined]); });
+
+ viewRect.x = viewRect.y = viewRect.width = viewRect.height = undefined;
+ editContext.updateControlBounds(viewRect);
+ editContext.updateSelectionBounds(viewRect);
+ editContext.updateCharacterBounds(0, [viewRect]);
+ }, 'Testing EditContext update text, selection and layout');
+
+ test(function() {
+ const editContext = new EditContext();
+ const test = document.getElementById('test');
+ var rect1 = DOMRect.fromRect({x:0, y:1, width:100, height:200});
+ var rect2 = DOMRect.fromRect({x:2, y:3, width:300, height:400});
+ var rectArray = [rect1, rect2];
+ var rangeStart = 2;
+ editContext.updateCharacterBounds(rangeStart, rectArray);
+ assert_equals(editContext.characterBoundsRangeStart, 2);
+
+ var actualRectArray = editContext.characterBounds();
+ assert_equals(actualRectArray.length, 2);
+ assert_equals(actualRectArray[0].x, 0);
+ assert_equals(actualRectArray[0].y, 1);
+ assert_equals(actualRectArray[0].width, 100);
+ assert_equals(actualRectArray[0].height, 200);
+ rect2.x=100;
+ assert_equals(actualRectArray[1].x, 2); // the cached value shouldn't change.
+ assert_equals(actualRectArray[1].y, 3);
+ assert_equals(actualRectArray[1].width, 300);
+ assert_equals(actualRectArray[1].height, 400);
+ }, 'updateCharacterBounds(), characterBounds(), and characterBoundsRangeStart should work properly');
+
+ // The behavior in this test case is not well-defined in the spec.
+ // See https://github.com/w3c/edit-context/issues/88
+ // test(function() {
+ // const editContext = new EditContext();
+ // assert_not_equals(editContext, null);
+ // editContext.updateText(0, 3, "foo");
+ // assert_equals(editContext.text, "foo");
+ // assert_throws_dom("IndexSizeError", function() { editContext.updateSelection(10, 0); });
+ // assert_equals(editContext.selectionStart, 0);
+ // assert_equals(editContext.selectionEnd, 0);
+ // assert_throws_dom("IndexSizeError", function() { editContext.updateText(10, 1, "h"); });
+ // assert_equals(editContext.text, "foo");
+ // }, 'Testing EditContext update text and selection with invalid values');
+
+ test(function() {
+ const editContext = new EditContext();
+ assert_not_equals(editContext, null);
+ editContext.updateText(0, 3, "foo");
+ assert_equals(editContext.text, "foo");
+ editContext.updateSelection(3, 0);
+ assert_equals(editContext.selectionStart, 3);
+ assert_equals(editContext.selectionEnd, 0);
+ }, 'EditContext should allow a backwards selection');
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/editing/edit-context/edit-context-execCommand.tentative.https.html b/testing/web-platform/tests/editing/edit-context/edit-context-execCommand.tentative.https.html
new file mode 100644
index 0000000000..daa9df3cce
--- /dev/null
+++ b/testing/web-platform/tests/editing/edit-context/edit-context-execCommand.tentative.https.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>EditContext: document.execCommand</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../../clipboard-apis/resources/user-activation.js"></script>
+</head>
+<body>
+ <script>
+ promise_test(async function() {
+ const editContext = new EditContext();
+ const test = document.createElement("div");
+ document.body.appendChild(test);
+ let firedTextUpdate = false;
+ editContext.addEventListener("textupdate", e => {
+ firedTextUpdate = true;
+ });
+ test.editContext = editContext;
+ test.focus();
+
+ assert_true(document.queryCommandSupported("inserttext"), "'inserttext' should be supported regardless of focus position");
+ assert_false(document.queryCommandEnabled("inserttext"), "'inserttext' should not be supported in EditContext");
+
+ document.execCommand("inserttext", false, "a");
+ assert_equals(test.innerHTML, "", "DOM should not be updated from execCommand('inserttext')");
+ assert_false(firedTextUpdate, "textupdate should not fire for to execCommand('inserttext')");
+ test.remove();
+ }, 'document.execCommand("inserttext") should not change the DOM or fire textupdate');
+
+ promise_test(async function() {
+ const editContext = new EditContext();
+ const test = document.createElement("div");
+ test.innerHTML = "abc";
+ document.body.appendChild(test);
+ let firedTextUpdate = false;
+ editContext.addEventListener("textupdate", e => {
+ firedTextUpdate = true;
+ });
+ test.editContext = editContext;
+ test.focus();
+
+ assert_true(document.queryCommandSupported("bold"), "'bold' should be supported regardless of focus position");
+ assert_false(document.queryCommandEnabled("bold"), "'bold' should not be supported in EditContext");
+
+ document.execCommand("bold");
+ assert_equals(test.innerHTML, "abc", "DOM should not be updated from execCommand('bold')");
+ assert_false(firedTextUpdate, "textupdate should not fire for execCommand('bold')");
+ test.remove();
+ }, 'document.execCommand("bold") should not change the DOM or fire textupdate');
+
+ promise_test(async function() {
+ const editContext = new EditContext();
+ const test = document.createElement("div");
+ test.innerHTML = "<b>ab</b>c";
+ document.body.appendChild(test);
+ let firedTextUpdate = false;
+ editContext.addEventListener("textupdate", e => {
+ firedTextUpdate = true;
+ });
+ test.editContext = editContext;
+ const selection = window.getSelection();
+ selection.setBaseAndExtent(test.firstChild.firstChild, 0, test.firstChild.firstChild, 1);
+
+ assert_false(document.queryCommandState("bold"), "queryCommandState should always return false in EditContext");
+ assert_equals(document.queryCommandValue("bold"), "false", "queryCommandValue should always return 'false' in EditContext for commands that return booleans");
+ assert_equals(document.queryCommandValue("forecolor"), "", "queryCommandValue should always return empty string in EditContext for commands that return strings");
+
+ selection.setBaseAndExtent(test.firstChild.firstChild, 1, test.lastChild, 1);
+ assert_false(document.queryCommandIndeterm("bold"), "'queryCommandInterm should always return false in EditContext");
+
+ test.remove();
+ }, 'queryCommandState, queryCommandvalue, and queryCommandInterm should always return false');
+
+ promise_test(async function() {
+ const editContext = new EditContext();
+ const test = document.createElement("div");
+ test.innerHTML = "abc";
+ document.body.appendChild(test);
+ let firedTextUpdate = false;
+ editContext.addEventListener("textupdate", e => {
+ firedTextUpdate = true;
+ });
+ test.editContext = editContext;
+ test.focus();
+
+ const selection = window.getSelection();
+ selection.setBaseAndExtent(test.firstChild, 0, test.firstChild, 1);
+
+ await test_driver.set_permission({name: 'clipboard-read'}, 'granted');
+ assert_true(document.execCommand("copy"), "'copy' always returns true regardless of whether it did anything");
+ await waitForUserActivation();
+ let clipboardText = await navigator.clipboard.readText();
+ assert_equals(clipboardText, "a", "'copy' should work in EditContext");
+
+ selection.setBaseAndExtent(test.firstChild, 1, test.firstChild, 2);
+
+ assert_true(document.execCommand("cut"), "'cut' always returns true regardless of whether it did anything");
+ assert_equals(test.innerHTML, "abc", "DOM should not be updated from execCommand('cut')");
+ await waitForUserActivation();
+ clipboardText = await navigator.clipboard.readText();
+ assert_equals(clipboardText, "a", "Failed 'cut' should not change clipboard");
+
+ test.remove();
+ }, 'document.execCommand("copy") should work but document.execCommand("cut") should not change the DOM or the clipboard');
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/editing/edit-context/edit-context-inheritability.tentative.html b/testing/web-platform/tests/editing/edit-context/edit-context-inheritability.tentative.html
new file mode 100644
index 0000000000..59c553d966
--- /dev/null
+++ b/testing/web-platform/tests/editing/edit-context/edit-context-inheritability.tentative.html
@@ -0,0 +1,182 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>EditContext: Inherited Editability</title>
+<meta name="author" title="Dan Clark" href="mailto:daniec@microsoft.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+</head>
+<body>
+ <div id="edit-context-top-0">Text in EditContext-associated element, should be editable</div>
+
+ <div id="edit-context-top-1">
+ <div id="default-1">
+ Element child of EditContext, should be editable
+ </div>
+ </div>
+
+ <div id="edit-context-top-2">
+ <div id="noteditable-2" contenteditable="false">
+ <div id="editable-in-noteditable-2" contenteditable="">
+ This is a contenteditable="" child of contenteditable="false" parent.
+ This node should be editable. It should be the target of beforeinput/input events when the user edits here.
+ The grandparent node #edit-context-0 should not be the target of beforeinput/input events,
+ and the EditContext should not receive
+ textupdate events
+ </div>
+ </div>
+ </div>
+
+ <div id="edit-context-top-3">
+ <div id="noteditable-3" contenteditable="false">
+ <div id="editable-in-noteditable-3" contenteditable="">
+ <div id="contenteditable-in-contenteditable-3" contenteditable="">
+ This is an contenteditable="" child of a contenteditable="". Since this is the child of an
+ editable element, when the user types here, it's the parent contenteditable="" that gets
+ input/beforeinput events, and this doesn't. Basically the child contenteditable="" attribute
+ is a no-op.
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div id="edit-context-top-4">
+ <div id="noteditable-4" contenteditable="false">
+ <div id="edit-context-in-noteditable-4">
+ This is an EditContext child of contenteditable="false" parent.
+ This node should be editable, and this EditContext (but not the
+ grandparent EditContext) should get events, since there is an
+ intermediate non-editable parent.
+ </div>
+ </div>
+ </div>
+
+ <div id="edit-context-top-5">
+ <div id="contenteditable-in-ec-5" contenteditable="">
+ This is a contenteditable="" child of EditContext.
+ It inherits editability from the parent and it should not be the target of beforeinput/input events.
+ Setting contenteditable="" on this node is basically a no-op.
+ </div>
+ </div>
+
+ <div id="edit-context-top-6">
+ <input id="input-in-ec-6" value="Input in EditContext. Events are fired against this element, and not against any parent EditContext.">
+ </div>
+
+ <div id="edit-context-top-7">
+ <div id="edit-context-in-ec-7">
+ EditContext child of EditContext. When user types here,
+ events are fired only against the parent EditContext, not this one.
+ Since the parent element was editable, the EditContext association is basically a no-op.
+ </div>
+ </div>
+
+<script>
+ const event_log = [];
+
+ const editContextElements = document.querySelectorAll("div[id^='edit-context-']");
+ editContextElements.forEach((element) => {
+ element.editContext = new EditContext();
+ });
+
+ const divs = Array.from(document.querySelectorAll("div"));
+ const inputs = Array.from(document.querySelectorAll("input"));
+ const eventTargets = divs.concat(inputs);
+ eventTargets.forEach((element) => {
+ element.addEventListener("beforeinput", (e) => {
+ if (element == e.target) {
+ event_log.push(`beforeinput: ${element.id}`);
+ }
+ });
+
+ element.addEventListener("input", (e) => {
+ if (element === e.target) {
+ event_log.push(`input: ${element.id}`);
+ }
+ });
+
+ if (element.editContext) {
+ element.editContext.addEventListener("textupdate", () => {
+ event_log.push(`textupdate: ${element.id}`);
+ });
+ }
+ });
+
+ promise_test(async () => {
+ event_log.length = 0;
+
+ const input_target = document.querySelector("#edit-context-top-0");
+ await test_driver.click(input_target);
+ await test_driver.send_keys(input_target, "a");
+ assert_array_equals(event_log, ["beforeinput: edit-context-top-0", "textupdate: edit-context-top-0"]);
+ }, 'Check that element with EditContext is editable and gets events');
+
+ promise_test(async () => {
+ event_log.length = 0;
+
+ const input_target = document.querySelector("#default-1");
+ await test_driver.click(input_target);
+ await test_driver.send_keys(input_target, "a");
+ assert_array_equals(event_log, ["beforeinput: edit-context-top-1", "textupdate: edit-context-top-1"]);
+ }, 'Check that child of EditContext is editable and the parent EditContext gets the events');
+
+ promise_test(async () => {
+ event_log.length = 0;
+
+ const input_target = document.querySelector("#editable-in-noteditable-2");
+ await test_driver.click(input_target);
+ await test_driver.send_keys(input_target, "a");
+ assert_array_equals(event_log, ["beforeinput: editable-in-noteditable-2", "input: editable-in-noteditable-2"]);
+ }, 'Check that a contenteditable child of a contenteditable="false" is editable');
+
+ promise_test(async () => {
+ event_log.length = 0;
+
+ const input_target = document.querySelector("#contenteditable-in-contenteditable-3");
+ await test_driver.click(input_target);
+ await test_driver.send_keys(input_target, "a");
+ assert_array_equals(event_log, ["beforeinput: editable-in-noteditable-3", "input: editable-in-noteditable-3"]);
+ }, 'Check that a contenteditable child of a contenteditable is editable, but the parent contenteditable gets the events');
+
+ promise_test(async () => {
+ event_log.length = 0;
+
+ const input_target = document.querySelector("#edit-context-in-noteditable-4");
+ await test_driver.click(input_target);
+ await test_driver.send_keys(input_target, "a");
+ assert_array_equals(event_log, ["beforeinput: edit-context-in-noteditable-4", "textupdate: edit-context-in-noteditable-4"]);
+ }, 'Check that an EditContext child of a contenteditable="false" parent is editable and gets events');
+
+ promise_test(async () => {
+ event_log.length = 0;
+
+ const input_target = document.querySelector("#contenteditable-in-ec-5");
+ await test_driver.click(input_target);
+ await test_driver.send_keys(input_target, "a");
+ assert_array_equals(event_log, ["beforeinput: edit-context-top-5", "textupdate: edit-context-top-5"]);
+ }, 'Check that an contenteditable child of an EditContext is editable, but the EditContext gets the events');
+
+ promise_test(async () => {
+ event_log.length = 0;
+
+ const input_target = document.querySelector("#input-in-ec-6");
+ await test_driver.click(input_target);
+ await test_driver.send_keys(input_target, "a");
+ assert_array_equals(event_log, ["beforeinput: input-in-ec-6", "input: input-in-ec-6"]);
+ }, 'Check that an input element in an EditContext is the event target for beforeinput/input');
+
+ promise_test(async () => {
+ event_log.length = 0;
+
+ const input_target = document.querySelector("#edit-context-in-ec-7");
+ await test_driver.click(input_target);
+ await test_driver.send_keys(input_target, "a");
+ assert_array_equals(event_log, ["beforeinput: edit-context-top-7", "textupdate: edit-context-top-7"]);
+ }, 'Check that for an EditContext child of an EditContext, the parent is the one that gets the events');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/editing/edit-context/edit-context-input.tentative.html b/testing/web-platform/tests/editing/edit-context/edit-context-input.tentative.html
new file mode 100644
index 0000000000..762ec59547
--- /dev/null
+++ b/testing/web-platform/tests/editing/edit-context/edit-context-input.tentative.html
@@ -0,0 +1,249 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>EditContext: The HTMLElement.editContext property</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+</head>
+<body>
+ <script>
+ const kBackspaceKey = "\uE003";
+ const kDeleteKey = "\uE017";
+
+ async function testBasicTestInput(element) {
+ const editContext = new EditContext();
+ let textForView = "";
+ document.body.appendChild(element);
+ let beforeInputType = null;
+ let beforeInputTargetRanges = null;
+ element.addEventListener("beforeinput", e => {
+ beforeInputType = e.inputType;
+ beforeInputTargetRanges = e.getTargetRanges().map(
+ staticRange => [staticRange.startOffset, staticRange.endOffset]);
+ });
+ editContext.addEventListener("textupdate", e => {
+ textForView = `${textForView.substring(0, e.updateRangeStart)}${e.text}${textForView.substring(e.updateRangeEnd)}`;
+ });
+ element.editContext = editContext;
+ element.focus();
+ await test_driver.send_keys(element, 'a');
+ assert_equals(editContext.text, "a");
+ assert_equals(textForView, "a");
+ assert_equals(beforeInputType, "insertText");
+ if (element instanceof HTMLCanvasElement) {
+ // DOM selection doesn't work inside <canvas>, so events
+ // in <canvas> can't have target ranges.
+ assert_equals(beforeInputTargetRanges.length, 0);
+ } else {
+ assert_equals(beforeInputTargetRanges.length, 1);
+ assert_array_equals(beforeInputTargetRanges[0], [0, 0]);
+ }
+
+ element.remove();
+ }
+
+ promise_test(testBasicTestInput.bind(null, document.createElement("div")), "Basic text input with div");
+ promise_test(testBasicTestInput.bind(null, document.createElement("canvas")), "Basic text input with canvas");
+
+ async function testBasicTestInputWithExistingSelection(element) {
+ const editContext = new EditContext();
+ let textForView = "";
+ document.body.appendChild(element);
+ editContext.addEventListener("textupdate", e => {
+ textForView = `${textForView.substring(0, e.updateRangeStart)}${e.text}${textForView.substring(e.updateRangeEnd)}`;
+ });
+ element.editContext = editContext;
+ element.focus();
+
+ editContext.updateText(0, 0, "abcd");
+ textForView = "abcd";
+ assert_equals(editContext.text, "abcd");
+ editContext.updateSelection(2, 3);
+ await test_driver.send_keys(element, 'Z');
+ assert_equals(editContext.text, "abZd");
+ assert_equals(textForView, "abZd");
+
+ editContext.updateSelection(2, 1);
+ await test_driver.send_keys(element, 'Y');
+ assert_equals(editContext.text, "aYZd");
+ assert_equals(textForView, "aYZd");
+
+ element.remove();
+ }
+
+ promise_test(testBasicTestInputWithExistingSelection.bind(null, document.createElement("div")), "Text insertion with non-collapsed selection with div");
+ promise_test(testBasicTestInputWithExistingSelection.bind(null, document.createElement("canvas")), "Text insertion with non-collapsed selection with canvas");
+
+ promise_test(async function() {
+ const editContext = new EditContext();
+ assert_not_equals(editContext, null);
+ const test = document.createElement("div");
+ document.body.appendChild(test);
+ test.editContext = editContext;
+ test.focus();
+ await test_driver.send_keys(test, 'a');
+ assert_equals(test.innerHTML, "");
+ test.remove();
+ }, 'EditContext should disable DOM mutation');
+
+ promise_test(async function() {
+ const editContext = new EditContext();
+ assert_not_equals(editContext, null);
+ const test = document.createElement("div");
+ document.body.appendChild(test);
+ test.focus();
+ test.editContext = editContext;
+ test.addEventListener("beforeinput", e => {
+ if (e.inputType === "insertText") {
+ e.preventDefault();
+ }
+ });
+ await test_driver.send_keys(test, 'a');
+ assert_equals(editContext.text, "");
+ test.remove();
+ }, 'beforeInput(insertText) should be cancelable');
+
+ promise_test(async () => {
+ let div = document.createElement("div");
+ document.body.appendChild(div);
+ let divText = "Hello World";
+ div.innerText = divText;
+ div.editContext = new EditContext();
+ div.focus();
+ let got_before_input_event = false;
+ div.addEventListener("beforeinput", e => {
+ got_before_input_event = true;
+ });
+ let got_textupdate_event = false;
+ div.editContext.addEventListener("textupdate", e => {
+ got_textupdate_event = true;
+ });
+
+ div.editContext = null;
+ await test_driver.send_keys(div, "a");
+
+ assert_false(got_textupdate_event, "Shouldn't have received textupdate event after editContext was detached");
+ assert_false(got_before_input_event, "Shouldn't have received beforeinput event after editContext was detached");
+
+ div.remove();
+ }, "EditContext should not receive events after being detached from element");
+
+ async function testBackspaceAndDelete(element) {
+ const editContext = new EditContext();
+ let textForView = "hello there";
+ document.body.appendChild(element);
+ let beforeInputType = null;
+ let beforeInputTargetRanges = null;
+ element.addEventListener("beforeinput", e => {
+ beforeInputType = e.inputType;
+ beforeInputTargetRanges = e.getTargetRanges().map(
+ staticRange => [staticRange.startOffset, staticRange.endOffset]);
+ });
+ let textUpdateSelection = null;
+ editContext.addEventListener("textupdate", e => {
+ textUpdateSelection = [e.selectionStart, e.selectionEnd];
+ textForView = `${textForView.substring(0, e.updateRangeStart)}${e.text}${textForView.substring(e.updateRangeEnd)}`;
+ });
+ element.editContext = editContext;
+ editContext.updateText(0, 11, "hello there");
+ editContext.updateSelection(10, 10);
+ const selection = window.getSelection();
+
+ await test_driver.send_keys(element, kBackspaceKey);
+ assert_equals(textForView, "hello thee");
+ assert_array_equals(textUpdateSelection, [9, 9]);
+ assert_equals(beforeInputType, "deleteContentBackward");
+ assert_equals(beforeInputTargetRanges.length, 0, "Backspace should not have a target range in EditContext");
+
+ await test_driver.send_keys(element, kDeleteKey);
+ assert_equals(textForView, "hello the");
+ assert_array_equals(textUpdateSelection, [9, 9]);
+ assert_equals(beforeInputType, "deleteContentForward");
+ assert_equals(beforeInputTargetRanges.length, 0, "Delete should not have a target range in EditContext");
+ element.remove();
+ }
+
+ promise_test(testBackspaceAndDelete.bind(null, document.createElement("div")), "Backspace and delete in EditContext with div");
+ promise_test(testBackspaceAndDelete.bind(null, document.createElement("canvas")) , "Backspace and delete in EditContext with canvas");
+
+ async function testBackspaceAndDeleteWithExistingSelection(element) {
+ const editContext = new EditContext();
+ let textForView = "hello there";
+ document.body.appendChild(element);
+ let beforeInputType = null;
+ let beforeInputTargetRanges = null;
+ element.addEventListener("beforeinput", e => {
+ beforeInputType = e.inputType;
+ beforeInputTargetRanges = e.getTargetRanges().map(
+ staticRange => [staticRange.startOffset, staticRange.endOffset]);
+ });
+ let textUpdateSelection = null;
+ editContext.addEventListener("textupdate", e => {
+ textUpdateSelection = [e.selectionStart, e.selectionEnd];
+ textForView = `${textForView.substring(0, e.updateRangeStart)}${e.text}${textForView.substring(e.updateRangeEnd)}`;
+ });
+ element.editContext = editContext;
+ const initialText = "abcdefghijklmnopqrstuvwxyz";
+ editContext.updateText(0, initialText.length, initialText);
+ textForView = initialText;
+ element.focus();
+
+ editContext.updateSelection(3, 6);
+ await test_driver.send_keys(element, kBackspaceKey);
+ assert_equals(editContext.text, "abcghijklmnopqrstuvwxyz");
+ assert_equals(textForView, "abcghijklmnopqrstuvwxyz");
+ assert_array_equals(textUpdateSelection, [3, 3]);
+ assert_equals(beforeInputType, "deleteContentBackward");
+ assert_equals(beforeInputTargetRanges.length, 0, "Backspace should not have a target range in EditContext");
+
+ editContext.updateSelection(3, 6);
+ await test_driver.send_keys(element, kDeleteKey);
+ assert_equals(editContext.text, "abcjklmnopqrstuvwxyz");
+ assert_equals(textForView, "abcjklmnopqrstuvwxyz");
+ assert_array_equals(textUpdateSelection, [3, 3]);
+ assert_equals(beforeInputType, "deleteContentForward");
+ assert_equals(beforeInputTargetRanges.length, 0, "Delete should not have a target range in EditContext");
+
+ editContext.updateSelection(6, 3);
+ await test_driver.send_keys(element, kBackspaceKey);
+ assert_equals(editContext.text, "abcmnopqrstuvwxyz");
+ assert_equals(textForView, "abcmnopqrstuvwxyz");
+ assert_array_equals(textUpdateSelection, [3, 3]);
+ assert_equals(beforeInputType, "deleteContentBackward");
+ assert_equals(beforeInputTargetRanges.length, 0, "Backspace should not have a target range in EditContext");
+
+ editContext.updateSelection(6, 3);
+ await test_driver.send_keys(element, kDeleteKey);
+ assert_equals(editContext.text, "abcpqrstuvwxyz");
+ assert_equals(textForView, "abcpqrstuvwxyz");
+ assert_array_equals(textUpdateSelection, [3, 3]);
+ assert_equals(beforeInputType, "deleteContentForward");
+ assert_equals(beforeInputTargetRanges.length, 0, "Delete should not have a target range in EditContext");
+
+ element.remove();
+ }
+
+ promise_test(testBackspaceAndDeleteWithExistingSelection.bind(null, document.createElement("div")), "Backspace and delete with existing selection with div");
+ promise_test(testBackspaceAndDeleteWithExistingSelection.bind(null, document.createElement("canvas")) , "Backspace and delete with existing selection with canvas");
+
+ promise_test(async function() {
+ const iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ const editContext = new EditContext();
+ iframe.contentDocument.body.editContext = editContext;
+ iframe.contentDocument.body.focus();
+ let got_textupdate_event = false;
+ editContext.addEventListener("textupdate", e => {
+ got_textupdate_event = true;
+ });
+ await test_driver.send_keys(iframe.contentDocument.body, "a");
+ assert_equals(iframe.contentDocument.body.innerHTML, "", "EditContext should disable DOM modification in iframe.");
+ assert_true(got_textupdate_event, "Input in iframe EditContext should trigger textupdate event");
+ iframe.remove();
+ }, 'EditContext constructed outside iframe can be used in iframe');
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/editing/edit-context/edit-context-property.tentative.html b/testing/web-platform/tests/editing/edit-context/edit-context-property.tentative.html
new file mode 100644
index 0000000000..c63d5f8cf6
--- /dev/null
+++ b/testing/web-platform/tests/editing/edit-context/edit-context-property.tentative.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>EditContext: The HTMLElement.editContext property</title>
+<meta name="author" title="Dan Clark" href="mailto:daniec@microsoft.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src='../../html/resources/common.js'></script>
+</head>
+<body>
+<script>
+
+test(function () {
+ assert_true('editContext' in HTMLElement.prototype, 'Element.prototype.editContext must exist');
+ assert_equals(typeof(document.createElement('div').editContext), 'object', 'An instance of div must have editContext which is an object');
+}, 'Check the existence of HTMLElement.editContext');
+
+test(function () {
+ assert_false('editContext' in Node.prototype, 'Node.prototype.editContext must not exist');
+ assert_false('editContext' in Element.prototype, 'Element.prototype.editContext must not exist');
+ assert_false('editContext' in CharacterData.prototype, 'CharacterData.prototype.editContext must not exist');
+ assert_false('editContext' in Comment.prototype, 'Comment.prototype.editContext must not exist');
+ assert_equals(typeof(document.createComment('').editContext), 'undefined', 'An instance of comment must not have editContext');
+ assert_false('editContext' in Document.prototype, 'Document.prototype.editContext must not exist');
+ assert_equals(typeof(document.editContext), 'undefined', 'An instance of document must not have editContext which is a function');
+ assert_false('editContext' in DocumentFragment.prototype, 'DocumentFragment.prototype.editContext must not exist');
+ assert_equals(typeof((new DOMParser()).parseFromString('', 'text/html').editContext), 'undefined', 'An instance of document must not have editContext which is a function');
+ assert_false('editContext' in Text.prototype, 'Text.prototype.editContext must not exist');
+ assert_equals(typeof(document.createTextNode('').editContext), 'undefined', 'An instance of text node must not have editContext');
+}, 'Nodes other than Element should not have editContext');
+
+test(function () {
+ assert_throws_js(TypeError, function () {
+ document.createElement('div').editContext = "hello";
+ }, 'editContext must throw a TypeError when set to a string');
+
+ assert_throws_js(TypeError, function () {
+ document.createElement('div').editContext = 42;
+ }, 'editContext must throw a TypeError when set to a number');
+
+ assert_throws_js(TypeError, function () {
+ document.createElement('div').editContext = document.createElement('span');
+ }, 'editContext must throw a TypeError when set to a node');
+}, 'HTMLElement.editContext must throw a TypeError if set to something other than an EditContext');
+
+test(function () {
+ const EDIT_CONTEXT_ALLOWED_ELEMENTS = HTML5_SHADOW_ALLOWED_ELEMENTS.concat(['canvas']);
+ for (const elementName of EDIT_CONTEXT_ALLOWED_ELEMENTS) {
+ const element = document.createElement(elementName);
+ const ec = new EditContext();
+ element.editContext = ec;
+ assert_equals(element.editContext, ec, 'Getting HTMLElement.editContext should yield the same EditContext instance');
+ }
+}, 'HTMLElement.editContext can be set on the shadow root elements plus canvas.');
+
+test(function () {
+ // EditContext shares all of the shadow root disallowed elements except for canvas.
+ const EDIT_CONTEXT_DISALLOWED_ELEMENTS = HTML5_SHADOW_DISALLOWED_ELEMENTS.toSpliced(HTML5_SHADOW_DISALLOWED_ELEMENTS.indexOf('canvas'), 1);
+ for (const elementName of EDIT_CONTEXT_DISALLOWED_ELEMENTS) {
+ const element = document.createElement(elementName);
+ const ec = new EditContext();
+ assert_throws_dom('NotSupportedError', () => {
+ element.editContext = ec;
+ }, `Setting editContext on <${elementName}> must throw.`);
+ assert_equals(element.editContext, null);
+ }
+}, 'Setting HTMLElement.editContext must throw a NotSupportedError for disallowed elements');
+
+test(function () {
+ const element1 = document.createElement('div');
+ const element2 = document.createElement('div');
+ const editContext1 = new EditContext();
+ const editContext2 = new EditContext();
+ element1.editContext = editContext1;
+ assert_throws_dom('NotSupportedError', () => {
+ element2.editContext = editContext1;
+ }, `TypeError should be thrown when author attempts to associate an EditContext with a second element`);
+ assert_equals(element1.editContext, editContext1, "element1 association should not have changed");
+ assert_equals(element2.editContext, null, "element2 association should not have changed");
+
+ element1.editContext = editContext2;
+ assert_equals(element1.editContext, editContext2, "Association can be switched directly to second EditConext");
+
+ element1.editContext = editContext2;
+ assert_equals(element1.editContext, editContext2, "Assigning to the same EditContext again is a no-op");
+}, 'An EditContext can only be associated with one element at a time');
+
+</script>
+</body>
+</html>