summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/editing/include/editor-test-utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/editing/include/editor-test-utils.js')
-rw-r--r--testing/web-platform/tests/editing/include/editor-test-utils.js387
1 files changed, 387 insertions, 0 deletions
diff --git a/testing/web-platform/tests/editing/include/editor-test-utils.js b/testing/web-platform/tests/editing/include/editor-test-utils.js
new file mode 100644
index 0000000000..8d8e778836
--- /dev/null
+++ b/testing/web-platform/tests/editing/include/editor-test-utils.js
@@ -0,0 +1,387 @@
+/**
+ * EditorTestUtils is a helper utilities to test HTML editor. This can be
+ * instantiated per an editing host. If you test `designMode`, the editing
+ * host should be the <body> element.
+ * Note that if you want to use sendKey in a sub-document, you need to include
+ * testdriver.js (and related files) from the sub-document before creating this.
+ */
+class EditorTestUtils {
+ kShift = "\uE008";
+ kMeta = "\uE03d";
+ kControl = "\uE009";
+ kAlt = "\uE00A";
+
+ editingHost;
+
+ constructor(aEditingHost, aHarnessWindow = window) {
+ this.editingHost = aEditingHost;
+ if (aHarnessWindow != this.window && this.window.test_driver) {
+ this.window.test_driver.set_test_context(aHarnessWindow);
+ }
+ }
+
+ get document() {
+ return this.editingHost.ownerDocument;
+ }
+ get window() {
+ return this.document.defaultView;
+ }
+ get selection() {
+ return this.window.getSelection();
+ }
+
+ sendKey(key, modifier) {
+ if (!modifier) {
+ return new this.window.test_driver.Actions()
+ .keyDown(key)
+ .keyUp(key)
+ .send();
+ }
+ return new this.window.test_driver.Actions()
+ .keyDown(modifier)
+ .keyDown(key)
+ .keyUp(key)
+ .keyUp(modifier)
+ .send();
+ }
+
+ sendDeleteKey(modifier) {
+ const kDeleteKey = "\uE017";
+ return this.sendKey(kDeleteKey, modifier);
+ }
+
+ sendBackspaceKey(modifier) {
+ const kBackspaceKey = "\uE003";
+ return this.sendKey(kBackspaceKey, modifier);
+ }
+
+ sendArrowLeftKey(modifier) {
+ const kArrowLeft = "\uE012";
+ return this.sendKey(kArrowLeft, modifier);
+ }
+
+ sendArrowRightKey(modifier) {
+ const kArrowRight = "\uE014";
+ return this.sendKey(kArrowRight, modifier);
+ }
+
+ sendHomeKey(modifier) {
+ const kHome = "\uE011";
+ return this.sendKey(kHome, modifier);
+ }
+
+ sendEndKey(modifier) {
+ const kEnd = "\uE010";
+ return this.sendKey(kEnd, modifier);
+ }
+
+ sendEnterKey(modifier) {
+ const kEnter = "\uE007";
+ return this.sendKey(kEnter, modifier);
+ }
+
+ sendSelectAllShortcutKey() {
+ return this.sendKey(
+ "a",
+ this.window.navigator.platform.includes("Mac")
+ ? this.kMeta
+ : this.kControl
+ );
+ }
+
+ // Similar to `setupDiv` in editing/include/tests.js, this method sets
+ // innerHTML value of this.editingHost, 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
+ setupEditingHost(innerHTMLWithRangeMarkers) {
+ const startBoundaries = innerHTMLWithRangeMarkers.match(/\{|\[/g) || [];
+ const endBoundaries = innerHTMLWithRangeMarkers.match(/\}|\]/g) || [];
+ if (startBoundaries.length !== endBoundaries.length) {
+ throw "Should match number of open/close markers";
+ }
+
+ this.editingHost.innerHTML = innerHTMLWithRangeMarkers;
+ this.editingHost.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;
+ }
+
+ let getNextRangeAndDeleteMarker = startNode => {
+ let getNextLeafNode = node => {
+ let inclusiveDeepestFirstChildNode = container => {
+ while (container.firstChild) {
+ container = container.firstChild;
+ }
+ return container;
+ };
+ if (node.hasChildNodes()) {
+ return inclusiveDeepestFirstChildNode(node);
+ }
+ if (node === this.editingHost) {
+ return null;
+ }
+ if (node.nextSibling) {
+ return inclusiveDeepestFirstChildNode(node.nextSibling);
+ }
+ let nextSibling = (child => {
+ for (
+ let parent = child.parentElement;
+ parent && parent != this.editingHost;
+ parent = parent.parentElement
+ ) {
+ if (parent.nextSibling) {
+ return parent.nextSibling;
+ }
+ }
+ return null;
+ })(node);
+ if (!nextSibling) {
+ return null;
+ }
+ return inclusiveDeepestFirstChildNode(nextSibling);
+ };
+ let scanMarkerInTextNode = (textNode, offset) => {
+ return /[\{\[\]\}]/.exec(textNode.data.substr(offset));
+ };
+ let startMarker = ((startContainer, startOffset) => {
+ let 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 = ((startContainer, startOffset) => {
+ let 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";
+ }
+ let 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";
+ };
+ let deleteFoundMarkers = () => {
+ let 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)}`;
+ }
+ };
+ deleteFoundMarkers();
+
+ let 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';
+ }
+ }
+ };
+ handleNodeSelectMarker();
+
+ let range = document.createRange();
+ range.setStart(startMarker.container, startMarker.offset);
+ range.setEnd(endMarker.container, endMarker.offset);
+ return range;
+ };
+
+ let ranges = [];
+ for (
+ let range = getNextRangeAndDeleteMarker(this.editingHost.firstChild);
+ range;
+ range = getNextRangeAndDeleteMarker(range.endContainer)
+ ) {
+ ranges.push(range);
+ }
+
+ this.selection.removeAllRanges();
+ for (let range of ranges) {
+ this.selection.addRange(range);
+ }
+
+ if (this.selection.rangeCount != ranges.length) {
+ throw `Failed to set selection to the given ranges whose length is ${ranges.length}, but only ${this.selection.rangeCount} ranges are added`;
+ }
+ }
+
+ // Originated from normalizeSerializedStyle in include/tests.js
+ normalizeStyleAttributeValues() {
+ for (const element of Array.from(
+ this.editingHost.querySelectorAll("[style]")
+ )) {
+ element.setAttribute(
+ "style",
+ element
+ .getAttribute("style")
+ // Random spacing differences
+ .replace(/; ?$/, "")
+ .replace(/: /g, ":")
+ // Gecko likes "transparent"
+ .replace(/transparent/g, "rgba(0, 0, 0, 0)")
+ // WebKit likes to look overly precise
+ .replace(/, 0.496094\)/g, ", 0.5)")
+ // Gecko converts anything with full alpha to "transparent" which
+ // then becomes "rgba(0, 0, 0, 0)", so we have to make other
+ // browsers match
+ .replace(/rgba\([0-9]+, [0-9]+, [0-9]+, 0\)/g, "rgba(0, 0, 0, 0)")
+ );
+ }
+ }
+}