diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/editing/edit-context | |
parent | Initial commit. (diff) | |
download | firefox-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')
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> |