OLD | NEW |
---|---|
1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 'use strict'; | 5 'use strict'; |
6 | 6 |
7 // This file provides | 7 // This file provides |
8 // |spellcheck_test(sample, tester, expectedMarkers, opt_title)| asynchronous | 8 // |spellcheck_test(sample, tester, expectedText, opt_title)| asynchronous test |
9 // test to W3C test harness for easier writing of editing test cases. | 9 // to W3C test harness for easier writing of spellchecker test cases. |
10 // | 10 // |
11 // |sample| is an HTML fragment text which is inserted as |innerHTML|. It should | 11 // |sample| is an HTML fragment text which is inserted as |innerHTML|. It should |
12 // have at least one focus boundary point marker "|" and at most one anchor | 12 // have at least one focus boundary point marker "|" and at most one anchor |
13 // boundary point marker "^" indicating the initial selection. | 13 // boundary point marker "^" indicating the initial selection. |
14 // TODO(editing-dev): Make initial selection work with TEXTAREA and INPUT. | |
14 // | 15 // |
15 // |tester| is either name with parameter of execCommand or function taking | 16 // |tester| is either name with parameter of execCommand or function taking |
16 // one parameter |Document|. | 17 // one parameter |Document|. |
17 // | 18 // |
18 // |expectedMarkers| is either a |Marker| or a |Marker| array, where each | 19 // |expectedText| is an HTML fragment where characters with spelling and/or |
19 // |Marker| is an |Object| with the following properties: | 20 // grammar markers under them are replaced by special symbols. Specifically: |
20 // - |location| and |length| are integers indicating the range of the marked | 21 // - Characters with only spelling markers are replaced by '_'; |
21 // text. It must hold that |location >= 0| and |length > 0|. | 22 // - Characters with only grammar markers are replaced by '~'; |
22 // - |type| is an optional string indicating the marker type. When present, it | 23 // - Characters with both spelling and grammar markers are replaced by '#'. |
23 // must be equal to either "spelling" or "grammer". When absent, it is | |
24 // regarded as "spelling". | |
25 // - |description| is an optional string indicating the description of a marker. | |
26 // | 24 // |
27 // |opt_title| is an optional string giving the title of the test case. | 25 // |opt_title| is an optional string giving the title of the test case. |
28 // | 26 // |
29 // Examples: | 27 // See spellcheck_test.html for sample usage. |
30 // | |
31 // spellcheck_test( | |
32 // '<div contentEditable>|</div>', | |
33 // 'insertText wellcome.', | |
34 // spellingMarker(0, 8, 'welcome'), // 'wellcome' | |
35 // 'Mark misspellings and give replacement suggestions after typing.'); | |
36 // | |
37 // spellcheck_test( | |
38 // '<div contentEditable>|</div>', | |
39 // 'insertText You has the right.', | |
40 // grammarMarker(4, 3), // 'has' | |
41 // 'Mark ungrammatical phrases after typing.'); | |
42 | 28 |
43 (function() { | 29 (function() { |
44 const Sample = window.Sample; | 30 const Sample = window.Sample; |
45 | 31 |
32 // TODO(editing-dev): Once we can import JavaScript file from scripts, we should | |
33 // import "imported/wpt/html/resources/common.js", since |HTML5_VOID_ELEMENTS| | |
34 // is defined in there. | |
35 /** | |
36 * @const @type {!Set<string>} | |
37 * only void (without end tag) HTML5 elements | |
38 */ | |
39 const HTML5_VOID_ELEMENTS = new Set([ | |
40 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', | |
41 'keygen', 'link', 'meta', 'param', 'source','track', 'wbr' ]); | |
42 | |
43 // TODO(editing-dev): Reduce code duplication with assert_selection's Serializer | |
44 // once we can import and export Javascript modules. | |
45 | |
46 /** | |
47 * @param {!Node} node | |
48 * @return {boolean} | |
49 */ | |
50 function isCharacterData(node) { | |
51 return node.nodeType === Node.TEXT_NODE || | |
52 node.nodeType === Node.COMMENT_NODE; | |
53 } | |
54 | |
55 /** | |
56 * @param {!Node} node | |
57 * @return {boolean} | |
58 */ | |
59 function isElement(node) { | |
60 return node.nodeType === Node.ELEMENT_NODE; | |
61 } | |
62 | |
63 /** | |
64 * @param {!Node} node | |
65 * @return {boolean} | |
66 */ | |
67 function isHTMLInputElement(node) { | |
68 if (!isElement(node)) | |
69 return false; | |
70 const window = node.ownerDocument.defaultView; | |
71 return node instanceof window.HTMLInputElement; | |
72 } | |
73 | |
74 /** | |
75 * @param {!Node} node | |
76 * @return {boolean} | |
77 */ | |
78 function isHTMLTextAreaElement(node) { | |
79 if (!isElement(node)) | |
80 return false; | |
81 const window = node.ownerDocument.defaultView; | |
82 return node instanceof window.HTMLTextAreaElement; | |
83 } | |
84 | |
85 /** | |
86 * @param {?Range} range | |
87 * @param {!Node} node | |
88 * @param {number} offset | |
89 */ | |
90 function isAtRangeEnd(range, node, offset) { | |
91 return range && node === range.endContainer && offset === range.endOffset; | |
92 } | |
93 | |
94 class MarkerSerializer { | |
95 /** | |
96 * @public | |
97 * @param {!Array<string>} markerTypes | |
98 * @param {string} replacementSymbols | |
99 */ | |
100 constructor(markerTypes, replacementSymbols) { | |
101 assert_equals(replacementSymbols.length + 1, 1 << markerTypes.length, | |
102 'Must have one replacement symbol for each non-empty marker ' + | |
103 'combination.'); | |
104 /** @type {!Array<string>} */ | |
105 this.strings_ = []; | |
106 /** @type {!Array<string>} */ | |
107 this.markerTypes_ = markerTypes; | |
108 /** @type {string} */ | |
109 this.replacementSymbols_ = replacementSymbols; | |
110 /** @type {!Object} */ | |
111 this.activeMarkerRanges_ = {}; | |
112 markerTypes.forEach(type => this.activeMarkerRanges_[type] = null); | |
113 } | |
114 | |
115 /** | |
116 * @private | |
117 * @return {number} | |
118 */ | |
119 get activeMarkerTypes() { | |
120 return this.markerTypes_.reduce( | |
121 (result, type, index) => | |
122 this.activeMarkerRanges_[type] ? (result | (1 << index)) : result, | |
123 0); | |
124 } | |
125 | |
126 /** | |
127 * @private | |
128 * @param {string} string | |
129 */ | |
130 emit(string) { this.strings_.push(string); } | |
131 | |
132 /** | |
133 * @private | |
134 * @param {!Node} node | |
135 * @param {number} offset | |
136 */ | |
137 advancedTo(node, offset) { | |
138 this.markerTypes_.forEach(type => { | |
139 if (isAtRangeEnd(this.activeMarkerRanges_[type], node, offset)) | |
140 this.activeMarkerRanges_[type] = null; | |
141 if (this.activeMarkerRanges_[type]) | |
142 return; | |
143 const markerCount = window.internals.markerCountForNode(node, type); | |
144 for (let i = 0; i < markerCount; ++i) { | |
145 const marker = window.internals.markerRangeForNode(node, type, i); | |
146 assert_equals( | |
147 marker.startContainer, node, | |
148 'Internal error: marker range not starting in the annotated node.'); | |
149 assert_equals( | |
150 marker.endContainer, node, | |
151 'Internal error: marker range not ending in the annotated node.'); | |
152 if (marker.startOffset == offset) { | |
153 assert_greater_than(marker.endOffset, offset, | |
154 'Internal error: marker range is collapsed.'); | |
155 this.activeMarkerRanges_[type] = marker; | |
156 break; | |
157 } | |
158 } | |
159 }); | |
160 } | |
161 | |
162 /** | |
163 * @private | |
164 * @param {!CharacterData} node | |
165 */ | |
166 handleCharacterData(node) { | |
167 /** @type {string} */ | |
168 const text = node.nodeValue; | |
169 /** @type {number} */ | |
170 const length = text.length; | |
171 for (let offset = 0; offset < length; ++offset) { | |
172 this.advancedTo(node, offset); | |
173 /** @type {number} */ | |
174 const activeTypes = this.activeMarkerTypes; | |
175 /** @type {string} */ | |
176 const charToEmit = activeTypes | |
177 ? this.replacementSymbols_[activeTypes - 1] | |
178 : text[offset]; | |
179 this.emit(charToEmit); | |
180 } | |
181 this.advancedTo(node, length); | |
182 } | |
183 | |
184 /** | |
185 * @private | |
186 * @param {!HTMLElement} element | |
187 */ | |
188 handleInnerEditorOf(element) { | |
189 /** @type {!ShadowRoot} */ | |
190 const shadowRoot = window.internals.shadowRoot(element); | |
191 assert_not_equals( | |
192 shadowRoot, undefined, | |
193 'Internal error: text form control element not having shadow tree as ' + | |
194 'inner editor.'); | |
195 /** @type {!HTMLDivElement} */ | |
196 const innerEditor = shadowRoot.firstChild; | |
197 assert_equals(innerEditor.tagName, 'DIV', | |
198 'Internal error: inner editor is not DIV'); | |
199 innerEditor.childNodes.forEach((child, index) => { | |
200 assert_true(isCharacterData(child), | |
201 'Internal error: inner editor having child node that is ' + | |
202 'not CharacterData.'); | |
203 this.advancedTo(innerEditor, index); | |
204 this.handleCharacterData(child); | |
205 }); | |
206 this.advancedTo(innerEditor, innerEditor.childNodes.length); | |
207 } | |
208 | |
209 /** | |
210 * @private | |
211 * @param {!HTMLTextAreaElement} element | |
212 */ | |
213 handleTextAreaNode(element) { | |
214 this.handleInnerEditorOf(element); | |
215 } | |
216 | |
217 /** | |
218 * @private | |
219 * @param {!HTMLInputElement} element | |
220 */ | |
221 handleInputNode(element) { | |
222 this.emit(' value="'); | |
223 this.handleInnerEditorOf(element); | |
224 this.emit('"'); | |
225 } | |
226 | |
227 /** | |
228 * @private | |
229 * @param {!HTMLElement} element | |
230 */ | |
231 handleElementNode(element) { | |
232 /** @type {string} */ | |
233 const tagName = element.tagName.toLowerCase(); | |
234 this.emit(`<${tagName}`); | |
235 Array.from(element.attributes) | |
236 .sort((attr1, attr2) => attr1.name.localeCompare(attr2.name)) | |
237 .forEach(attr => { | |
238 if (attr.value === '') | |
239 return this.emit(` ${attr.name}`); | |
240 const value = attr.value.replace(/&/g, '&') | |
241 .replace(/\u0022/g, '"') | |
242 .replace(/\u0027/g, '''); | |
243 this.emit(` ${attr.name}="${value}"`); | |
244 }); | |
245 if (isHTMLInputElement(element) && element.value) | |
246 this.handleInputNode(element); | |
247 this.emit('>'); | |
248 | |
249 if (isHTMLTextAreaElement(element) && element.value) | |
250 this.handleTextAreaNode(element); | |
251 | |
252 if (element.childNodes.length === 0 && | |
253 HTML5_VOID_ELEMENTS.has(tagName)) { | |
254 return; | |
255 } | |
256 this.serializeChildren(element); | |
Xiaocheng
2016/10/25 13:19:17
This is a bug that if a TEXTAREA has non-empty chi
| |
257 this.emit(`</${tagName}>`); | |
258 } | |
259 | |
260 /** | |
261 * @public | |
262 * @param {!HTMLDocument} document | |
263 */ | |
264 serialize(document) { | |
265 if (document.body) | |
266 this.serializeChildren(document.body); | |
267 else | |
268 this.serializeInternal(document.documentElement); | |
269 return this.strings_.join(''); | |
270 } | |
271 | |
272 /** | |
273 * @private | |
274 * @param {!HTMLElement} element | |
275 */ | |
276 serializeChildren(element) { | |
277 /** @type {!Array<!Node>} */ | |
278 const childNodes = Array.from(element.childNodes); | |
279 if (childNodes.length === 0) { | |
280 this.advancedTo(element, 0); | |
281 return; | |
282 } | |
283 | |
284 /** @type {number} */ | |
285 let childIndex = 0; | |
286 for (const child of childNodes) { | |
287 this.advancedTo(element, childIndex); | |
288 this.serializeInternal(child, childIndex); | |
289 ++childIndex; | |
290 } | |
291 this.advancedTo(element, childIndex); | |
292 } | |
293 | |
294 /** | |
295 * @private | |
296 * @param {!Node} node | |
297 */ | |
298 serializeInternal(node) { | |
299 if (isElement(node)) | |
300 return this.handleElementNode(node); | |
301 if (isCharacterData(node)) | |
302 return this.handleCharacterData(node); | |
303 throw new Error(`Unexpected node ${node}`); | |
304 } | |
305 } | |
306 | |
46 /** @type {string} */ | 307 /** @type {string} */ |
47 const kSpelling = 'spelling'; | 308 const kSpelling = 'spelling'; |
48 /** @type {string} */ | 309 /** @type {string} */ |
49 const kGrammar = 'grammar'; | 310 const kGrammar = 'grammar'; |
50 | 311 /** @type {string} */ |
51 class Marker { | 312 const kSpellingMarker = '_'; |
52 /** | 313 /** @type {string} */ |
53 * @public | 314 const kGrammarMarker = '~'; |
54 * @param {number} location | 315 /** @type {string} */ |
55 * @param {number} length | 316 const kBothMarkers = '#'; |
56 * @param {string=} opt_type | |
57 * @param {string=} opt_description | |
58 */ | |
59 constructor(location, length, opt_type, opt_description) { | |
60 /** @type {number} */ | |
61 this.location_ = location; | |
62 /** @type {number} */ | |
63 this.length_ = length; | |
64 /** @type {string} */ | |
65 this.type_ = opt_type || 'spelling'; | |
66 /** @type {boolean} */ | |
67 this.ignoreDescription_ = opt_description === undefined; | |
68 /** @type {string} */ | |
69 this.description_ = opt_description || ''; | |
70 } | |
71 | |
72 /** @return {number} */ | |
73 get location() { return this.location_; } | |
74 | |
75 /** @return {number} */ | |
76 get length() { return this.length_; } | |
77 | |
78 /** @return {string} */ | |
79 get type() { return this.type_; } | |
80 | |
81 /** @return {boolean} */ | |
82 get ignoreDescription() { return this.ignoreDescription_; } | |
83 | |
84 /** @return {string} */ | |
85 get description() { return this.description_; } | |
86 | |
87 /** | |
88 * @public | |
89 */ | |
90 assertValid() { | |
91 // TODO(xiaochengh): Add proper assert descriptions when needed. | |
92 assert_true(Number.isInteger(this.location_)); | |
93 assert_greater_than_equal(this.location_, 0); | |
94 assert_true(Number.isInteger(this.length_)); | |
95 assert_greater_than(this.length_, 0); | |
96 assert_true(this.type_ === kSpelling || this.type_ === kGrammar); | |
97 assert_true(typeof this.description_ === 'string'); | |
98 } | |
99 | |
100 /** | |
101 * @public | |
102 * @param {!Marker} expected | |
103 */ | |
104 assertMatch(expected) { | |
105 try { | |
106 assert_equals(this.location, expected.location); | |
107 assert_equals(this.length, expected.length); | |
108 assert_equals(this.type, expected.type); | |
109 if (expected.ignoreDescription) | |
110 return; | |
111 assert_equals(this.description, expected.description); | |
112 } catch (error) { | |
113 throw new Error(`Expected ${expected} but got ${this}.`); | |
114 } | |
115 } | |
116 | |
117 /** @override */ | |
118 toString() { | |
119 return `${this.type_} marker at ` + | |
120 `[${this.location_}, ${this.location_ + this.length_}]` + | |
121 (this.description_ ? ` with description "${this.description_}"` : ``); | |
122 } | |
123 } | |
124 | |
125 /** | |
126 * @param {number} location | |
127 * @param {number} length | |
128 * @param {string=} opt_description | |
129 * @return {!Marker} | |
130 */ | |
131 function spellingMarker(location, length, opt_description) { | |
132 return new Marker(location, length, kSpelling, opt_description); | |
133 } | |
134 | |
135 /** | |
136 * @param {number} location | |
137 * @param {number} length | |
138 * @param {string=} opt_description | |
139 * @return {!Marker} | |
140 */ | |
141 function grammarMarker(location, length, opt_description) { | |
142 return new Marker(location, length, kGrammar, opt_description); | |
143 } | |
144 | |
145 /** | |
146 * @param {!Marker} marker1 | |
147 * @param {!Marker} marker2 | |
148 * @return {number} | |
149 */ | |
150 function markerComparison(marker1, marker2) { | |
151 return marker1.location - marker2.location; | |
152 } | |
153 | |
154 /** | |
155 * @param {!Array<!Marker>} expectedMarkers | |
156 */ | |
157 function checkExpectedMarkers(expectedMarkers) { | |
158 if (expectedMarkers.length === 0) | |
159 return; | |
160 expectedMarkers.forEach(marker => marker.assertValid()); | |
161 expectedMarkers.sort(markerComparison); | |
162 expectedMarkers.reduce((lastMarker, currentMarker) => { | |
163 assert_less_than( | |
164 lastMarker.location + lastMarker.length, currentMarker.location, | |
165 'Marker ranges should be disjoint.'); | |
166 return currentMarker; | |
167 }); | |
168 } | |
169 | |
170 /** | |
171 * @param {!Node} node | |
172 * @param {string} type | |
173 * @param {!Array<!Marker>} markers | |
174 */ | |
175 function extractMarkersOfType(node, type, markers) { | |
176 /** @type {!HTMLBodyElement} */ | |
177 const body = node.ownerDocument.body; | |
178 /** @type {number} */ | |
179 const markerCount = window.internals.markerCountForNode(node, type); | |
180 for (let i = 0; i < markerCount; ++i) { | |
181 /** @type {!Range} */ | |
182 const markerRange = window.internals.markerRangeForNode(node, type, i); | |
183 /** @type {string} */ | |
184 const description = window.internals.markerDescriptionForNode(node, type, i) ; | |
185 /** @type {number} */ | |
186 const location = window.internals.locationFromRange(body, markerRange); | |
187 /** @type {number} */ | |
188 const length = window.internals.lengthFromRange(body, markerRange); | |
189 | |
190 markers.push(new Marker(location, length, type, description)); | |
191 } | |
192 } | |
193 | |
194 /** | |
195 * @param {!Node} node | |
196 * @param {!Array<!Marker>} markers | |
197 */ | |
198 function extractAllMarkersRecursivelyTo(node, markers) { | |
199 extractMarkersOfType(node, kSpelling, markers); | |
200 extractMarkersOfType(node, kGrammar, markers); | |
201 node.childNodes.forEach( | |
202 child => extractAllMarkersRecursivelyTo(child, markers)); | |
203 } | |
204 | |
205 /** | |
206 * @param {!Document} doc | |
207 * @return {!Array<!Marker>} | |
208 */ | |
209 function extractAllMarkers(doc) { | |
210 /** @type {!Array<!Marker>} */ | |
211 const markers = []; | |
212 extractAllMarkersRecursivelyTo(doc.body, markers); | |
213 markers.sort(markerComparison); | |
214 return markers; | |
215 } | |
216 | 317 |
217 /** | 318 /** |
218 * @param {!Test} testObject | 319 * @param {!Test} testObject |
219 * @param {!Sample} sample, | 320 * @param {!Sample} sample |
220 * @param {!Array<!Marker>} expectedMarkers | 321 * @param {string} expectedText |
221 * @param {number} remainingRetry | 322 * @param {number} remainingRetry |
222 * @param {number} retryInterval | 323 * @param {number} retryInterval |
223 */ | 324 */ |
224 function verifyMarkers( | 325 function verifyMarkers( |
225 testObject, sample, expectedMarkers, remainingRetry, retryInterval) { | 326 testObject, sample, expectedText, remainingRetry, retryInterval) { |
226 assert_not_equals( | 327 assert_not_equals( |
227 window.internals, undefined, | 328 window.internals, undefined, |
228 'window.internals is required for running automated spellcheck tests.'); | 329 'window.internals is required for running automated spellcheck tests.'); |
229 | 330 |
230 /** @type {!Array<!Marker>} */ | 331 /** @type {!MarkerSerializer} */ |
231 const actualMarkers = extractAllMarkers(sample.document); | 332 const serializer = new MarkerSerializer( |
333 [kSpelling, kGrammar], | |
334 [kSpellingMarker, kGrammarMarker, kBothMarkers].join('')); | |
335 | |
232 try { | 336 try { |
233 assert_equals(actualMarkers.length, expectedMarkers.length, | 337 assert_equals(serializer.serialize(sample.document), expectedText); |
234 'Number of markers mismatch.'); | |
235 actualMarkers.forEach( | |
236 (marker, index) => marker.assertMatch(expectedMarkers[index])); | |
237 testObject.done(); | 338 testObject.done(); |
238 sample.remove(); | 339 sample.remove(); |
239 } catch (error) { | 340 } catch (error) { |
240 if (remainingRetry <= 0) | 341 if (remainingRetry <= 0) |
241 throw error; | 342 throw error; |
242 | 343 |
243 // Force invoking idle time spellchecker in case it has not been run yet. | 344 // Force invoking idle time spellchecker in case it has not been run yet. |
244 if (window.testRunner) | 345 if (window.testRunner) |
245 window.testRunner.runIdleTasks(() => {}); | 346 window.testRunner.runIdleTasks(() => {}); |
246 | 347 |
247 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger | 348 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger |
248 // something in JavaScript (e.g., a |Promise|), so that we can actively | 349 // something in JavaScript (e.g., a |Promise|), so that we can actively |
249 // know the completion of spellchecking instead of passively waiting for | 350 // know the completion of spellchecking instead of passively waiting for |
250 // markers to appear or disappear. | 351 // markers to appear or disappear. |
251 testObject.step_timeout( | 352 testObject.step_timeout( |
252 () => verifyMarkers(testObject, sample, expectedMarkers, | 353 () => verifyMarkers(testObject, sample, expectedText, |
253 remainingRetry - 1, retryInterval), | 354 remainingRetry - 1, retryInterval), |
254 retryInterval); | 355 retryInterval); |
255 } | 356 } |
256 } | 357 } |
257 | 358 |
258 // Spellchecker gets triggered not only by text and selection change, but also | 359 // Spellchecker gets triggered not only by text and selection change, but also |
259 // by focus change. For example, misspelling markers in <INPUT> disappear when | 360 // by focus change. For example, misspelling markers in <INPUT> disappear when |
260 // the window loses focus, even though the selection does not change. | 361 // the window loses focus, even though the selection does not change. |
261 // Therefore, we disallow spellcheck tests from running simultaneously to | 362 // Therefore, we disallow spellcheck tests from running simultaneously to |
262 // prevent interference among them. If we call spellcheck_test while another | 363 // prevent interference among them. If we call spellcheck_test while another |
263 // test is running, the new test will be added into testQueue waiting for the | 364 // test is running, the new test will be added into testQueue waiting for the |
264 // completion of the previous test. | 365 // completion of the previous test. |
265 | 366 |
266 /** @type {boolean} */ | 367 /** @type {boolean} */ |
267 var spellcheckTestRunning = false; | 368 var spellcheckTestRunning = false; |
268 /** @type {!Array<!Object>} */ | 369 /** @type {!Array<!Object>} */ |
269 const testQueue = []; | 370 const testQueue = []; |
270 | 371 |
271 /** | 372 /** |
272 * @param {string} inputText | 373 * @param {string} inputText |
273 * @param {function(!Document)|string} tester | 374 * @param {function(!Document)|string} tester |
274 * @param {!Marker|!Array<!Marker>} expectedMarkers | 375 * @param {string} expectedText |
275 * @param {string=} opt_title | 376 * @param {string=} opt_title |
276 */ | 377 */ |
277 function invokeSpellcheckTest(inputText, tester, expectedMarkers, opt_title) { | 378 function invokeSpellcheckTest(inputText, tester, expectedText, opt_title) { |
278 spellcheckTestRunning = true; | 379 spellcheckTestRunning = true; |
279 | 380 |
280 /** @type {!Test} */ | 381 async_test(testObject => { |
281 const testObject = async_test(opt_title, {isSpellcheckTest: true}); | 382 // TODO(xiaochengh): Merge the following part with |assert_selection|. |
383 /** @type {!Sample} */ | |
384 const sample = new Sample(inputText); | |
385 if (typeof(tester) === 'function') { | |
386 tester.call(window, sample.document); | |
387 } else if (typeof(tester) === 'string') { | |
388 const strings = tester.split(/ (.+)/); | |
389 sample.document.execCommand(strings[0], false, strings[1]); | |
390 } else { | |
391 assert_unreached(`Invalid tester: ${tester}`); | |
392 } | |
282 | 393 |
283 if (!(expectedMarkers instanceof Array)) | 394 /** @type {number} */ |
284 expectedMarkers = [expectedMarkers] | 395 const kMaxRetry = 10; |
285 testObject.step(() => checkExpectedMarkers(expectedMarkers)); | 396 /** @type {number} */ |
397 const kRetryInterval = 50; | |
286 | 398 |
287 if (window.testRunner) | 399 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger |
288 window.testRunner.setMockSpellCheckerEnabled(true); | 400 // something in JavaScript (e.g., a |Promise|), so that we can actively know |
289 | 401 // the completion of spellchecking instead of passively waiting for markers |
290 // TODO(xiaochengh): Merge the following part with |assert_selection|. | 402 // to appear or disappear. |
291 /** @type {!Sample} */ | 403 testObject.step_timeout( |
292 const sample = new Sample(inputText); | 404 () => verifyMarkers(testObject, sample, expectedText, |
293 if (typeof(tester) === 'function') { | 405 kMaxRetry, kRetryInterval), |
294 tester.call(window, sample.document); | 406 kRetryInterval); |
295 } else if (typeof(tester) === 'string') { | 407 }, opt_title, {isSpellcheckTest: true}); |
296 const strings = tester.split(/ (.+)/); | |
297 sample.document.execCommand(strings[0], false, strings[1]); | |
298 } else { | |
299 testObject.step(() => assert_unreached(`Invalid tester: ${tester}`)); | |
300 } | |
301 | |
302 /** @type {number} */ | |
303 const kMaxRetry = 10; | |
304 /** @type {number} */ | |
305 const kRetryInterval = 50; | |
306 | |
307 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger | |
308 // something in JavaScript (e.g., a |Promise|), so that we can actively know | |
309 // the completion of spellchecking instead of passively waiting for markers to | |
310 // appear or disappear. | |
311 testObject.step_timeout( | |
312 () => verifyMarkers(testObject, sample, expectedMarkers, | |
313 kMaxRetry, kRetryInterval), | |
314 kRetryInterval); | |
315 } | 408 } |
316 | 409 |
317 add_result_callback(testObj => { | 410 add_result_callback(testObj => { |
318 if (!testObj.properties.isSpellcheckTest) | 411 if (!testObj.properties.isSpellcheckTest) |
319 return; | 412 return; |
320 spellcheckTestRunning = false; | 413 spellcheckTestRunning = false; |
321 /** @type {Object} */ | 414 /** @type {Object} */ |
322 const args = testQueue.shift(); | 415 const args = testQueue.shift(); |
323 if (args === undefined) | 416 if (args === undefined) |
324 return; | 417 return; |
325 invokeSpellcheckTest(args.inputText, args.tester, | 418 invokeSpellcheckTest(args.inputText, args.tester, |
326 args.expectedMarkers, args.opt_title); | 419 args.expectedText, args.opt_title); |
327 }); | 420 }); |
328 | 421 |
329 /** | 422 /** |
330 * @param {string} inputText | 423 * @param {string} inputText |
331 * @param {function(!Document)|string} tester | 424 * @param {function(!Document)|string} tester |
332 * @param {!Marker|!Array<!Marker>} expectedMarkers | 425 * @param {string} expectedText |
333 * @param {string=} opt_title | 426 * @param {string=} opt_title |
334 */ | 427 */ |
335 function spellcheckTest(inputText, tester, expectedMarkers, opt_title) { | 428 function spellcheckTest(inputText, tester, expectedText, opt_title) { |
429 if (window.testRunner) | |
430 window.testRunner.setMockSpellCheckerEnabled(true); | |
431 | |
336 if (spellcheckTestRunning) { | 432 if (spellcheckTestRunning) { |
337 testQueue.push({ | 433 testQueue.push({ |
338 inputText: inputText, tester: tester, | 434 inputText: inputText, tester: tester, |
339 expectedMarkers: expectedMarkers, opt_title: opt_title}); | 435 expectedText: expectedText, opt_title: opt_title}); |
340 return; | 436 return; |
341 } | 437 } |
342 | 438 |
343 invokeSpellcheckTest(inputText, tester, expectedMarkers, opt_title); | 439 invokeSpellcheckTest(inputText, tester, expectedText, opt_title); |
344 } | 440 } |
345 | 441 |
346 // Export symbols | 442 // Export symbols |
347 window.Marker = Marker; | |
348 window.spellingMarker = spellingMarker; | |
349 window.grammarMarker = grammarMarker; | |
350 window.spellcheck_test = spellcheckTest; | 443 window.spellcheck_test = spellcheckTest; |
351 })(); | 444 })(); |
OLD | NEW |