| Index: third_party/WebKit/LayoutTests/editing/foo.js
|
| diff --git a/third_party/WebKit/LayoutTests/editing/foo.js b/third_party/WebKit/LayoutTests/editing/foo.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..090fbc1b54ea4a82f267953e21b58d3c59345a04
|
| --- /dev/null
|
| +++ b/third_party/WebKit/LayoutTests/editing/foo.js
|
| @@ -0,0 +1,943 @@
|
| +// 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.
|
| +//
|
| +// 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',
|
| + };
|
| +
|
| + /** @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 {!DOMSelection} selection
|
| + * @return !SampleSelection
|
| + */
|
| + fromDOMSelection(selection) {
|
| + 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 {!DOMSelection} selection
|
| + * @return !SampleSelection
|
| + */
|
| + fromDOMSelection(selection) {
|
| + return SampleSelection.fromDOMSelection(selection);
|
| + }
|
| +
|
| + /**
|
| + * @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 {!DOMSelection} selection
|
| + * @return !SampleSelection
|
| + */
|
| + fromDOMSelection(selection) {
|
| + // TODO(yosin): We should return non-scoped selection rather than
|
| + // selection
|
| + // scoped in main tree.
|
| + return SampleSelection.fromDOMSelection(selection);
|
| + }
|
| +
|
| + /**
|
| + * @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 "imported/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<strings>} */
|
| + 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, '&')
|
| + .replace(/\u0022/g, '"')
|
| + .replace(/\u0027/g, ''');
|
| + 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 {!HTMLTextArea}
|
| + */
|
| + 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
|
| + */
|
| + serialize(document) {
|
| + if (document.body)
|
| + 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}`);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * @this {!DOMSelection}
|
| + * @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 {!HTMLIFame} */
|
| + this.iframe_ = document.createElement('iframe');
|
| + if (!document.body)
|
| + document.body = document.createElement('body');
|
| + document.body.appendChild(this.iframe_);
|
| + /** @const @type {!HTMLDocument} */
|
| + this.document_ = this.iframe_.contentDocument;
|
| + /** @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;
|
| +
|
| + // Set focus to sample IFRAME to make |eventSender| and
|
| + // |testRunner.execCommand()| to work on sample rather than main frame.
|
| + this.iframe_.focus();
|
| + this.load(sampleText);
|
| + }
|
| +
|
| + /** @return {!HTMLDocument} */
|
| + get document() { return this.document_; }
|
| +
|
| + /** @return {!Selection} */
|
| + get selection() { return this.selection_; }
|
| +
|
| + /**
|
| + * @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);
|
| + 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
|
| + * @param {!Traversal} traversal
|
| + * @return {string}
|
| + */
|
| + serialize(traversal) {
|
| + /** @type {!SampleSelection} */
|
| + const selection = traversal.fromDOMSelection(this.selection_);
|
| + /** @type {!Serializer} */
|
| + const serializer = new Serializer(selection, traversal);
|
| + return serializer.serialize(this.document_);
|
| + }
|
| + }
|
| +
|
| + 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.*');
|
| + 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} inputText
|
| + * @param {function(!Selection)|string}
|
| + * @param {string} expectedText
|
| + * @param {Object=} opt_options
|
| + * @return {!Sample}
|
| + */
|
| + function assertSelection(inputText, tester, expectedText, opt_options = {}) {
|
| + const kDescription = 'description';
|
| + const kDumpAs = 'dumpAs';
|
| + const kRemoveSampleIfSucceeded = 'removeSampleIfSucceeded';
|
| + /** @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;
|
| +
|
| + 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);
|
| + // We keep sample HTML when assertion is false for ease of debugging test
|
| + // case.
|
| + if (actualText === expectedText) {
|
| + if (removeSampleIfSucceeded)
|
| + sample.remove();
|
| + return sample;
|
| + }
|
| + throw new Error(
|
| + `${description}\n` +
|
| + `\t expected ${expectedText},\n` +
|
| + `\t but got ${actualText},\n` +
|
| + `\t sameupto ${commonPrefixOf(expectedText, actualText)}`);
|
| + }
|
| +
|
| + // Export symbols
|
| + window.Sample = Sample;
|
| + window.assert_selection = assertSelection;
|
| +})();
|
|
|