mirror of
https://github.com/servo/servo.git
synced 2025-06-25 01:24:37 +01:00
516 lines
16 KiB
JavaScript
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(/ /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`;
|
|
}
|
|
}
|