summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/input-events/input-events-get-target-ranges.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/input-events/input-events-get-target-ranges.js')
-rw-r--r--testing/web-platform/tests/input-events/input-events-get-target-ranges.js518
1 files changed, 518 insertions, 0 deletions
diff --git a/testing/web-platform/tests/input-events/input-events-get-target-ranges.js b/testing/web-platform/tests/input-events/input-events-get-target-ranges.js
new file mode 100644
index 0000000000..004416ec2a
--- /dev/null
+++ b/testing/web-platform/tests/input-events/input-events-get-target-ranges.js
@@ -0,0 +1,518 @@
+"use strict";
+
+// TODO: extend `EditorTestUtils` in editing/include/edit-test-utils.mjs
+
+const kBackspaceKey = "\uE003";
+const kDeleteKey = "\uE017";
+const kArrowRight = "\uE014";
+const kArrowLeft = "\uE012";
+const kShift = "\uE008";
+const kMeta = "\uE03d";
+const kControl = "\uE009";
+const kAlt = "\uE00A";
+const kKeyA = "a";
+
+const kImgSrc =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEElEQVR42mNgaGD4D8YwBgAw9AX9Y9zBwwAAAABJRU5ErkJggg==";
+
+let gSelection, gEditor, gBeforeinput, gInput;
+
+function initializeTest(aInnerHTML) {
+ function onBeforeinput(event) {
+ // NOTE: Blink makes `getTargetRanges()` return empty range after
+ // propagation, but this test wants to check the result during
+ // propagation. Therefore, we need to cache the result, but will
+ // assert if `getTargetRanges()` returns different ranges after
+ // checking the cached ranges.
+ event.cachedRanges = event.getTargetRanges();
+ gBeforeinput.push(event);
+ }
+ function onInput(event) {
+ event.cachedRanges = event.getTargetRanges();
+ gInput.push(event);
+ }
+ if (gEditor !== document.querySelector("div[contenteditable]")) {
+ if (gEditor) {
+ gEditor.isListeningToInputEvents = false;
+ gEditor.removeEventListener("beforeinput", onBeforeinput);
+ gEditor.removeEventListener("input", onInput);
+ }
+ gEditor = document.querySelector("div[contenteditable]");
+ }
+ gSelection = getSelection();
+ gBeforeinput = [];
+ gInput = [];
+ if (!gEditor.isListeningToInputEvents) {
+ gEditor.isListeningToInputEvents = true;
+ gEditor.addEventListener("beforeinput", onBeforeinput);
+ gEditor.addEventListener("input", onInput);
+ }
+
+ setupEditor(aInnerHTML);
+ gBeforeinput = [];
+ gInput = [];
+}
+
+function getArrayOfRangesDescription(arrayOfRanges) {
+ if (arrayOfRanges === null) {
+ return "null";
+ }
+ if (arrayOfRanges === undefined) {
+ return "undefined";
+ }
+ if (!Array.isArray(arrayOfRanges)) {
+ return "Unknown Object";
+ }
+ if (arrayOfRanges.length === 0) {
+ return "[]";
+ }
+ let result = "[";
+ for (let range of arrayOfRanges) {
+ result += `{${getRangeDescription(range)}},`;
+ }
+ result += "]";
+ return result;
+}
+
+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})`;
+}
+
+function sendDeleteKey(modifier) {
+ if (!modifier) {
+ return new test_driver.Actions()
+ .keyDown(kDeleteKey)
+ .keyUp(kDeleteKey)
+ .send();
+ }
+ return new test_driver.Actions()
+ .keyDown(modifier)
+ .keyDown(kDeleteKey)
+ .keyUp(kDeleteKey)
+ .keyUp(modifier)
+ .send();
+}
+
+function sendBackspaceKey(modifier) {
+ if (!modifier) {
+ return new test_driver.Actions()
+ .keyDown(kBackspaceKey)
+ .keyUp(kBackspaceKey)
+ .send();
+ }
+ return new test_driver.Actions()
+ .keyDown(modifier)
+ .keyDown(kBackspaceKey)
+ .keyUp(kBackspaceKey)
+ .keyUp(modifier)
+ .send();
+}
+
+function sendKeyA() {
+ return new test_driver.Actions()
+ .keyDown(kKeyA)
+ .keyUp(kKeyA)
+ .send();
+}
+
+function sendArrowLeftKey() {
+ return new test_driver.Actions()
+ .keyDown(kArrowLeft)
+ .keyUp(kArrowLeft)
+ .send();
+}
+
+function sendArrowRightKey() {
+ return new test_driver.Actions()
+ .keyDown(kArrowRight)
+ .keyUp(kArrowRight)
+ .send();
+}
+
+function checkGetTargetRangesOfBeforeinputOnDeleteSomething(expectedRanges) {
+ assert_equals(
+ gBeforeinput.length,
+ 1,
+ "One beforeinput event should be fired if the key operation tries to delete something"
+ );
+ assert_true(
+ Array.isArray(gBeforeinput[0].cachedRanges),
+ "gBeforeinput[0].getTargetRanges() should return an array of StaticRange instances at least during propagation"
+ );
+ let arrayOfExpectedRanges = Array.isArray(expectedRanges)
+ ? expectedRanges
+ : [expectedRanges];
+ // Before checking the length of array of ranges, we should check the given
+ // range first because the ranges are more important than whether there are
+ // redundant additional unexpected ranges.
+ for (
+ let i = 0;
+ i <
+ Math.max(arrayOfExpectedRanges.length, gBeforeinput[0].cachedRanges.length);
+ i++
+ ) {
+ assert_equals(
+ getRangeDescription(gBeforeinput[0].cachedRanges[i]),
+ getRangeDescription(arrayOfExpectedRanges[i]),
+ `gBeforeinput[0].getTargetRanges()[${i}] should return expected range (inputType is "${gBeforeinput[0].inputType}")`
+ );
+ }
+ assert_equals(
+ gBeforeinput[0].cachedRanges.length,
+ arrayOfExpectedRanges.length,
+ `getTargetRanges() of beforeinput event should return ${arrayOfExpectedRanges.length} ranges`
+ );
+}
+
+function checkGetTargetRangesOfInputOnDeleteSomething() {
+ assert_equals(
+ gInput.length,
+ 1,
+ "One input event should be fired if the key operation deletes something"
+ );
+ // https://github.com/w3c/input-events/issues/113
+ assert_true(
+ Array.isArray(gInput[0].cachedRanges),
+ "gInput[0].getTargetRanges() should return an array of StaticRange instances at least during propagation"
+ );
+ assert_equals(
+ gInput[0].cachedRanges.length,
+ 0,
+ "gInput[0].getTargetRanges() should return empty array during propagation"
+ );
+}
+
+function checkGetTargetRangesOfInputOnDoNothing() {
+ assert_equals(
+ gInput.length,
+ 0,
+ "input event shouldn't be fired when the key operation does not cause modifying the DOM tree"
+ );
+}
+
+function checkBeforeinputAndInputEventsOnNOOP() {
+ assert_equals(
+ gBeforeinput.length,
+ 0,
+ "beforeinput event shouldn't be fired when the key operation does not cause modifying the DOM tree"
+ );
+ assert_equals(
+ gInput.length,
+ 0,
+ "input event shouldn't be fired when the key operation does not cause modifying the DOM tree"
+ );
+}
+
+function checkEditorContentResultAsSubTest(
+ expectedResult,
+ description,
+ options = {}
+) {
+ test(() => {
+ if (Array.isArray(expectedResult)) {
+ assert_in_array(
+ options.ignoreWhiteSpaceDifference
+ ? gEditor.innerHTML.replace(/&nbsp;/g, " ")
+ : gEditor.innerHTML,
+ expectedResult
+ );
+ } else {
+ assert_equals(
+ options.ignoreWhiteSpaceDifference
+ ? gEditor.innerHTML.replace(/&nbsp;/g, " ")
+ : gEditor.innerHTML,
+ expectedResult
+ );
+ }
+ }, `${description} - comparing innerHTML`);
+}
+
+// Similar to `setupDiv` in editing/include/tests.js, this method sets
+// innerHTML value of gEditor, and sets multiple selection ranges specified
+// with the markers.
+// - `[` specifies start boundary in a text node
+// - `{` specifies start boundary before a node
+// - `]` specifies end boundary in a text node
+// - `}` specifies end boundary after a node
+function setupEditor(innerHTMLWithRangeMarkers) {
+ const startBoundaries = innerHTMLWithRangeMarkers.match(/\{|\[/g) || [];
+ const endBoundaries = innerHTMLWithRangeMarkers.match(/\}|\]/g) || [];
+ if (startBoundaries.length !== endBoundaries.length) {
+ throw "Should match number of open/close markers";
+ }
+
+ gEditor.innerHTML = innerHTMLWithRangeMarkers;
+ gEditor.focus();
+
+ if (startBoundaries.length === 0) {
+ // Don't remove the range for now since some tests may assume that
+ // setting innerHTML does not remove all selection ranges.
+ return;
+ }
+
+ function getNextRangeAndDeleteMarker(startNode) {
+ function getNextLeafNode(node) {
+ function inclusiveDeepestFirstChildNode(container) {
+ while (container.firstChild) {
+ container = container.firstChild;
+ }
+ return container;
+ }
+ if (node.hasChildNodes()) {
+ return inclusiveDeepestFirstChildNode(node);
+ }
+ if (node.nextSibling) {
+ return inclusiveDeepestFirstChildNode(node.nextSibling);
+ }
+ let nextSibling = (function nextSiblingOfAncestorElement(child) {
+ for (
+ let parent = child.parentElement;
+ parent && parent != gEditor;
+ parent = parent.parentElement
+ ) {
+ if (parent.nextSibling) {
+ return parent.nextSibling;
+ }
+ }
+ return null;
+ })(node);
+ if (!nextSibling) {
+ return null;
+ }
+ return inclusiveDeepestFirstChildNode(nextSibling);
+ }
+ function scanMarkerInTextNode(textNode, offset) {
+ return /[\{\[\]\}]/.exec(textNode.data.substr(offset));
+ }
+ let startMarker = (function scanNextStartMaker(
+ startContainer,
+ startOffset
+ ) {
+ function scanStartMakerInTextNode(textNode, offset) {
+ let scanResult = scanMarkerInTextNode(textNode, offset);
+ if (scanResult === null) {
+ return null;
+ }
+ if (scanResult[0] === "}" || scanResult[0] === "]") {
+ throw "An end marker is found before a start marker";
+ }
+ return {
+ marker: scanResult[0],
+ container: textNode,
+ offset: scanResult.index + offset
+ };
+ }
+ if (startContainer.nodeType === Node.TEXT_NODE) {
+ let scanResult = scanStartMakerInTextNode(startContainer, startOffset);
+ if (scanResult !== null) {
+ return scanResult;
+ }
+ }
+ let nextNode = startContainer;
+ while ((nextNode = getNextLeafNode(nextNode))) {
+ if (nextNode.nodeType === Node.TEXT_NODE) {
+ let scanResult = scanStartMakerInTextNode(nextNode, 0);
+ if (scanResult !== null) {
+ return scanResult;
+ }
+ continue;
+ }
+ }
+ return null;
+ })(startNode, 0);
+ if (startMarker === null) {
+ return null;
+ }
+ let endMarker = (function scanNextEndMarker(startContainer, startOffset) {
+ function scanEndMarkerInTextNode(textNode, offset) {
+ let scanResult = scanMarkerInTextNode(textNode, offset);
+ if (scanResult === null) {
+ return null;
+ }
+ if (scanResult[0] === "{" || scanResult[0] === "[") {
+ throw "A start marker is found before an end marker";
+ }
+ return {
+ marker: scanResult[0],
+ container: textNode,
+ offset: scanResult.index + offset
+ };
+ }
+ if (startContainer.nodeType === Node.TEXT_NODE) {
+ let scanResult = scanEndMarkerInTextNode(startContainer, startOffset);
+ if (scanResult !== null) {
+ return scanResult;
+ }
+ }
+ let nextNode = startContainer;
+ while ((nextNode = getNextLeafNode(nextNode))) {
+ if (nextNode.nodeType === Node.TEXT_NODE) {
+ let scanResult = scanEndMarkerInTextNode(nextNode, 0);
+ if (scanResult !== null) {
+ return scanResult;
+ }
+ continue;
+ }
+ }
+ return null;
+ })(startMarker.container, startMarker.offset + 1);
+ if (endMarker === null) {
+ throw "Found an open marker, but not found corresponding close marker";
+ }
+ function indexOfContainer(container, child) {
+ let offset = 0;
+ for (let node = container.firstChild; node; node = node.nextSibling) {
+ if (node == child) {
+ return offset;
+ }
+ offset++;
+ }
+ throw "child must be a child node of container";
+ }
+ (function deleteFoundMarkers() {
+ function removeNode(node) {
+ let container = node.parentElement;
+ let offset = indexOfContainer(container, node);
+ node.remove();
+ return { container, offset };
+ }
+ if (startMarker.container == endMarker.container) {
+ // If the text node becomes empty, remove it and set collapsed range
+ // to the position where there is the text node.
+ if (startMarker.container.length === 2) {
+ if (!/[\[\{][\]\}]/.test(startMarker.container.data)) {
+ throw `Unexpected text node (data: "${startMarker.container.data}")`;
+ }
+ let { container, offset } = removeNode(startMarker.container);
+ startMarker.container = endMarker.container = container;
+ startMarker.offset = endMarker.offset = offset;
+ startMarker.marker = endMarker.marker = "";
+ return;
+ }
+ startMarker.container.data = `${startMarker.container.data.substring(
+ 0,
+ startMarker.offset
+ )}${startMarker.container.data.substring(
+ startMarker.offset + 1,
+ endMarker.offset
+ )}${startMarker.container.data.substring(endMarker.offset + 1)}`;
+ if (startMarker.offset >= startMarker.container.length) {
+ startMarker.offset = endMarker.offset = startMarker.container.length;
+ return;
+ }
+ endMarker.offset--; // remove the start marker's length
+ if (endMarker.offset > endMarker.container.length) {
+ endMarker.offset = endMarker.container.length;
+ }
+ return;
+ }
+ if (startMarker.container.length === 1) {
+ let { container, offset } = removeNode(startMarker.container);
+ startMarker.container = container;
+ startMarker.offset = offset;
+ startMarker.marker = "";
+ } else {
+ startMarker.container.data = `${startMarker.container.data.substring(
+ 0,
+ startMarker.offset
+ )}${startMarker.container.data.substring(startMarker.offset + 1)}`;
+ }
+ if (endMarker.container.length === 1) {
+ let { container, offset } = removeNode(endMarker.container);
+ endMarker.container = container;
+ endMarker.offset = offset;
+ endMarker.marker = "";
+ } else {
+ endMarker.container.data = `${endMarker.container.data.substring(
+ 0,
+ endMarker.offset
+ )}${endMarker.container.data.substring(endMarker.offset + 1)}`;
+ }
+ })();
+ (function handleNodeSelectMarker() {
+ if (startMarker.marker === "{") {
+ if (startMarker.offset === 0) {
+ // The range start with the text node.
+ let container = startMarker.container.parentElement;
+ startMarker.offset = indexOfContainer(
+ container,
+ startMarker.container
+ );
+ startMarker.container = container;
+ } else if (startMarker.offset === startMarker.container.data.length) {
+ // The range start after the text node.
+ let container = startMarker.container.parentElement;
+ startMarker.offset =
+ indexOfContainer(container, startMarker.container) + 1;
+ startMarker.container = container;
+ } else {
+ throw 'Start marker "{" is allowed start or end of a text node';
+ }
+ }
+ if (endMarker.marker === "}") {
+ if (endMarker.offset === 0) {
+ // The range ends before the text node.
+ let container = endMarker.container.parentElement;
+ endMarker.offset = indexOfContainer(container, endMarker.container);
+ endMarker.container = container;
+ } else if (endMarker.offset === endMarker.container.data.length) {
+ // The range ends with the text node.
+ let container = endMarker.container.parentElement;
+ endMarker.offset =
+ indexOfContainer(container, endMarker.container) + 1;
+ endMarker.container = container;
+ } else {
+ throw 'End marker "}" is allowed start or end of a text node';
+ }
+ }
+ })();
+ let range = document.createRange();
+ range.setStart(startMarker.container, startMarker.offset);
+ range.setEnd(endMarker.container, endMarker.offset);
+ return range;
+ }
+
+ let ranges = [];
+ for (
+ let range = getNextRangeAndDeleteMarker(gEditor.firstChild);
+ range;
+ range = getNextRangeAndDeleteMarker(range.endContainer)
+ ) {
+ ranges.push(range);
+ }
+
+ gSelection.removeAllRanges();
+ for (let range of ranges) {
+ gSelection.addRange(range);
+ }
+
+ if (gSelection.rangeCount != ranges.length) {
+ throw `Failed to set selection to the given ranges whose length is ${ranges.length}, but only ${gSelection.rangeCount} ranges are added`;
+ }
+}