| Index: third_party/WebKit/LayoutTests/editing/spelling/spellcheck_test.js
|
| diff --git a/third_party/WebKit/LayoutTests/editing/spelling/spellcheck_test.js b/third_party/WebKit/LayoutTests/editing/spelling/spellcheck_test.js
|
| index c98232bd9d33690a6f8a6834644254994e325c0f..8c7a076a60e7627267c537013ed4209c0c89efbb 100644
|
| --- a/third_party/WebKit/LayoutTests/editing/spelling/spellcheck_test.js
|
| +++ b/third_party/WebKit/LayoutTests/editing/spelling/spellcheck_test.js
|
| @@ -5,235 +5,278 @@
|
| 'use strict';
|
|
|
| // This file provides
|
| -// |spellcheck_test(sample, tester, expectedMarkers, opt_title)| asynchronous
|
| -// test to W3C test harness for easier writing of editing test cases.
|
| +// |spellcheck_test(sample, tester, expectedText, opt_title)| asynchronous test
|
| +// to W3C test harness for easier writing of spellchecker test cases.
|
| //
|
| // |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 "^" indicating the initial selection.
|
| +// TODO(editing-dev): Make initial selection work with TEXTAREA and INPUT.
|
| //
|
| // |tester| is either name with parameter of execCommand or function taking
|
| // one parameter |Document|.
|
| //
|
| -// |expectedMarkers| is either a |Marker| or a |Marker| array, where each
|
| -// |Marker| is an |Object| with the following properties:
|
| -// - |location| and |length| are integers indicating the range of the marked
|
| -// text. It must hold that |location >= 0| and |length > 0|.
|
| -// - |type| is an optional string indicating the marker type. When present, it
|
| -// must be equal to either "spelling" or "grammer". When absent, it is
|
| -// regarded as "spelling".
|
| -// - |description| is an optional string indicating the description of a marker.
|
| +// |expectedText| is an HTML fragment indicating the expected result, where text
|
| +// with spelling marker is surrounded by '_', and text with grammar marker is
|
| +// surrounded by '~'.
|
| //
|
| // |opt_title| is an optional string giving the title of the test case.
|
| //
|
| -// Examples:
|
| -//
|
| -// spellcheck_test(
|
| -// '<div contentEditable>|</div>',
|
| -// 'insertText wellcome.',
|
| -// spellingMarker(0, 8, 'welcome'), // 'wellcome'
|
| -// 'Mark misspellings and give replacement suggestions after typing.');
|
| -//
|
| -// spellcheck_test(
|
| -// '<div contentEditable>|</div>',
|
| -// 'insertText You has the right.',
|
| -// grammarMarker(4, 3), // 'has'
|
| -// 'Mark ungrammatical phrases after typing.');
|
| +// See spellcheck_test.html for sample usage.
|
|
|
| (function() {
|
| const Sample = window.Sample;
|
|
|
| -/** @type {string} */
|
| -const kSpelling = 'spelling';
|
| -/** @type {string} */
|
| -const kGrammar = 'grammar';
|
| -
|
| -class Marker {
|
| - /**
|
| - * @public
|
| - * @param {number} location
|
| - * @param {number} length
|
| - * @param {string=} opt_type
|
| - * @param {string=} opt_description
|
| - */
|
| - constructor(location, length, opt_type, opt_description) {
|
| - /** @type {number} */
|
| - this.location_ = location;
|
| - /** @type {number} */
|
| - this.length_ = length;
|
| - /** @type {string} */
|
| - this.type_ = opt_type || 'spelling';
|
| - /** @type {boolean} */
|
| - this.ignoreDescription_ = opt_description === undefined;
|
| - /** @type {string} */
|
| - this.description_ = opt_description || '';
|
| - }
|
| -
|
| - /** @return {number} */
|
| - get location() { return this.location_; }
|
| -
|
| - /** @return {number} */
|
| - get length() { return this.length_; }
|
| -
|
| - /** @return {string} */
|
| - get type() { return this.type_; }
|
| -
|
| - /** @return {boolean} */
|
| - get ignoreDescription() { return this.ignoreDescription_; }
|
| -
|
| - /** @return {string} */
|
| - get description() { return this.description_; }
|
| -
|
| - /**
|
| - * @public
|
| - */
|
| - assertValid() {
|
| - // TODO(xiaochengh): Add proper assert descriptions when needed.
|
| - assert_true(Number.isInteger(this.location_));
|
| - assert_greater_than_equal(this.location_, 0);
|
| - assert_true(Number.isInteger(this.length_));
|
| - assert_greater_than(this.length_, 0);
|
| - assert_true(this.type_ === kSpelling || this.type_ === kGrammar);
|
| - assert_true(typeof this.description_ === 'string');
|
| - }
|
| -
|
| - /**
|
| - * @public
|
| - * @param {!Marker} expected
|
| - */
|
| - assertMatch(expected) {
|
| - try {
|
| - assert_equals(this.location, expected.location);
|
| - assert_equals(this.length, expected.length);
|
| - assert_equals(this.type, expected.type);
|
| - if (expected.ignoreDescription)
|
| - return;
|
| - assert_equals(this.description, expected.description);
|
| - } catch (error) {
|
| - throw new Error(`Expected ${expected} but got ${this}.`);
|
| - }
|
| - }
|
| +// TODO(editing-dev): 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' ]);
|
|
|
| - /** @override */
|
| - toString() {
|
| - return `${this.type_} marker at ` +
|
| - `[${this.location_}, ${this.location_ + this.length_}]` +
|
| - (this.description_ ? ` with description "${this.description_}"` : ``);
|
| - }
|
| -}
|
| +// TODO(editing-dev): Reduce code duplication with assert_selection's Serializer
|
| +// once we can import and export Javascript modules.
|
|
|
| /**
|
| - * @param {number} location
|
| - * @param {number} length
|
| - * @param {string=} opt_description
|
| - * @return {!Marker}
|
| + * @param {!Node} node
|
| + * @return {boolean}
|
| */
|
| -function spellingMarker(location, length, opt_description) {
|
| - return new Marker(location, length, kSpelling, opt_description);
|
| +function isCharacterData(node) {
|
| + return node.nodeType === Node.TEXT_NODE ||
|
| + node.nodeType === Node.COMMENT_NODE;
|
| }
|
|
|
| /**
|
| - * @param {number} location
|
| - * @param {number} length
|
| - * @param {string=} opt_description
|
| - * @return {!Marker}
|
| + * @param {!Node} node
|
| + * @return {boolean}
|
| */
|
| -function grammarMarker(location, length, opt_description) {
|
| - return new Marker(location, length, kGrammar, opt_description);
|
| +function isElement(node) {
|
| + return node.nodeType === Node.ELEMENT_NODE;
|
| }
|
|
|
| /**
|
| - * @param {!Marker} marker1
|
| - * @param {!Marker} marker2
|
| - * @return {number}
|
| + * @param {!Node} node
|
| + * @return {boolean}
|
| */
|
| -function markerComparison(marker1, marker2) {
|
| - return marker1.location - marker2.location;
|
| +function isHTMLInputElement(node) {
|
| + return node.nodeName === 'INPUT';
|
| }
|
|
|
| /**
|
| - * @param {!Array<!Marker>} expectedMarkers
|
| + * @param {!Node} node
|
| + * @return {boolean}
|
| */
|
| -function checkExpectedMarkers(expectedMarkers) {
|
| - if (expectedMarkers.length === 0)
|
| - return;
|
| - expectedMarkers.forEach(marker => marker.assertValid());
|
| - expectedMarkers.sort(markerComparison);
|
| - expectedMarkers.reduce((lastMarker, currentMarker) => {
|
| - assert_less_than(
|
| - lastMarker.location + lastMarker.length, currentMarker.location,
|
| - 'Marker ranges should be disjoint.');
|
| - return currentMarker;
|
| - });
|
| +function isHTMLTextAreaElement(node) {
|
| + return node.nodeName === 'TEXTAREA';
|
| }
|
|
|
| /**
|
| + * @param {?Range} range
|
| * @param {!Node} node
|
| - * @param {string} type
|
| - * @param {!Array<!Marker>} markers
|
| + * @param {number} offset
|
| */
|
| -function extractMarkersOfType(node, type, markers) {
|
| - /** @type {!HTMLBodyElement} */
|
| - const body = node.ownerDocument.body;
|
| - /** @type {number} */
|
| - const markerCount = window.internals.markerCountForNode(node, type);
|
| - for (let i = 0; i < markerCount; ++i) {
|
| - /** @type {!Range} */
|
| - const markerRange = window.internals.markerRangeForNode(node, type, i);
|
| +function isAtRangeEnd(range, node, offset) {
|
| + return range && node === range.endContainer && offset === range.endOffset;
|
| +}
|
| +
|
| +class MarkerSerializer {
|
| + /**
|
| + * @public
|
| + * @param {!Object} markerTypes
|
| + */
|
| + constructor(markerTypes) {
|
| + /** @type {!Array<string>} */
|
| + this.strings_ = [];
|
| + /** @type {!Object} */
|
| + this.markerTypes_ = markerTypes;
|
| + /** @type {!Object} */
|
| + this.activeMarkerRanges_ = {};
|
| + for (let type in markerTypes)
|
| + this.activeMarkerRanges_[type] = null;
|
| + }
|
| +
|
| + /**
|
| + * @private
|
| + * @param {string} string
|
| + */
|
| + emit(string) { this.strings_.push(string); }
|
| +
|
| + /**
|
| + * @private
|
| + * @param {!Node} node
|
| + * @param {number} offset
|
| + */
|
| + advancedTo(node, offset) {
|
| + for (let type in this.markerTypes_) {
|
| + // Handle the ending of the current active marker.
|
| + if (isAtRangeEnd(this.activeMarkerRanges_[type], node, offset)) {
|
| + this.activeMarkerRanges_[type] = null;
|
| + this.emit(this.markerTypes_[type]);
|
| + }
|
| +
|
| + // Handle the starting of the next active marker.
|
| + if (this.activeMarkerRanges_[type])
|
| + return;
|
| + /** @type {number} */
|
| + const markerCount = window.internals.markerCountForNode(node, type);
|
| + for (let i = 0; i < markerCount; ++i) {
|
| + const marker = window.internals.markerRangeForNode(node, type, i);
|
| + assert_equals(
|
| + marker.startContainer, node,
|
| + 'Internal error: marker range not starting in the annotated node.');
|
| + assert_equals(
|
| + marker.endContainer, node,
|
| + 'Internal error: marker range not ending in the annotated node.');
|
| + if (marker.startOffset === offset) {
|
| + assert_greater_than(marker.endOffset, offset,
|
| + 'Internal error: marker range is collapsed.');
|
| + this.activeMarkerRanges_[type] = marker;
|
| + this.emit(this.markerTypes_[type]);
|
| + break;
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * @private
|
| + * @param {!CharacterData} node
|
| + */
|
| + handleCharacterData(node) {
|
| /** @type {string} */
|
| - const description = window.internals.markerDescriptionForNode(node, type, i);
|
| + const text = node.nodeValue;
|
| /** @type {number} */
|
| - const location = window.internals.locationFromRange(body, markerRange);
|
| - /** @type {number} */
|
| - const length = window.internals.lengthFromRange(body, markerRange);
|
| + const length = text.length;
|
| + for (let offset = 0; offset < length; ++offset) {
|
| + this.advancedTo(node, offset);
|
| + this.emit(text[offset]);
|
| + }
|
| + this.advancedTo(node, length);
|
| + }
|
|
|
| - markers.push(new Marker(location, length, type, description));
|
| + /**
|
| + * @private
|
| + * @param {!HTMLElement} element
|
| + */
|
| + handleInnerEditorOf(element) {
|
| + /** @type {!ShadowRoot} */
|
| + const shadowRoot = window.internals.shadowRoot(element);
|
| + assert_not_equals(
|
| + shadowRoot, undefined,
|
| + 'Internal error: text form control element not having shadow tree as ' +
|
| + 'inner editor.');
|
| + /** @type {!HTMLDivElement} */
|
| + const innerEditor = shadowRoot.firstChild;
|
| + assert_equals(innerEditor.tagName, 'DIV',
|
| + 'Internal error: inner editor is not DIV');
|
| + innerEditor.childNodes.forEach(child => {
|
| + assert_true(isCharacterData(child),
|
| + 'Internal error: inner editor having child node that is ' +
|
| + 'not CharacterData.');
|
| + this.handleCharacterData(child);
|
| + });
|
| }
|
| -}
|
|
|
| -/**
|
| - * @param {!Node} node
|
| - * @param {!Array<!Marker>} markers
|
| - */
|
| -function extractAllMarkersRecursivelyTo(node, markers) {
|
| - extractMarkersOfType(node, kSpelling, markers);
|
| - extractMarkersOfType(node, kGrammar, markers);
|
| - node.childNodes.forEach(
|
| - child => extractAllMarkersRecursivelyTo(child, markers));
|
| -}
|
| + /**
|
| + * @private
|
| + * @param {!HTMLInputElement} element
|
| + */
|
| + handleInputNode(element) {
|
| + this.emit(' value="');
|
| + this.handleInnerEditorOf(element);
|
| + this.emit('"');
|
| + }
|
|
|
| -/**
|
| - * @param {!Document} doc
|
| - * @return {!Array<!Marker>}
|
| - */
|
| -function extractAllMarkers(doc) {
|
| - /** @type {!Array<!Marker>} */
|
| - const markers = [];
|
| - extractAllMarkersRecursivelyTo(doc.body, markers);
|
| - markers.sort(markerComparison);
|
| - return markers;
|
| + /**
|
| + * @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}"`);
|
| + });
|
| + if (isHTMLInputElement(element) && element.value)
|
| + this.handleInputNode(element);
|
| + this.emit('>');
|
| + if (HTML5_VOID_ELEMENTS.has(tagName))
|
| + return;
|
| + this.serializeChildren(element);
|
| + this.emit(`</${tagName}>`);
|
| + }
|
| +
|
| + /**
|
| + * @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) {
|
| + // For TEXTAREA, handle its inner editor instead of its children.
|
| + if (isHTMLTextAreaElement(element) && element.value) {
|
| + this.handleInnerEditorOf(element);
|
| + return;
|
| + }
|
| +
|
| + element.childNodes.forEach(child => this.serializeInternal(child));
|
| + }
|
| +
|
| + /**
|
| + * @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 {!Test} testObject
|
| - * @param {!Sample} sample,
|
| - * @param {!Array<!Marker>} expectedMarkers
|
| + * @param {!Sample} sample
|
| + * @param {string} expectedText
|
| * @param {number} remainingRetry
|
| * @param {number} retryInterval
|
| */
|
| function verifyMarkers(
|
| - testObject, sample, expectedMarkers, remainingRetry, retryInterval) {
|
| + testObject, sample, expectedText, remainingRetry, retryInterval) {
|
| assert_not_equals(
|
| window.internals, undefined,
|
| 'window.internals is required for running automated spellcheck tests.');
|
|
|
| - /** @type {!Array<!Marker>} */
|
| - const actualMarkers = extractAllMarkers(sample.document);
|
| + /** @type {!MarkerSerializer} */
|
| + const serializer = new MarkerSerializer({
|
| + spelling: '_',
|
| + grammar: '~'});
|
| +
|
| try {
|
| - assert_equals(actualMarkers.length, expectedMarkers.length,
|
| - 'Number of markers mismatch.');
|
| - actualMarkers.forEach(
|
| - (marker, index) => marker.assertMatch(expectedMarkers[index]));
|
| + assert_equals(serializer.serialize(sample.document), expectedText);
|
| testObject.done();
|
| sample.remove();
|
| } catch (error) {
|
| @@ -249,7 +292,7 @@ function verifyMarkers(
|
| // know the completion of spellchecking instead of passively waiting for
|
| // markers to appear or disappear.
|
| testObject.step_timeout(
|
| - () => verifyMarkers(testObject, sample, expectedMarkers,
|
| + () => verifyMarkers(testObject, sample, expectedText,
|
| remainingRetry - 1, retryInterval),
|
| retryInterval);
|
| }
|
| @@ -271,47 +314,39 @@ const testQueue = [];
|
| /**
|
| * @param {string} inputText
|
| * @param {function(!Document)|string} tester
|
| - * @param {!Marker|!Array<!Marker>} expectedMarkers
|
| + * @param {string} expectedText
|
| * @param {string=} opt_title
|
| */
|
| -function invokeSpellcheckTest(inputText, tester, expectedMarkers, opt_title) {
|
| +function invokeSpellcheckTest(inputText, tester, expectedText, opt_title) {
|
| spellcheckTestRunning = true;
|
|
|
| - /** @type {!Test} */
|
| - const testObject = async_test(opt_title, {isSpellcheckTest: true});
|
| -
|
| - if (!(expectedMarkers instanceof Array))
|
| - expectedMarkers = [expectedMarkers]
|
| - testObject.step(() => checkExpectedMarkers(expectedMarkers));
|
| -
|
| - if (window.testRunner)
|
| - window.testRunner.setMockSpellCheckerEnabled(true);
|
| + async_test(testObject => {
|
| + // TODO(xiaochengh): Merge the following part with |assert_selection|.
|
| + /** @type {!Sample} */
|
| + const sample = new Sample(inputText);
|
| + if (typeof(tester) === 'function') {
|
| + tester.call(window, sample.document);
|
| + } else if (typeof(tester) === 'string') {
|
| + const strings = tester.split(/ (.+)/);
|
| + sample.document.execCommand(strings[0], false, strings[1]);
|
| + } else {
|
| + assert_unreached(`Invalid tester: ${tester}`);
|
| + }
|
|
|
| - // TODO(xiaochengh): Merge the following part with |assert_selection|.
|
| - /** @type {!Sample} */
|
| - const sample = new Sample(inputText);
|
| - if (typeof(tester) === 'function') {
|
| - tester.call(window, sample.document);
|
| - } else if (typeof(tester) === 'string') {
|
| - const strings = tester.split(/ (.+)/);
|
| - sample.document.execCommand(strings[0], false, strings[1]);
|
| - } else {
|
| - testObject.step(() => assert_unreached(`Invalid tester: ${tester}`));
|
| - }
|
| + /** @type {number} */
|
| + const kMaxRetry = 10;
|
| + /** @type {number} */
|
| + const kRetryInterval = 50;
|
|
|
| - /** @type {number} */
|
| - const kMaxRetry = 10;
|
| - /** @type {number} */
|
| - const kRetryInterval = 50;
|
| -
|
| - // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger
|
| - // something in JavaScript (e.g., a |Promise|), so that we can actively know
|
| - // the completion of spellchecking instead of passively waiting for markers to
|
| - // appear or disappear.
|
| - testObject.step_timeout(
|
| - () => verifyMarkers(testObject, sample, expectedMarkers,
|
| - kMaxRetry, kRetryInterval),
|
| - kRetryInterval);
|
| + // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger
|
| + // something in JavaScript (e.g., a |Promise|), so that we can actively know
|
| + // the completion of spellchecking instead of passively waiting for markers
|
| + // to appear or disappear.
|
| + testObject.step_timeout(
|
| + () => verifyMarkers(testObject, sample, expectedText,
|
| + kMaxRetry, kRetryInterval),
|
| + kRetryInterval);
|
| + }, opt_title, {isSpellcheckTest: true});
|
| }
|
|
|
| add_result_callback(testObj => {
|
| @@ -323,29 +358,29 @@ add_result_callback(testObj => {
|
| if (args === undefined)
|
| return;
|
| invokeSpellcheckTest(args.inputText, args.tester,
|
| - args.expectedMarkers, args.opt_title);
|
| + args.expectedText, args.opt_title);
|
| });
|
|
|
| /**
|
| * @param {string} inputText
|
| * @param {function(!Document)|string} tester
|
| - * @param {!Marker|!Array<!Marker>} expectedMarkers
|
| + * @param {string} expectedText
|
| * @param {string=} opt_title
|
| */
|
| -function spellcheckTest(inputText, tester, expectedMarkers, opt_title) {
|
| +function spellcheckTest(inputText, tester, expectedText, opt_title) {
|
| + if (window.testRunner)
|
| + window.testRunner.setMockSpellCheckerEnabled(true);
|
| +
|
| if (spellcheckTestRunning) {
|
| testQueue.push({
|
| inputText: inputText, tester: tester,
|
| - expectedMarkers: expectedMarkers, opt_title: opt_title});
|
| + expectedText: expectedText, opt_title: opt_title});
|
| return;
|
| }
|
|
|
| - invokeSpellcheckTest(inputText, tester, expectedMarkers, opt_title);
|
| + invokeSpellcheckTest(inputText, tester, expectedText, opt_title);
|
| }
|
|
|
| // Export symbols
|
| -window.Marker = Marker;
|
| -window.spellingMarker = spellingMarker;
|
| -window.grammarMarker = grammarMarker;
|
| window.spellcheck_test = spellcheckTest;
|
| })();
|
|
|