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, expectedMarkers, opt_title)| asynchronous |
9 // test to W3C test harness for easier writing of editing test cases. | 9 // test to W3C test harness for easier writing of editing test cases. |
10 // | 10 // |
(...skipping 25 matching lines...) Expand all Loading... | |
36 // | 36 // |
37 // spellcheck_test( | 37 // spellcheck_test( |
38 // '<div contentEditable>|</div>', | 38 // '<div contentEditable>|</div>', |
39 // 'insertText You has the right.', | 39 // 'insertText You has the right.', |
40 // grammarMarker(4, 3), // 'has' | 40 // grammarMarker(4, 3), // 'has' |
41 // 'Mark ungrammatical phrases after typing.'); | 41 // 'Mark ungrammatical phrases after typing.'); |
42 | 42 |
43 (function() { | 43 (function() { |
44 const Sample = window.Sample; | 44 const Sample = window.Sample; |
45 | 45 |
46 // TODO(editing-dev): Once we can import JavaScript file from scripts, we should | |
47 // import "imported/wpt/html/resources/common.js", since |HTML5_VOID_ELEMENTS| | |
48 // is defined in there. | |
49 /** | |
50 * @const @type {!Set<string>} | |
51 * only void (without end tag) HTML5 elements | |
52 */ | |
53 const HTML5_VOID_ELEMENTS = new Set([ | |
54 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', | |
55 'keygen', 'link', 'meta', 'param', 'source','track', 'wbr' ]); | |
56 | |
57 // TODO(editing-dev): Reduce code duplication with assert_selection's Serializer | |
58 // once we can import and export Javascript modules. | |
59 | |
60 /** | |
61 * @param {!Node} node | |
62 * @return {boolean} | |
63 */ | |
64 function isCharacterData(node) { | |
65 return node.nodeType === Node.TEXT_NODE || | |
66 node.nodeType === Node.COMMENT_NODE; | |
67 } | |
68 | |
69 /** | |
70 * @param {!Node} node | |
71 * @return {boolean} | |
72 */ | |
73 function isElement(node) { | |
74 return node.nodeType === Node.ELEMENT_NODE; | |
75 } | |
76 | |
77 /** | |
78 * @param {!Node} node | |
79 * @return {boolean} | |
80 */ | |
81 function isHTMLInputElement(node) { | |
82 if (!isElement(node)) | |
yosin_UTC9
2016/10/25 09:41:11
|node.nodeName === 'INPUT'| is enough.
Xiaocheng
2016/10/25 13:19:17
Done.
| |
83 return false; | |
84 const window = node.ownerDocument.defaultView; | |
85 return node instanceof window.HTMLInputElement; | |
86 } | |
87 | |
88 /** | |
89 * @param {!Node} node | |
90 * @return {boolean} | |
91 */ | |
92 function isHTMLTextAreaElement(node) { | |
93 if (!isElement(node)) | |
yosin_UTC9
2016/10/25 09:41:11
|node.nodeName === 'TEXTAREA'| is enough.
Xiaocheng
2016/10/25 13:19:17
Done.
| |
94 return false; | |
95 const window = node.ownerDocument.defaultView; | |
96 return node instanceof window.HTMLTextAreaElement; | |
97 } | |
98 | |
99 /** | |
100 * @param {?Range} range | |
101 * @param {!Node} node | |
102 * @param {number} offset | |
103 */ | |
104 function isAtRangeEnd(range, node, offset) { | |
105 return range && node === range.endContainer && offset === range.endOffset; | |
106 } | |
107 | |
108 class MarkerSerializer { | |
109 /** | |
110 * @public | |
111 * @param {!Array<string>} markerTypes | |
112 * @param {string} markerMasks | |
113 */ | |
114 constructor(markerTypes, markerMasks) { | |
115 assert_equals(markerMasks.length + 1, 1 << markerTypes.length, | |
116 'Maker types and masks do not match each other.'); | |
117 /** @type {!Array<string>} */ | |
118 this.strings_ = []; | |
119 /** @type {!Array<string>} */ | |
120 this.markerTypes_ = markerTypes; | |
121 /** @type {string} */ | |
122 this.markerMasks_ = markerMasks; | |
123 /** @type {!Object} */ | |
124 this.activeMarkerRanges_ = {}; | |
125 markerTypes.forEach(type => this.activeMarkerRanges_[type] = null); | |
126 } | |
127 | |
128 /** | |
129 * @private | |
130 * @return {number} | |
131 */ | |
132 get activeMarkerTypes() { | |
yosin_UTC9
2016/10/25 09:41:11
This should be a method instead of a getter, since
Xiaocheng
2016/10/25 13:19:17
Done.
| |
133 return this.markerTypes_.reduce( | |
134 (result, type, index) => | |
135 this.activeMarkerRanges_[type] ? (result | (1 << index)) : result, | |
136 0); | |
137 } | |
138 | |
139 /** | |
140 * @private | |
141 * @param {string} string | |
142 */ | |
143 emit(string) { this.strings_.push(string); } | |
144 | |
145 /** | |
146 * @private | |
147 * @param {!Node} node | |
148 * @param {number} offset | |
149 */ | |
150 advancedTo(node, offset) { | |
151 this.markerTypes_.forEach(type => { | |
152 if (isAtRangeEnd(this.activeMarkerRanges_[type], node, offset)) | |
153 this.activeMarkerRanges_[type] = null; | |
154 if (this.activeMarkerRanges_[type]) | |
155 return; | |
156 const markerCount = window.internals.markerCountForNode(node, type); | |
157 for (let i = 0; i < markerCount; ++i) { | |
158 const marker = window.internals.markerRangeForNode(node, type, i); | |
159 assert_equals(marker.startContainer, node, | |
yosin_UTC9
2016/10/25 09:41:10
We don't need to verity marker. It should work as
Xiaocheng
2016/10/25 13:19:17
I prefer keeping them in case anything goes wrong.
| |
160 'Marker range does not start in the annotated node.'); | |
161 assert_equals(marker.endContainer, node, | |
162 'Marker range does not end in the annotated node.'); | |
163 if (marker.startOffset == offset) { | |
yosin_UTC9
2016/10/25 09:41:11
nit: s/==/===/
Xiaocheng
2016/10/25 13:19:16
Done.
| |
164 assert_greater_than(marker.endOffset, offset, | |
165 'Marker range is collapsed.'); | |
166 this.activeMarkerRanges_[type] = marker; | |
167 break; | |
168 } | |
169 } | |
170 }); | |
171 } | |
172 | |
173 /** | |
174 * @private | |
175 * @param {!CharacterData} node | |
176 */ | |
177 handleCharacterData(node) { | |
178 /** @type {string} */ | |
179 const text = node.nodeValue; | |
180 /** @type {number} */ | |
181 const length = text.length; | |
182 for (let offset = 0; offset < length; ++offset) { | |
183 this.advancedTo(node, offset); | |
184 /** @type {number} */ | |
185 const activeTypes = this.activeMarkerTypes; | |
186 /** @type {string} */ | |
187 const charToEmit = activeTypes ? this.markerMasks_[activeTypes - 1] : text [offset]; | |
188 this.emit(charToEmit); | |
189 } | |
190 this.advancedTo(node, length); | |
191 } | |
192 | |
193 /** | |
194 * @private | |
195 * @param {!HTMLElement} element | |
196 */ | |
197 handleInnerEditorOf(element) { | |
198 /** @type {!ShadowRoot} */ | |
199 const shadowRoot = window.internals.shadowRoot(element); | |
200 /** @type {!HTMLDivElement} */ | |
201 const innerEditor = shadowRoot.firstChild; | |
202 assert_equals(innerEditor.tagName, 'DIV'); | |
203 innerEditor.childNodes.forEach((child, index) => { | |
204 assert_true(isCharacterData(child)); | |
205 this.advancedTo(innerEditor, index); | |
206 this.handleCharacterData(child); | |
207 }); | |
208 this.advancedTo(innerEditor, innerEditor.childNodes.length); | |
209 } | |
210 | |
211 /** | |
212 * @private | |
213 * @param {!HTMLTextAreaElement} element | |
214 */ | |
215 handleTextAreaNode(element) { | |
216 this.handleInnerEditorOf(element); | |
217 } | |
218 | |
219 /** | |
220 * @private | |
221 * @param {!HTMLInputElement} element | |
222 */ | |
223 handleInputNode(element) { | |
224 this.emit(' value="'); | |
225 this.handleInnerEditorOf(element); | |
226 this.emit('"'); | |
227 } | |
228 | |
229 /** | |
230 * @private | |
231 * @param {!HTMLElement} element | |
232 */ | |
233 handleElementNode(element) { | |
234 /** @type {string} */ | |
235 const tagName = element.tagName.toLowerCase(); | |
236 this.emit(`<${tagName}`); | |
237 Array.from(element.attributes) | |
238 .sort((attr1, attr2) => attr1.name.localeCompare(attr2.name)) | |
239 .forEach(attr => { | |
240 if (attr.value === '') | |
241 return this.emit(` ${attr.name}`); | |
242 const value = attr.value.replace(/&/g, '&') | |
243 .replace(/\u0022/g, '"') | |
244 .replace(/\u0027/g, '''); | |
245 this.emit(` ${attr.name}="${value}"`); | |
246 }); | |
247 if (isHTMLInputElement(element) && element.value) | |
248 this.handleInputNode(element); | |
249 this.emit('>'); | |
250 | |
251 if (isHTMLTextAreaElement(element) && element.value) | |
252 this.handleTextAreaNode(element); | |
253 | |
254 if (element.childNodes.length === 0 && | |
255 HTML5_VOID_ELEMENTS.has(tagName)) { | |
256 return; | |
257 } | |
258 this.serializeChildren(element); | |
259 this.emit(`</${tagName}>`); | |
260 } | |
261 | |
262 /** | |
263 * @public | |
264 * @param {!HTMLDocument} document | |
265 */ | |
266 serialize(document) { | |
267 if (document.body) | |
268 this.serializeChildren(document.body); | |
269 else | |
270 this.serializeInternal(document.documentElement); | |
271 return this.strings_.join(''); | |
272 } | |
273 | |
274 /** | |
275 * @private | |
276 * @param {!HTMLElement} element | |
277 */ | |
278 serializeChildren(element) { | |
279 /** @type {!Array<!Node>} */ | |
280 const childNodes = Array.from(element.childNodes); | |
yosin_UTC9
2016/10/25 09:41:11
Since Node#childNodes works with for-of, we don't
Xiaocheng
2016/10/25 13:19:17
Switched to a for loop.
| |
281 if (childNodes.length === 0) { | |
yosin_UTC9
2016/10/25 09:41:11
if (element.childNodes.length === 0)
Xiaocheng
2016/10/25 13:19:16
Switched to a for loop and removed this check.
Pe
| |
282 this.advancedTo(element, 0); | |
283 return; | |
284 } | |
285 | |
286 /** @type {number} */ | |
287 let childIndex = 0; | |
288 for (const child of childNodes) { | |
289 this.advancedTo(element, childIndex); | |
290 this.serializeInternal(child, childIndex); | |
291 ++childIndex; | |
292 } | |
293 this.advancedTo(element, childIndex); | |
294 } | |
295 | |
296 /** | |
297 * @private | |
298 * @param {!Node} node | |
299 */ | |
300 serializeInternal(node) { | |
301 if (isElement(node)) | |
302 return this.handleElementNode(node); | |
303 if (isCharacterData(node)) | |
304 return this.handleCharacterData(node); | |
305 throw new Error(`Unexpected node ${node}`); | |
306 } | |
307 } | |
308 | |
46 /** @type {string} */ | 309 /** @type {string} */ |
47 const kSpelling = 'spelling'; | 310 const kSpelling = 'spelling'; |
48 /** @type {string} */ | 311 /** @type {string} */ |
49 const kGrammar = 'grammar'; | 312 const kGrammar = 'grammar'; |
50 | 313 /** @type {string} */ |
51 class Marker { | 314 const kSpellingMarker = '_'; |
52 /** | 315 /** @type {string} */ |
53 * @public | 316 const kGrammarMarker = '~'; |
54 * @param {number} location | 317 /** @type {string} */ |
55 * @param {number} length | 318 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 | 319 |
217 /** | 320 /** |
218 * @param {!Test} testObject | 321 * @param {!Test} testObject |
219 * @param {!Sample} sample, | 322 * @param {!Sample} sample |
220 * @param {!Array<!Marker>} expectedMarkers | 323 * @param {string} expectedText |
221 * @param {number} remainingRetry | 324 * @param {number} remainingRetry |
222 * @param {number} retryInterval | 325 * @param {number} retryInterval |
223 */ | 326 */ |
224 function verifyMarkers( | 327 function verifyMarkers( |
225 testObject, sample, expectedMarkers, remainingRetry, retryInterval) { | 328 testObject, sample, expectedText, remainingRetry, retryInterval) { |
226 assert_not_equals( | 329 assert_not_equals( |
227 window.internals, undefined, | 330 window.internals, undefined, |
228 'window.internals is required for running automated spellcheck tests.'); | 331 'window.internals is required for running automated spellcheck tests.'); |
229 | 332 |
230 /** @type {!Array<!Marker>} */ | 333 /** @type {!MarkerSerializer} */ |
231 const actualMarkers = extractAllMarkers(sample.document); | 334 const serializer = new MarkerSerializer( |
335 [kSpelling, kGrammar], | |
336 [kSpellingMarker, kGrammarMarker, kBothMarkers].join('')); | |
337 | |
232 try { | 338 try { |
233 assert_equals(actualMarkers.length, expectedMarkers.length, | 339 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(); | 340 testObject.done(); |
238 sample.remove(); | 341 sample.remove(); |
239 } catch (error) { | 342 } catch (error) { |
240 if (remainingRetry <= 0) | 343 if (remainingRetry <= 0) |
241 throw error; | 344 throw error; |
242 | 345 |
243 // Force invoking idle time spellchecker in case it has not been run yet. | 346 // Force invoking idle time spellchecker in case it has not been run yet. |
244 if (window.testRunner) | 347 if (window.testRunner) |
245 window.testRunner.runIdleTasks(() => {}); | 348 window.testRunner.runIdleTasks(() => {}); |
246 | 349 |
247 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger | 350 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger |
248 // something in JavaScript (e.g., a |Promise|), so that we can actively | 351 // something in JavaScript (e.g., a |Promise|), so that we can actively |
249 // know the completion of spellchecking instead of passively waiting for | 352 // know the completion of spellchecking instead of passively waiting for |
250 // markers to appear or disappear. | 353 // markers to appear or disappear. |
251 testObject.step_timeout( | 354 testObject.step_timeout( |
252 () => verifyMarkers(testObject, sample, expectedMarkers, | 355 () => verifyMarkers(testObject, sample, expectedText, |
253 remainingRetry - 1, retryInterval), | 356 remainingRetry - 1, retryInterval), |
254 retryInterval); | 357 retryInterval); |
255 } | 358 } |
256 } | 359 } |
257 | 360 |
258 // Spellchecker gets triggered not only by text and selection change, but also | 361 // Spellchecker gets triggered not only by text and selection change, but also |
259 // by focus change. For example, misspelling markers in <INPUT> disappear when | 362 // by focus change. For example, misspelling markers in <INPUT> disappear when |
260 // the window loses focus, even though the selection does not change. | 363 // the window loses focus, even though the selection does not change. |
261 // Therefore, we disallow spellcheck tests from running simultaneously to | 364 // Therefore, we disallow spellcheck tests from running simultaneously to |
262 // prevent interference among them. If we call spellcheck_test while another | 365 // 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 | 366 // test is running, the new test will be added into testQueue waiting for the |
264 // completion of the previous test. | 367 // completion of the previous test. |
265 | 368 |
266 /** @type {boolean} */ | 369 /** @type {boolean} */ |
267 var spellcheckTestRunning = false; | 370 var spellcheckTestRunning = false; |
268 /** @type {!Array<!Object>} */ | 371 /** @type {!Array<!Object>} */ |
269 const testQueue = []; | 372 const testQueue = []; |
270 | 373 |
271 /** | 374 /** |
272 * @param {string} inputText | 375 * @param {string} inputText |
273 * @param {function(!Document)|string} tester | 376 * @param {function(!Document)|string} tester |
274 * @param {!Marker|!Array<!Marker>} expectedMarkers | 377 * @param {string} expectedText |
275 * @param {string=} opt_title | 378 * @param {string=} opt_title |
276 */ | 379 */ |
277 function invokeSpellcheckTest(inputText, tester, expectedMarkers, opt_title) { | 380 function invokeSpellcheckTest(inputText, tester, expectedText, opt_title) { |
278 spellcheckTestRunning = true; | 381 spellcheckTestRunning = true; |
279 | 382 |
280 /** @type {!Test} */ | 383 async_test(testObject => { |
281 const testObject = async_test(opt_title, {isSpellcheckTest: true}); | 384 // TODO(xiaochengh): Merge the following part with |assert_selection|. |
385 /** @type {!Sample} */ | |
386 const sample = new Sample(inputText); | |
387 if (typeof(tester) === 'function') { | |
388 tester.call(window, sample.document); | |
389 } else if (typeof(tester) === 'string') { | |
390 const strings = tester.split(/ (.+)/); | |
391 sample.document.execCommand(strings[0], false, strings[1]); | |
392 } else { | |
393 assert_unreached(`Invalid tester: ${tester}`); | |
394 } | |
282 | 395 |
283 if (!(expectedMarkers instanceof Array)) | 396 /** @type {number} */ |
284 expectedMarkers = [expectedMarkers] | 397 const kMaxRetry = 10; |
285 testObject.step(() => checkExpectedMarkers(expectedMarkers)); | 398 /** @type {number} */ |
399 const kRetryInterval = 50; | |
286 | 400 |
287 if (window.testRunner) | 401 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger |
288 window.testRunner.setMockSpellCheckerEnabled(true); | 402 // something in JavaScript (e.g., a |Promise|), so that we can actively know |
289 | 403 // the completion of spellchecking instead of passively waiting for markers to |
290 // TODO(xiaochengh): Merge the following part with |assert_selection|. | 404 // appear or disappear. |
291 /** @type {!Sample} */ | 405 testObject.step_timeout( |
292 const sample = new Sample(inputText); | 406 () => verifyMarkers(testObject, sample, expectedText, |
293 if (typeof(tester) === 'function') { | 407 kMaxRetry, kRetryInterval), |
294 tester.call(window, sample.document); | 408 kRetryInterval); |
295 } else if (typeof(tester) === 'string') { | 409 }, 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 } | 410 } |
316 | 411 |
317 add_result_callback(testObj => { | 412 add_result_callback(testObj => { |
318 if (!testObj.properties.isSpellcheckTest) | 413 if (!testObj.properties.isSpellcheckTest) |
319 return; | 414 return; |
320 spellcheckTestRunning = false; | 415 spellcheckTestRunning = false; |
321 /** @type {Object} */ | 416 /** @type {Object} */ |
322 const args = testQueue.shift(); | 417 const args = testQueue.shift(); |
323 if (args === undefined) | 418 if (args === undefined) |
324 return; | 419 return; |
325 invokeSpellcheckTest(args.inputText, args.tester, | 420 invokeSpellcheckTest(args.inputText, args.tester, |
326 args.expectedMarkers, args.opt_title); | 421 args.expectedText, args.opt_title); |
327 }); | 422 }); |
328 | 423 |
424 // TODO(xiaochengh): Add support for checking marker descriptions. | |
425 | |
329 /** | 426 /** |
330 * @param {string} inputText | 427 * @param {string} inputText |
331 * @param {function(!Document)|string} tester | 428 * @param {function(!Document)|string} tester |
332 * @param {!Marker|!Array<!Marker>} expectedMarkers | 429 * @param {string} expectedText |
333 * @param {string=} opt_title | 430 * @param {string=} opt_title |
334 */ | 431 */ |
335 function spellcheckTest(inputText, tester, expectedMarkers, opt_title) { | 432 function spellcheckTest(inputText, tester, expectedText, opt_title) { |
433 if (window.testRunner) | |
yosin_UTC9
2016/10/25 09:41:11
We should abort if window.internals is unavailable
Xiaocheng
2016/10/25 13:19:17
There's a reason not to abort here.
|internals| i
| |
434 window.testRunner.setMockSpellCheckerEnabled(true); | |
435 | |
336 if (spellcheckTestRunning) { | 436 if (spellcheckTestRunning) { |
337 testQueue.push({ | 437 testQueue.push({ |
338 inputText: inputText, tester: tester, | 438 inputText: inputText, tester: tester, |
339 expectedMarkers: expectedMarkers, opt_title: opt_title}); | 439 expectedText: expectedText, opt_title: opt_title}); |
340 return; | 440 return; |
341 } | 441 } |
342 | 442 |
343 invokeSpellcheckTest(inputText, tester, expectedMarkers, opt_title); | 443 invokeSpellcheckTest(inputText, tester, expectedText, opt_title); |
344 } | 444 } |
345 | 445 |
346 // Export symbols | 446 // Export symbols |
347 window.Marker = Marker; | |
348 window.spellingMarker = spellingMarker; | |
349 window.grammarMarker = grammarMarker; | |
350 window.spellcheck_test = spellcheckTest; | 447 window.spellcheck_test = spellcheckTest; |
351 })(); | 448 })(); |
OLD | NEW |