fune/testing/web-platform/tests/input-events/input-events-get-target-ranges.js
Masayuki Nakano 1edcd70cdd Bug 1676295 - Add WPT to test deleting in/around/across list items r=smaug
There are not enough tests comparing delete operation result and result of
`getTargetRanges()` when selection is in/around/across list item elements.
This patch creates a utility method to make the test body not need to use
Selection API for making simpler tests.

The expected behavior is based on Blink and WebKit unless their behavior is
buggy because their behavior is more reasonable than Gecko's in most cases.

Note that the removing tests are covered by the new tests.

Differential Revision: https://phabricator.services.mozilla.com/D96800
2020-11-14 10:30:31 +00:00

516 lines
16 KiB
JavaScript

"use strict";
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`;
}
}