diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/input-events/input-events-get-target-ranges.js | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
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.js | 518 |
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(/ /g, " ") + : gEditor.innerHTML, + expectedResult + ); + } else { + assert_equals( + options.ignoreWhiteSpaceDifference + ? gEditor.innerHTML.replace(/ /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`; + } +} |