blob: abccff8bbef3626c3b99fea731d826a42a02aab0 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
// This file provides |assert_selection(sample, tester, expectedText, options)|
// assertion to W3C test harness to write editing test cases easier.
//
// |sample| is an HTML fragment text which is inserted as |innerHTML|. It should
// have at least one focus boundary point marker "|" and at most one anchor
// boundary point marker "^".
//
// |tester| is either name with parameter of execCommand or function taking
// one parameter |Selection|.
//
// |expectedText| is an HTML fragment text containing at most one focus marker
// and anchor marker. If resulting selection is none, you don't need to have
// anchor and focus markers.
//
// |options| is a string as description, undefined, or a dictionary containing:
// description: A description
// dumpAs: 'domtree' or 'flattree'. Default is 'domtree'.
// removeSampleIfSucceeded: A boolean. Default is true.
// dumpFromRoot: A boolean. Default is false.
//
// Example:
// test(() => {
// assert_selection(
// '|foo',
// (selection) => selection.modify('extent', 'forward, 'character'),
// '<a href="http://bar">^f|oo</a>'
// });
//
// test(() => {
// assert_selection(
// 'x^y|z',
// 'bold', // execCommand name as a test
// 'x<b>y</b>z',
// 'Insert B tag');
// });
//
// test(() => {
// assert_selection(
// 'x^y|z',
// 'createLink http://foo', // execCommand name and parameter
// 'x<a href="http://foo/">y</a></b>z',
// 'Insert B tag');
// });
//
//
// TODO(yosin): Please use "clang-format -style=Chromium -i" for formatting
// this file.
(function() {
/** @enum{string} */
const DumpAs = {
DOM_TREE: 'domtree',
FLAT_TREE: 'flattree',
};
// border-size of IFRAME which hosts sample HTML. This value comes from
// "core/css/html.css".
const kIFrameBorderSize = 2;
/** @const @type {string} */
const kTextArea = 'TEXTAREA';
class Traversal {
/**
* @param {!Node} node
* @return {Node}
*/
firstChildOf(node) { throw new Error('You should implement firstChildOf'); }
/**
* @param {!Node} node
* @return {!Generator<Node>}
*/
*childNodesOf(node) {
for (let child = this.firstChildOf(node); child !== null;
child = this.nextSiblingOf(child)) {
yield child;
}
}
/**
* @param {!Window} window
* @return !SampleSelection
*/
fromDOMSelection(window) {
throw new Error('You should implement fromDOMSelection');
}
/**
* @param {!Node} node
* @return {Node}
*/
nextSiblingOf(node) { throw new Error('You should implement nextSiblingOf'); }
}
class DOMTreeTraversal extends Traversal {
/**
* @override
* @param {!Node} node
* @return {Node}
*/
firstChildOf(node) { return node.firstChild; }
/**
* @param {!Window} window
* @return !SampleSelection
*/
fromDOMSelection(window) {
return SampleSelection.fromDOMSelection(window.getSelection());
}
/**
* @param {!Node} node
* @return {Node}
*/
nextSiblingOf(node) { return node.nextSibling; }
};
class FlatTreeTraversal extends Traversal {
/**
* @override
* @param {!Node} node
* @return {Node}
*/
firstChildOf(node) { return internals.firstChildInFlatTree(node); }
/**
* @param {!Window} window
* @return !SampleSelection
*/
fromDOMSelection(window) {
return SampleSelection.fromDOMSelection(
internals.getSelectionInFlatTree(window));
}
/**
* @param {!Node} node
* @return {Node}
*/
nextSiblingOf(node) { return internals.nextSiblingInFlatTree(node); }
}
/**
* @param {!Node} node
* @return {boolean}
*/
function isCharacterData(node) {
return node.nodeType === Node.TEXT_NODE ||
node.nodeType === Node.COMMENT_NODE;
}
/**
* @param {!Node} node
* @return {boolean}
*/
function isElement(node) {
return node.nodeType === Node.ELEMENT_NODE;
}
/**
* @param {!Node} node
* @param {number} offset
*/
function checkValidNodeAndOffset(node, offset) {
if (!node)
throw new Error('Node parameter should not be a null.');
if (offset < 0)
throw new Error(`Assumes ${offset} >= 0`);
if (isElement(node)) {
if (offset > node.childNodes.length)
throw new Error(`Bad offset ${offset} for ${node}`);
return;
}
if (isCharacterData(node)) {
if (offset > node.nodeValue.length)
throw new Error(`Bad offset ${offset} for ${node}`);
return;
}
throw new Error(`Invalid node: ${node}`);
}
class SampleSelection {
/** @public */
constructor() {
/** @type {?Node} */
this.anchorNode_ = null;
/** @type {number} */
this.anchorOffset_ = 0;
/** @type {?Node} */
this.focusNode_ = null;
/** @type {number} */
this.focusOffset_ = 0;
/** @type {HTMLElement} */
this.shadowHost_ = null;
}
/**
* @public
* @param {!Node} node
* @param {number} offset
*/
collapse(node, offset) {
checkValidNodeAndOffset(node, offset);
this.anchorNode_ = this.focusNode_ = node;
this.anchorOffset_ = this.focusOffset_ = offset;
}
/**
* @public
* @param {!Node} node
* @param {number} offset
*/
extend(node, offset) {
checkValidNodeAndOffset(node, offset);
this.focusNode_ = node;
this.focusOffset_ = offset;
}
/** @public @return {?Node} */
get anchorNode() {
console.assert(!this.isNone, 'Selection should not be a none.');
return this.anchorNode_;
}
/** @public @return {number} */
get anchorOffset() {
console.assert(!this.isNone, 'Selection should not be a none.');
return this.anchorOffset_;
}
/** @public @return {?Node} */
get focusNode() {
console.assert(!this.isNone, 'Selection should not be a none.');
return this.focusNode_;
}
/** @public @return {number} */
get focusOffset() {
console.assert(!this.isNone, 'Selection should not be a none.');
return this.focusOffset_;
}
/** @public @return {HTMLElement} */
get shadowHost() {
return this.shadowHost_;
}
/**
* @public
* @return {boolean}
*/
get isCollapsed() {
return this.anchorNode === this.focusNode &&
this.anchorOffset === this.focusOffset;
}
/**
* @public
* @return {boolean}
*/
get isNone() { return this.anchorNode_ === null; }
/**
* @public
* @param {!Selection} domSelection
* @return {!SampleSelection}
*/
static fromDOMSelection(domSelection) {
/** type {!SampleSelection} */
const selection = new SampleSelection();
selection.anchorNode_ = domSelection.anchorNode;
selection.anchorOffset_ = domSelection.anchorOffset;
selection.focusNode_ = domSelection.focusNode;
selection.focusOffset_ = domSelection.focusOffset;
if (selection.anchorNode_ === null)
return selection;
const document = selection.anchorNode_.ownerDocument;
selection.shadowHost_ = (() => {
if (!document.activeElement)
return null;
if (document.activeElement.nodeName !== kTextArea)
return null;
const selectedNode =
selection.anchorNode.childNodes[selection.anchorOffset];
if (document.activeElement !== selectedNode)
return null;
return selectedNode;
})();
return selection;
}
/** @override */
toString() {
if (this.isNone)
return 'SampleSelection()';
if (this.isCollapsed)
return `SampleSelection(${this.focusNode_}@${this.focusOffset_})`;
return `SampleSelection(anchor: ${this.anchorNode_}@${this.anchorOffset_}` +
`focus: ${this.focusNode_}@${this.focusOffset_}`;
}
}
// Extracts selection from marker "^" as anchor and "|" as focus from
// DOM tree and removes them.
class Parser {
/** @private */
constructor() {
/** @type {?Node} */
this.anchorNode_ = null;
/** @type {number} */
this.anchorOffset_ = 0;
/** @type {?Node} */
this.focusNode_ = null;
/** @type {number} */
this.focusOffset_ = 0;
}
/**
* @public
* @return {!SampleSelection}
*/
get selection() {
const selection = new SampleSelection();
if (!this.anchorNode_ && !this.focusNode_)
return selection;
if (this.anchorNode_ && this.focusNode_) {
selection.collapse(this.anchorNode_, this.anchorOffset_);
selection.extend(this.focusNode_, this.focusOffset_);
return selection;
}
if (this.focusNode_) {
selection.collapse(this.focusNode_, this.focusOffset_);
return selection;
}
throw new Error('There is no focus marker');
}
/**
* @private
* @param {!CharacterData} node
* @param {number} nodeIndex
*/
handleCharacterData(node, nodeIndex) {
/** @type {string} */
const text = node.nodeValue;
/** @type {number} */
const anchorOffset = text.indexOf('^');
/** @type {number} */
const focusOffset = text.indexOf('|');
/** @type {!Node} */
const parentNode = node.parentNode;
node.nodeValue = text.replace('^', '').replace('|', '');
if (node.nodeValue.length == 0) {
if (anchorOffset >= 0)
this.rememberSelectionAnchor(parentNode, nodeIndex);
if (focusOffset >= 0)
this.rememberSelectionFocus(parentNode, nodeIndex);
node.remove();
return;
}
if (anchorOffset >= 0 && focusOffset >= 0) {
if (anchorOffset > focusOffset) {
this.rememberSelectionAnchor(node, anchorOffset - 1);
this.rememberSelectionFocus(node, focusOffset);
return;
}
this.rememberSelectionAnchor(node, anchorOffset);
this.rememberSelectionFocus(node, focusOffset - 1);
return;
}
if (anchorOffset >= 0) {
this.rememberSelectionAnchor(node, anchorOffset);
return;
}
if (focusOffset < 0)
return;
this.rememberSelectionFocus(node, focusOffset);
}
/**
* @private
* @param {!Element} element
*/
handleElementNode(element) {
/** @type {number} */
let childIndex = 0;
for (const child of Array.from(element.childNodes)) {
this.parseInternal(child, childIndex);
if (!child.parentNode)
continue;
++childIndex;
}
}
/**
* @private
* @param {!Node} node
* @return {!SampleSelection}
*/
parse(node) {
this.parseInternal(node, 0);
return this.selection;
}
/**
* @private
* @param {!Node} node
* @param {number} nodeIndex
*/
parseInternal(node, nodeIndex) {
if (isElement(node))
return this.handleElementNode(node);
if (isCharacterData(node))
return this.handleCharacterData(node, nodeIndex);
throw new Error(`Unexpected node ${node}`);
}
/**
* @private
* @param {!Node} node
* @param {number} offset
*/
rememberSelectionAnchor(node, offset) {
checkValidNodeAndOffset(node, offset);
console.assert(
this.anchorNode_ === null, 'Anchor marker should be one.',
this.anchorNode_, this.anchorOffset_);
this.anchorNode_ = node;
this.anchorOffset_ = offset;
}
/**
* @private
* @param {!Node} node
* @param {number} offset
*/
rememberSelectionFocus(node, offset) {
checkValidNodeAndOffset(node, offset);
console.assert(
this.focusNode_ === null, 'Focus marker should be one.',
this.focusNode_, this.focusOffset_);
this.focusNode_ = node;
this.focusOffset_ = offset;
}
/**
* @public
* @param {!Node} node
* @return {!SampleSelection}
*/
static parse(node) { return (new Parser()).parse(node); }
}
// TODO(yosin): Once we can import JavaScript file from scripts, we should
// import "external/wpt/html/resources/common.js", since |HTML5_VOID_ELEMENTS|
// is defined in there.
/**
* @const @type {!Set<string>}
* only void (without end tag) HTML5 elements
*/
const HTML5_VOID_ELEMENTS = new Set([
'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input',
'keygen', 'link', 'meta', 'param', 'source','track', 'wbr' ]);
class Serializer {
/**
* @public
* @param {!SampleSelection} selection
* @param {!Traversal} traversal
*/
constructor(selection, traversal) {
/** @type {!SampleSelection} */
this.selection_ = selection;
/** @type {!Array<string>} */
this.strings_ = [];
/** @type {!Traversal} */
this.traversal_ = traversal;
}
/**
* @private
* @param {string} string
*/
emit(string) { this.strings_.push(string); }
/**
* @private
* @param {!HTMLElement} parentNode
* @param {number} childIndex
*/
handleSelection(parentNode, childIndex) {
if (this.selection_.isNone)
return;
if (this.selection_.shadowHost)
return;
if (parentNode === this.selection_.focusNode &&
childIndex === this.selection_.focusOffset) {
this.emit('|');
return;
}
if (parentNode === this.selection_.anchorNode &&
childIndex === this.selection_.anchorOffset) {
this.emit('^');
}
}
/**
* @private
* @param {!CharacterData} node
*/
handleCharacterData(node) {
/** @type {string} */
const text = node.nodeValue;
if (this.selection_.isNone)
return this.emit(text);
/** @type {number} */
const anchorOffset = this.selection_.anchorOffset;
/** @type {number} */
const focusOffset = this.selection_.focusOffset;
if (node === this.selection_.focusNode &&
node === this.selection_.anchorNode) {
if (anchorOffset === focusOffset) {
this.emit(text.substr(0, focusOffset));
this.emit('|');
this.emit(text.substr(focusOffset));
return;
}
if (anchorOffset < focusOffset) {
this.emit(text.substr(0, anchorOffset));
this.emit('^');
this.emit(text.substr(anchorOffset, focusOffset - anchorOffset));
this.emit('|');
this.emit(text.substr(focusOffset));
return;
}
this.emit(text.substr(0, focusOffset));
this.emit('|');
this.emit(text.substr(focusOffset, anchorOffset - focusOffset));
this.emit('^');
this.emit(text.substr(anchorOffset));
return;
}
if (node === this.selection_.anchorNode) {
this.emit(text.substr(0, anchorOffset));
this.emit('^');
this.emit(text.substr(anchorOffset));
return;
}
if (node === this.selection_.focusNode) {
this.emit(text.substr(0, focusOffset));
this.emit('|');
this.emit(text.substr(focusOffset));
return;
}
this.emit(text);
}
/**
* @private
* @param {!HTMLElement} element
*/
handleElementNode(element) {
/** @type {string} */
const tagName = element.tagName.toLowerCase();
this.emit(`<${tagName}`);
Array.from(element.attributes)
.sort((attr1, attr2) => attr1.name.localeCompare(attr2.name))
.forEach(attr => {
if (attr.value === '')
return this.emit(` ${attr.name}`);
const value = attr.value.replace(/&/g, '&amp;')
.replace(/\u0022/g, '&quot;')
.replace(/\u0027/g, '&apos;');
this.emit(` ${attr.name}="${value}"`);
});
this.emit('>');
if (element.nodeName === kTextArea)
return this.handleTextArea(element);
if (this.traversal_.firstChildOf(element) === null &&
HTML5_VOID_ELEMENTS.has(tagName)) {
return;
}
this.serializeChildren(element);
this.emit(`</${tagName}>`);
}
/**
* @private
* @param {!HTMLTextAreaElement} textArea
*/
handleTextArea(textArea) {
/** @type {string} */
const value = textArea.value;
if (this.selection_.shadowHost !== textArea) {
this.emit(value);
} else {
/** @type {number} */
const start = textArea.selectionStart;
/** @type {number} */
const end = textArea.selectionEnd;
/** @type {boolean} */
const isBackward = start < end &&
textArea.selectionDirection === 'backward';
const startMarker = isBackward ? '|' : '^';
const endMarker = isBackward ? '^' : '|';
this.emit(value.substr(0, start));
if (start < end) {
this.emit(startMarker);
this.emit(value.substr(start, end - start));
}
this.emit(endMarker);
this.emit(value.substr(end));
}
this.emit('</textarea>');
}
/**
* @public
* @param {!HTMLDocument} document
* @param {boolean} dumpFromRoot
*/
serialize(document, dumpFromRoot) {
if (document.body && !dumpFromRoot)
this.serializeChildren(document.body);
else
this.serializeInternal(document.documentElement);
return this.strings_.join('');
}
/**
* @private
* @param {!HTMLElement} element
*/
serializeChildren(element) {
if (this.traversal_.firstChildOf(element) === null) {
this.handleSelection(element, 0);
return;
}
/** @type {number} */
let childIndex = 0;
for (let child of this.traversal_.childNodesOf(element)) {
this.handleSelection(element, childIndex);
this.serializeInternal(child, childIndex);
++childIndex;
}
this.handleSelection(element, childIndex);
}
/**
* @private
* @param {!Node} node
*/
serializeInternal(node) {
if (isElement(node))
return this.handleElementNode(node);
if (isCharacterData(node))
return this.handleCharacterData(node);
throw new Error(`Unexpected node ${node}`);
}
}
/**
* @param {!HTMLElement} element
* @return {number}
*/
function computeLeft(element) {
let left = kIFrameBorderSize + element.ownerDocument.offsetLeft;
for (let runner = element; runner; runner = runner.offsetParent)
left += runner.offsetLeft;
return left;
}
/**
* @param {!HTMLElement} element
* @return {number}
*/
function computeRight(element) {
return this.computeLeft(element) + element.offsetWidth;
}
/**
* @param {!HTMLElement} element
* @return {number}
*/
function computeTop(element) {
let top = kIFrameBorderSize + element.ownerDocument.offsetTop;
for (let runner = element; runner; runner = runner.offsetParent)
top += runner.offsetTop;
return top;
}
/**
* @param {!HTMLElement} element
* @return {number}
*/
function computeBottom(element) {
return this.computeTop(element) + element.offsetHeight;
}
/**
* @this {!Selection}
* @param {string} html
* @param {string=} opt_text
*/
function setClipboardData(html, opt_text) {
assert_not_equals(window.internals, undefined,
'This test requests clipboard access from JavaScript.');
function computeTextData() {
if (opt_text !== undefined)
return opt_text;
const element = document.createElement('div');
element.innerHTML = html;
return element.textContent;
}
function copyHandler(event) {
const clipboardData = event.clipboardData;
clipboardData.setData('text/plain', computeTextData());
clipboardData.setData('text/html', html);
event.preventDefault();
}
document.addEventListener('copy', copyHandler);
document.execCommand('copy');
document.removeEventListener('copy', copyHandler);
}
class Sample {
/**
* @public
* @param {string} sampleText
*/
constructor(sampleText) {
/** @const @type {!HTMLIFrameElement} */
this.iframe_ = Sample.getOrCreatePlayground();
/** @const @type {!HTMLDocument} */
this.document_ = this.iframe_.contentDocument;
// Set focus to sample IFRAME to make |eventSender| and
// |testRunner.execCommand()| to work on sample rather than main frame.
this.iframe_.focus();
/** @const @type {!Selection} */
this.selection_ = this.iframe_.contentWindow.getSelection();
this.selection_.document = this.document_;
this.selection_.document.offsetLeft = this.iframe_.offsetLeft;
this.selection_.document.offsetTop = this.iframe_.offsetTop;
this.selection_.setClipboardData = setClipboardData;
this.selection_.computeLeft = computeLeft;
this.selection_.computeRight = computeRight;
this.selection_.computeTop = computeTop;
this.selection_.computeBottom = computeBottom;
this.load(sampleText);
}
/** @return {!HTMLDocument} */
get document() { return this.document_; }
/** @return {!Selection} */
get selection() { return this.selection_; }
/** @return {string} */
static get playgroundId() { return 'playground'; }
/**
* @public
* Marks this sample not to be reused.
*/
keep() {
this.iframe_.removeAttribute('id');
}
/**
* @private
* @param {string} sampleText
*/
load(sampleText) {
const anchorMarker = sampleText.indexOf('^');
const focusMarker = sampleText.indexOf('|');
if (focusMarker < 0 && anchorMarker >= 0) {
throw new Error(`You should specify caret position in "${sampleText}".`);
}
if (focusMarker != sampleText.lastIndexOf('|')) {
throw new Error(
`You should have at least one focus marker "|" in "${sampleText}".`);
}
if (anchorMarker != sampleText.lastIndexOf('^')) {
throw new Error(
`You should have at most one anchor marker "^" in "${sampleText}".`);
}
if (anchorMarker >= 0 && focusMarker >= 0 &&
(anchorMarker + 1 === focusMarker || anchorMarker - 1 === focusMarker)) {
throw new Error(
`You should have focus marker and should not have anchor marker if and only if selection is a caret in "${sampleText}".`);
}
this.document_.body.innerHTML = sampleText;
/** @type {!SampleSelection} */
const selection = Parser.parse(this.document_.body);
if (selection.isNone)
return;
if (this.loadSelectionInTextArea(selection))
return;
this.selection_.collapse(selection.anchorNode, selection.anchorOffset);
if (this.selection_.rangeCount > 0)
this.selection_.extend(selection.focusNode, selection.focusOffset);
}
/**
* @private
* @param {!SampleSelection} selection
* @return {boolean} Returns true if selection is in TEXTAREA.
*/
loadSelectionInTextArea(selection) {
/** @type {Node} */
const enclosingNode = selection.anchorNode.parentNode;
if (selection.focusNode.parentNode !== enclosingNode)
return false;
if (enclosingNode.nodeName !== kTextArea)
return false;
if (selection.anchorNode !== selection.focusNode)
throw new Error('Selection in TEXTAREA should be in same Text node.');
enclosingNode.focus();
if (selection.anchorOffset < selection.focusOffset) {
enclosingNode.setSelectionRange(selection.anchorOffset,
selection.focusOffset);
return true;
}
enclosingNode.setSelectionRange(selection.focusOffset,
selection.anchorOffset,
'backward');
return true;
}
/**
* @public
*/
remove() { this.iframe_.remove(); }
/**
* @public
*/
reset() {
if (window.internals && internals.isOverwriteModeEnabled(this.document_))
internals.toggleOverwriteModeEnabled(this.document_);
this.document_.documentElement.innerHTML = '<head></head><body></body>';
this.selection.removeAllRanges();
this.iframe_.style.display = 'none';
}
/** @return {HTMLIFrameElement} */
static getOrCreatePlayground() {
const present = document.getElementById(Sample.playgroundId);
if (present) {
present.style.display = 'block';
return present;
}
const iframe = document.createElement('iframe');
iframe.setAttribute('id', Sample.playgroundId);
if (!document.body)
document.body = document.createElement("body");
document.body.appendChild(iframe);
return iframe;
}
/**
* @public
* @param {!Traversal} traversal
* @param {boolean} dumpFromRoot
* @return {string}
*/
serialize(traversal, dumpFromRoot) {
/** @type {!SampleSelection} */
const selection = traversal.fromDOMSelection(this.document_.defaultView);
/** @type {!Serializer} */
const serializer = new Serializer(selection, traversal);
return serializer.serialize(this.document_, dumpFromRoot);
}
}
function assembleDescription() {
function getStack() {
let stack;
try {
throw new Error('get line number');
} catch (error) {
stack = error.stack.split('\n').slice(1);
}
return stack
}
const RE_IN_ASSERT_SELECTION = new RegExp('assert_selection\\.js');
for (const line of getStack()) {
const match = RE_IN_ASSERT_SELECTION.exec(line);
if (!match) {
const RE_LAYOUTTESTS = new RegExp('(?:LayoutTests|web_tests).*');
return RE_LAYOUTTESTS.exec(line);
}
}
return '';
}
/**
* @param {string} expectedText
*/
function checkExpectedText(expectedText) {
/** @type {number} */
const anchorOffset = expectedText.indexOf('^');
/** @type {number} */
const focusOffset = expectedText.indexOf('|');
if (anchorOffset != expectedText.lastIndexOf('^')) {
throw new Error(
`You should have at most one anchor marker "^" in "${expectedText}".`);
}
if (focusOffset != expectedText.lastIndexOf('|')) {
throw new Error(
`You should have at most one focus marker "|" in "${expectedText}".`);
}
if (anchorOffset >= 0 && focusOffset < 0) {
throw new Error(
`You should have a focus marker "|" in "${expectedText}".`);
}
if (anchorOffset >= 0 && focusOffset >= 0 &&
(anchorOffset + 1 === focusOffset || anchorOffset - 1 === focusOffset)) {
throw new Error(
`You should have focus marker and should not have anchor marker if and only if selection is a caret in "${expectedText}".`);
}
}
/**
* @param {string} str1
* @param {string} str2
* @return {string}
*/
function commonPrefixOf(str1, str2) {
for (let index = 0; index < str1.length; ++index) {
if (str1[index] !== str2[index])
return str1.substr(0, index);
}
return str1;
}
/**
* @param {string} passedInputText
* @param {function(!Selection)|string} tester
* @param {string} expectedText
* @param {Object=} opt_options
* @return {!Sample}
*/
function assertSelectionAndReturnSample(
passedInputText, tester, passedExpectedText, opt_options = {}) {
const kDescription = 'description';
const kDumpAs = 'dumpAs';
const kRemoveSampleIfSucceeded = 'removeSampleIfSucceeded';
const kDumpFromRoot = 'dumpFromRoot';
/** @type {!Object} */
const options = typeof(opt_options) === 'string'
? {description: opt_options} : opt_options;
/** @type {string} */
const description = kDescription in options
? options[kDescription] : assembleDescription();
/** @type {boolean} */
const removeSampleIfSucceeded = kRemoveSampleIfSucceeded in options
? !!options[kRemoveSampleIfSucceeded] : true;
/** @type {DumpAs} */
const dumpAs = options[kDumpAs] || DumpAs.DOM_TREE;
/** @type {boolean} */
const dumpFromRoot = options[kDumpFromRoot] || false;
const inputText = (() => {
if (typeof(passedInputText) === 'string')
return passedInputText;
if (Array.isArray(passedInputText))
return passedInputText.join("");
throw new Error('InputText must be a string or an array of strings.');
})();
const expectedText = (() => {
if (typeof(passedExpectedText) === 'string')
return passedExpectedText;
if (Array.isArray(passedExpectedText))
return passedExpectedText.join("");
throw new Error('ExpectedText must be a string or an array of strings.');
})();
checkExpectedText(expectedText);
const sample = new Sample(inputText);
if (typeof(tester) === 'function') {
tester.call(window, sample.selection);
} else if (typeof(tester) === 'string') {
const strings = tester.split(/ (.+)/);
sample.document.execCommand(strings[0], false, strings[1]);
} else {
throw new Error(`Invalid tester: ${tester}`);
}
/** @type {!Traversal} */
const traversal = (() => {
switch (dumpAs) {
case DumpAs.DOM_TREE:
return new DOMTreeTraversal();
case DumpAs.FLAT_TREE:
if (!window.internals)
throw new Error('This test requires window.internals.');
return new FlatTreeTraversal();
default:
throw `${kDumpAs} must be one of ` +
`{${Object.values(DumpAs).join(', ')}}` +
` instead of '${dumpAs}'`;
}
})();
/** @type {string} */
const actualText = sample.serialize(traversal, dumpFromRoot);
// We keep sample HTML when assertion is false for ease of debugging test
// case.
if (actualText === expectedText) {
if (removeSampleIfSucceeded)
sample.reset();
else
sample.keep();
return sample;
}
sample.keep();
throw new Error(`${description}\n` +
`\t expected ${expectedText},\n` +
`\t but got ${actualText},\n` +
`\t sameupto ${commonPrefixOf(expectedText, actualText)}`);
}
/** Like `assertSelectionAndReturnSample` but without return value.
*
* @param {string} passedInputText
* @param {function(!Selection)|string} tester
* @param {string} expectedText
* @param {Object=} opt_options
*/
function assertSelection(
passedInputText, tester, passedExpectedText, opt_options) {
assertSelectionAndReturnSample(passedInputText, tester, passedExpectedText, opt_options);
}
/**
* @param {string} inputText
* @param {function(!Selection)|string} tester
* @param {string} expectedText
* @param {Object=} opt_options
* @param {string=} opt_description
*/
function selectionTest(inputText, tester, expectedText, opt_options,
opt_description) {
const description = typeof(opt_options) === 'string' ? opt_options
: opt_description;
const options = typeof(opt_options) === 'string' ? undefined : opt_options;
test(() => assertSelection(inputText, tester, expectedText, options),
description);
}
// Export symbols
window.Sample = Sample;
window.assert_selection = assertSelection;
window.assert_selection_and_return_sample = assertSelectionAndReturnSample;
window.selection_test = selectionTest;
window.DOMTreeTraversal = DOMTreeTraversal;
})();