Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(25)

Unified Diff: third_party/WebKit/LayoutTests/editing/assert_selection.js

Issue 2013603004: Introduce assert_selection() for editing tests (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: 2016-05-26T19:25:56 Created 4 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | third_party/WebKit/LayoutTests/editing/execCommand/createLink.html » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: third_party/WebKit/LayoutTests/editing/assert_selection.js
diff --git a/third_party/WebKit/LayoutTests/editing/assert_selection.js b/third_party/WebKit/LayoutTests/editing/assert_selection.js
new file mode 100644
index 0000000000000000000000000000000000000000..9599a1745076f392b7e4c0513afbe767a9f95734
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/editing/assert_selection.js
@@ -0,0 +1,612 @@
+// 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)| 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.
+//
+// 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() {
+/**
+ * @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;
+ }
+
+ /**
+ * @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 {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;
+ 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
+ */
+ constructor(selection) {
+ /** @type {!SampleSelection} */
+ this.selection_ = selection;
+ /** @type {!Array<strings>} */
+ this.strings_ = [];
+ }
+
+ /**
+ * @private
+ * @param {string} string
+ */
+ emit(string) { this.strings_.push(string); }
+
+ /**
+ * @private
+ * @param {!CharacterData} node
+ */
+ handleCharacterData(node) {
+ /** @type {string} */
+ const text = node.nodeValue;
+ /** @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
+ * @param {number} nodeIndex
+ */
+ handleElementNode(element, nodeIndex) {
+ if (element.parenNode === this.selection_.focusNode &&
+ nodeIndex === this.selection_.focusOffset) {
+ this.emit('|');
+ } else if (
+ element === this.selection_.anchorNode &&
+ nodeIndex === this.selection_.anchorOffset) {
+ this.emit('^');
+ }
+ /** @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.childNodes.length === 0 &&
+ HTML5_VOID_ELEMENTS.has(tagName)) {
+ return;
+ }
+ /** @type {number} */
+ let childIndex = 0;
+ for (const child of Array.from(element.childNodes)) {
+ this.serializeInternal(child, childIndex);
+ ++childIndex;
+ }
+ this.emit(`</${tagName}>`);
+ }
+
+ /**
+ * @public
+ * @param {!HTMLElement} element
+ */
+ serialize(element) {
+ if (this.selection_.isNone)
+ return node.outerHTML;
+ this.serializeInternal(element, 0);
+ return this.strings_.join('');
+ }
+
+ /**
+ * @private
+ * @param {!Node} node
+ * @param {number} nodeIndex
+ */
+ serializeInternal(node, nodeIndex) {
+ if (isElement(node))
+ return this.handleElementNode(node, nodeIndex);
+ if (isCharacterData(node))
+ return this.handleCharacterData(node);
+ throw new Error(`Unexpected node ${node}`);
+ }
+}
+
+class Sample {
+ /**
+ * @public
+ * @param {string} sampleText
+ */
+ constructor(sampleText) {
+ /** @const @type {!HTMLIFame} */
+ this.iframe_ = document.createElement('iframe');
+ 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.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) {
+ 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}".`);
+ }
+ this.document_.body.innerHTML = sampleText;
+ /** @type {!SampleSelection} */
+ const selection = Parser.parse(this.document_.body);
+ if (selection.isNone)
+ return;
+ this.selection_.collapse(selection.anchorNode, selection.anchorOffset);
+ this.selection_.extend(selection.focusNode, selection.focusOffset);
+ }
+
+ /**
+ * @public
+ */
+ remove() { this.iframe_.remove(); }
+
+ /**
+ * @public
+ * @return {string}
+ */
+ serialize() {
+ /** @type {!SampleSelection} */
+ const selection = SampleSelection.fromDOMSelection(this.selection_);
+ /** @type {!Serializer} */
+ const serializer = new Serializer(selection);
+ return serializer.serialize(this.document_.body.firstChild);
+ }
+}
+
+function assembleDescription() {
+ const RE_TEST_FUNCTION =
+ new RegExp('at Object.test \\(.*?([^/]+?):(\\d+):(\\d+)\\)');
+ function getStack() {
+ let stack;
+ try {
+ throw new Error('get line number');
+ } catch (error) {
+ stack = error.stack.split('\n').slice(1);
+ }
+ return stack
+ }
+ for (const line of getStack()) {
+ const match = RE_TEST_FUNCTION.exec(line);
+ if (!match)
+ continue;
+ return `${match[1]}(${match[2]})`;
+ }
+ return '';
+}
+
+/**
+ * @param {string} inputText
+ * @param {function(!Selection)|string}
+ * @param {string} expectedText
+ * @param {string=} opt_description
+ */
+function assertSelection(
+ inputText, tester, expectedText, opt_description = '') {
+ /** @type {string} */
+ const description =
+ opt_description === '' ? assembleDescription() : opt_description;
+ if (expectedText.indexOf('^') != expectedText.lastIndexOf('^')) {
+ throw new Error(
+ `You should have at most one anchor marker "^" in "${expectedText}".`);
+ }
+ if (expectedText.indexOf('|') != expectedText.lastIndexOf('|')) {
+ throw new Error(
+ `You should have at most one focus marker "|" in "${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 {string} */
+ const actualText = sample.serialize();
+ // We keep sample HTML when assertion is false for ease of debugging test
+ // case.
+ if (actualText == expectedText)
+ sample.remove();
+ assert_equals(actualText, expectedText, description);
+}
+
+// Export symbols
+window.Sample = Sample;
+window.assert_selection = assertSelection;
+})();
« no previous file with comments | « no previous file | third_party/WebKit/LayoutTests/editing/execCommand/createLink.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698