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