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 indicating the expected result, where text |
19 // |Marker| is an |Object| with the following properties: | 20 // with spelling marker is surrounded by '_', and text with grammar marker is |
20 // - |location| and |length| are integers indicating the range of the marked | 21 // surrounded by '~'. |
21 // text. It must hold that |location >= 0| and |length > 0|. | |
22 // - |type| is an optional string indicating the marker type. When present, it | |
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 // | 22 // |
27 // |opt_title| is an optional string giving the title of the test case. | 23 // |opt_title| is an optional string giving the title of the test case. |
28 // | 24 // |
29 // Examples: | 25 // 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 | 26 |
43 (function() { | 27 (function() { |
44 const Sample = window.Sample; | 28 const Sample = window.Sample; |
45 | 29 |
46 /** @type {string} */ | 30 // TODO(editing-dev): Once we can import JavaScript file from scripts, we should |
47 const kSpelling = 'spelling'; | 31 // import "imported/wpt/html/resources/common.js", since |HTML5_VOID_ELEMENTS| |
48 /** @type {string} */ | 32 // is defined in there. |
49 const kGrammar = 'grammar'; | 33 /** |
50 | 34 * @const @type {!Set<string>} |
51 class Marker { | 35 * only void (without end tag) HTML5 elements |
| 36 */ |
| 37 const HTML5_VOID_ELEMENTS = new Set([ |
| 38 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', |
| 39 'keygen', 'link', 'meta', 'param', 'source','track', 'wbr' ]); |
| 40 |
| 41 // TODO(editing-dev): Reduce code duplication with assert_selection's Serializer |
| 42 // once we can import and export Javascript modules. |
| 43 |
| 44 /** |
| 45 * @param {!Node} node |
| 46 * @return {boolean} |
| 47 */ |
| 48 function isCharacterData(node) { |
| 49 return node.nodeType === Node.TEXT_NODE || |
| 50 node.nodeType === Node.COMMENT_NODE; |
| 51 } |
| 52 |
| 53 /** |
| 54 * @param {!Node} node |
| 55 * @return {boolean} |
| 56 */ |
| 57 function isElement(node) { |
| 58 return node.nodeType === Node.ELEMENT_NODE; |
| 59 } |
| 60 |
| 61 /** |
| 62 * @param {!Node} node |
| 63 * @return {boolean} |
| 64 */ |
| 65 function isHTMLInputElement(node) { |
| 66 return node.nodeName === 'INPUT'; |
| 67 } |
| 68 |
| 69 /** |
| 70 * @param {!Node} node |
| 71 * @return {boolean} |
| 72 */ |
| 73 function isHTMLTextAreaElement(node) { |
| 74 return node.nodeName === 'TEXTAREA'; |
| 75 } |
| 76 |
| 77 /** |
| 78 * @param {?Range} range |
| 79 * @param {!Node} node |
| 80 * @param {number} offset |
| 81 */ |
| 82 function isAtRangeEnd(range, node, offset) { |
| 83 return range && node === range.endContainer && offset === range.endOffset; |
| 84 } |
| 85 |
| 86 class MarkerSerializer { |
52 /** | 87 /** |
53 * @public | 88 * @public |
54 * @param {number} location | 89 * @param {!Object} markerTypes |
55 * @param {number} length | 90 */ |
56 * @param {string=} opt_type | 91 constructor(markerTypes) { |
57 * @param {string=} opt_description | 92 /** @type {!Array<string>} */ |
58 */ | 93 this.strings_ = []; |
59 constructor(location, length, opt_type, opt_description) { | 94 /** @type {!Object} */ |
| 95 this.markerTypes_ = markerTypes; |
| 96 /** @type {!Object} */ |
| 97 this.activeMarkerRanges_ = {}; |
| 98 for (let type in markerTypes) |
| 99 this.activeMarkerRanges_[type] = null; |
| 100 } |
| 101 |
| 102 /** |
| 103 * @private |
| 104 * @param {string} string |
| 105 */ |
| 106 emit(string) { this.strings_.push(string); } |
| 107 |
| 108 /** |
| 109 * @private |
| 110 * @param {!Node} node |
| 111 * @param {number} offset |
| 112 */ |
| 113 advancedTo(node, offset) { |
| 114 for (let type in this.markerTypes_) { |
| 115 // Handle the ending of the current active marker. |
| 116 if (isAtRangeEnd(this.activeMarkerRanges_[type], node, offset)) { |
| 117 this.activeMarkerRanges_[type] = null; |
| 118 this.emit(this.markerTypes_[type]); |
| 119 } |
| 120 |
| 121 // Handle the starting of the next active marker. |
| 122 if (this.activeMarkerRanges_[type]) |
| 123 return; |
| 124 /** @type {number} */ |
| 125 const markerCount = window.internals.markerCountForNode(node, type); |
| 126 for (let i = 0; i < markerCount; ++i) { |
| 127 const marker = window.internals.markerRangeForNode(node, type, i); |
| 128 assert_equals( |
| 129 marker.startContainer, node, |
| 130 'Internal error: marker range not starting in the annotated node.'); |
| 131 assert_equals( |
| 132 marker.endContainer, node, |
| 133 'Internal error: marker range not ending in the annotated node.'); |
| 134 if (marker.startOffset === offset) { |
| 135 assert_greater_than(marker.endOffset, offset, |
| 136 'Internal error: marker range is collapsed.'); |
| 137 this.activeMarkerRanges_[type] = marker; |
| 138 this.emit(this.markerTypes_[type]); |
| 139 break; |
| 140 } |
| 141 } |
| 142 } |
| 143 } |
| 144 |
| 145 /** |
| 146 * @private |
| 147 * @param {!CharacterData} node |
| 148 */ |
| 149 handleCharacterData(node) { |
| 150 /** @type {string} */ |
| 151 const text = node.nodeValue; |
60 /** @type {number} */ | 152 /** @type {number} */ |
61 this.location_ = location; | 153 const length = text.length; |
62 /** @type {number} */ | 154 for (let offset = 0; offset < length; ++offset) { |
63 this.length_ = length; | 155 this.advancedTo(node, offset); |
| 156 this.emit(text[offset]); |
| 157 } |
| 158 this.advancedTo(node, length); |
| 159 } |
| 160 |
| 161 /** |
| 162 * @private |
| 163 * @param {!HTMLElement} element |
| 164 */ |
| 165 handleInnerEditorOf(element) { |
| 166 /** @type {!ShadowRoot} */ |
| 167 const shadowRoot = window.internals.shadowRoot(element); |
| 168 assert_not_equals( |
| 169 shadowRoot, undefined, |
| 170 'Internal error: text form control element not having shadow tree as ' + |
| 171 'inner editor.'); |
| 172 /** @type {!HTMLDivElement} */ |
| 173 const innerEditor = shadowRoot.firstChild; |
| 174 assert_equals(innerEditor.tagName, 'DIV', |
| 175 'Internal error: inner editor is not DIV'); |
| 176 innerEditor.childNodes.forEach(child => { |
| 177 assert_true(isCharacterData(child), |
| 178 'Internal error: inner editor having child node that is ' + |
| 179 'not CharacterData.'); |
| 180 this.handleCharacterData(child); |
| 181 }); |
| 182 } |
| 183 |
| 184 /** |
| 185 * @private |
| 186 * @param {!HTMLInputElement} element |
| 187 */ |
| 188 handleInputNode(element) { |
| 189 this.emit(' value="'); |
| 190 this.handleInnerEditorOf(element); |
| 191 this.emit('"'); |
| 192 } |
| 193 |
| 194 /** |
| 195 * @private |
| 196 * @param {!HTMLElement} element |
| 197 */ |
| 198 handleElementNode(element) { |
64 /** @type {string} */ | 199 /** @type {string} */ |
65 this.type_ = opt_type || 'spelling'; | 200 const tagName = element.tagName.toLowerCase(); |
66 /** @type {boolean} */ | 201 this.emit(`<${tagName}`); |
67 this.ignoreDescription_ = opt_description === undefined; | 202 Array.from(element.attributes) |
68 /** @type {string} */ | 203 .sort((attr1, attr2) => attr1.name.localeCompare(attr2.name)) |
69 this.description_ = opt_description || ''; | 204 .forEach(attr => { |
70 } | 205 if (attr.value === '') |
71 | 206 return this.emit(` ${attr.name}`); |
72 /** @return {number} */ | 207 const value = attr.value.replace(/&/g, '&') |
73 get location() { return this.location_; } | 208 .replace(/\u0022/g, '"') |
74 | 209 .replace(/\u0027/g, '''); |
75 /** @return {number} */ | 210 this.emit(` ${attr.name}="${value}"`); |
76 get length() { return this.length_; } | 211 }); |
77 | 212 if (isHTMLInputElement(element) && element.value) |
78 /** @return {string} */ | 213 this.handleInputNode(element); |
79 get type() { return this.type_; } | 214 this.emit('>'); |
80 | 215 if (HTML5_VOID_ELEMENTS.has(tagName)) |
81 /** @return {boolean} */ | 216 return; |
82 get ignoreDescription() { return this.ignoreDescription_; } | 217 this.serializeChildren(element); |
83 | 218 this.emit(`</${tagName}>`); |
84 /** @return {string} */ | 219 } |
85 get description() { return this.description_; } | |
86 | 220 |
87 /** | 221 /** |
88 * @public | 222 * @public |
89 */ | 223 * @param {!HTMLDocument} document |
90 assertValid() { | 224 */ |
91 // TODO(xiaochengh): Add proper assert descriptions when needed. | 225 serialize(document) { |
92 assert_true(Number.isInteger(this.location_)); | 226 if (document.body) |
93 assert_greater_than_equal(this.location_, 0); | 227 this.serializeChildren(document.body); |
94 assert_true(Number.isInteger(this.length_)); | 228 else |
95 assert_greater_than(this.length_, 0); | 229 this.serializeInternal(document.documentElement); |
96 assert_true(this.type_ === kSpelling || this.type_ === kGrammar); | 230 return this.strings_.join(''); |
97 assert_true(typeof this.description_ === 'string'); | 231 } |
98 } | 232 |
99 | 233 /** |
100 /** | 234 * @private |
101 * @public | 235 * @param {!HTMLElement} element |
102 * @param {!Marker} expected | 236 */ |
103 */ | 237 serializeChildren(element) { |
104 assertMatch(expected) { | 238 // For TEXTAREA, handle its inner editor instead of its children. |
105 try { | 239 if (isHTMLTextAreaElement(element) && element.value) { |
106 assert_equals(this.location, expected.location); | 240 this.handleInnerEditorOf(element); |
107 assert_equals(this.length, expected.length); | 241 return; |
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 } | 242 } |
115 } | 243 |
116 | 244 element.childNodes.forEach(child => this.serializeInternal(child)); |
117 /** @override */ | 245 } |
118 toString() { | 246 |
119 return `${this.type_} marker at ` + | 247 /** |
120 `[${this.location_}, ${this.location_ + this.length_}]` + | 248 * @private |
121 (this.description_ ? ` with description "${this.description_}"` : ``); | 249 * @param {!Node} node |
122 } | 250 */ |
123 } | 251 serializeInternal(node) { |
124 | 252 if (isElement(node)) |
125 /** | 253 return this.handleElementNode(node); |
126 * @param {number} location | 254 if (isCharacterData(node)) |
127 * @param {number} length | 255 return this.handleCharacterData(node); |
128 * @param {string=} opt_description | 256 throw new Error(`Unexpected node ${node}`); |
129 * @return {!Marker} | 257 } |
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 } | 258 } |
216 | 259 |
217 /** | 260 /** |
218 * @param {!Test} testObject | 261 * @param {!Test} testObject |
219 * @param {!Sample} sample, | 262 * @param {!Sample} sample |
220 * @param {!Array<!Marker>} expectedMarkers | 263 * @param {string} expectedText |
221 * @param {number} remainingRetry | 264 * @param {number} remainingRetry |
222 * @param {number} retryInterval | 265 * @param {number} retryInterval |
223 */ | 266 */ |
224 function verifyMarkers( | 267 function verifyMarkers( |
225 testObject, sample, expectedMarkers, remainingRetry, retryInterval) { | 268 testObject, sample, expectedText, remainingRetry, retryInterval) { |
226 assert_not_equals( | 269 assert_not_equals( |
227 window.internals, undefined, | 270 window.internals, undefined, |
228 'window.internals is required for running automated spellcheck tests.'); | 271 'window.internals is required for running automated spellcheck tests.'); |
229 | 272 |
230 /** @type {!Array<!Marker>} */ | 273 /** @type {!MarkerSerializer} */ |
231 const actualMarkers = extractAllMarkers(sample.document); | 274 const serializer = new MarkerSerializer({ |
| 275 spelling: '_', |
| 276 grammar: '~'}); |
| 277 |
232 try { | 278 try { |
233 assert_equals(actualMarkers.length, expectedMarkers.length, | 279 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(); | 280 testObject.done(); |
238 sample.remove(); | 281 sample.remove(); |
239 } catch (error) { | 282 } catch (error) { |
240 if (remainingRetry <= 0) | 283 if (remainingRetry <= 0) |
241 throw error; | 284 throw error; |
242 | 285 |
243 // Force invoking idle time spellchecker in case it has not been run yet. | 286 // Force invoking idle time spellchecker in case it has not been run yet. |
244 if (window.testRunner) | 287 if (window.testRunner) |
245 window.testRunner.runIdleTasks(() => {}); | 288 window.testRunner.runIdleTasks(() => {}); |
246 | 289 |
247 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger | 290 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger |
248 // something in JavaScript (e.g., a |Promise|), so that we can actively | 291 // something in JavaScript (e.g., a |Promise|), so that we can actively |
249 // know the completion of spellchecking instead of passively waiting for | 292 // know the completion of spellchecking instead of passively waiting for |
250 // markers to appear or disappear. | 293 // markers to appear or disappear. |
251 testObject.step_timeout( | 294 testObject.step_timeout( |
252 () => verifyMarkers(testObject, sample, expectedMarkers, | 295 () => verifyMarkers(testObject, sample, expectedText, |
253 remainingRetry - 1, retryInterval), | 296 remainingRetry - 1, retryInterval), |
254 retryInterval); | 297 retryInterval); |
255 } | 298 } |
256 } | 299 } |
257 | 300 |
258 // Spellchecker gets triggered not only by text and selection change, but also | 301 // Spellchecker gets triggered not only by text and selection change, but also |
259 // by focus change. For example, misspelling markers in <INPUT> disappear when | 302 // by focus change. For example, misspelling markers in <INPUT> disappear when |
260 // the window loses focus, even though the selection does not change. | 303 // the window loses focus, even though the selection does not change. |
261 // Therefore, we disallow spellcheck tests from running simultaneously to | 304 // Therefore, we disallow spellcheck tests from running simultaneously to |
262 // prevent interference among them. If we call spellcheck_test while another | 305 // 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 | 306 // test is running, the new test will be added into testQueue waiting for the |
264 // completion of the previous test. | 307 // completion of the previous test. |
265 | 308 |
266 /** @type {boolean} */ | 309 /** @type {boolean} */ |
267 var spellcheckTestRunning = false; | 310 var spellcheckTestRunning = false; |
268 /** @type {!Array<!Object>} */ | 311 /** @type {!Array<!Object>} */ |
269 const testQueue = []; | 312 const testQueue = []; |
270 | 313 |
271 /** | 314 /** |
272 * @param {string} inputText | 315 * @param {string} inputText |
273 * @param {function(!Document)|string} tester | 316 * @param {function(!Document)|string} tester |
274 * @param {!Marker|!Array<!Marker>} expectedMarkers | 317 * @param {string} expectedText |
275 * @param {string=} opt_title | 318 * @param {string=} opt_title |
276 */ | 319 */ |
277 function invokeSpellcheckTest(inputText, tester, expectedMarkers, opt_title) { | 320 function invokeSpellcheckTest(inputText, tester, expectedText, opt_title) { |
278 spellcheckTestRunning = true; | 321 spellcheckTestRunning = true; |
279 | 322 |
280 /** @type {!Test} */ | 323 async_test(testObject => { |
281 const testObject = async_test(opt_title, {isSpellcheckTest: true}); | 324 // TODO(xiaochengh): Merge the following part with |assert_selection|. |
| 325 /** @type {!Sample} */ |
| 326 const sample = new Sample(inputText); |
| 327 if (typeof(tester) === 'function') { |
| 328 tester.call(window, sample.document); |
| 329 } else if (typeof(tester) === 'string') { |
| 330 const strings = tester.split(/ (.+)/); |
| 331 sample.document.execCommand(strings[0], false, strings[1]); |
| 332 } else { |
| 333 assert_unreached(`Invalid tester: ${tester}`); |
| 334 } |
282 | 335 |
283 if (!(expectedMarkers instanceof Array)) | 336 /** @type {number} */ |
284 expectedMarkers = [expectedMarkers] | 337 const kMaxRetry = 10; |
285 testObject.step(() => checkExpectedMarkers(expectedMarkers)); | 338 /** @type {number} */ |
| 339 const kRetryInterval = 50; |
286 | 340 |
287 if (window.testRunner) | 341 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger |
288 window.testRunner.setMockSpellCheckerEnabled(true); | 342 // something in JavaScript (e.g., a |Promise|), so that we can actively know |
289 | 343 // the completion of spellchecking instead of passively waiting for markers |
290 // TODO(xiaochengh): Merge the following part with |assert_selection|. | 344 // to appear or disappear. |
291 /** @type {!Sample} */ | 345 testObject.step_timeout( |
292 const sample = new Sample(inputText); | 346 () => verifyMarkers(testObject, sample, expectedText, |
293 if (typeof(tester) === 'function') { | 347 kMaxRetry, kRetryInterval), |
294 tester.call(window, sample.document); | 348 kRetryInterval); |
295 } else if (typeof(tester) === 'string') { | 349 }, 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 } | 350 } |
316 | 351 |
317 add_result_callback(testObj => { | 352 add_result_callback(testObj => { |
318 if (!testObj.properties.isSpellcheckTest) | 353 if (!testObj.properties.isSpellcheckTest) |
319 return; | 354 return; |
320 spellcheckTestRunning = false; | 355 spellcheckTestRunning = false; |
321 /** @type {Object} */ | 356 /** @type {Object} */ |
322 const args = testQueue.shift(); | 357 const args = testQueue.shift(); |
323 if (args === undefined) | 358 if (args === undefined) |
324 return; | 359 return; |
325 invokeSpellcheckTest(args.inputText, args.tester, | 360 invokeSpellcheckTest(args.inputText, args.tester, |
326 args.expectedMarkers, args.opt_title); | 361 args.expectedText, args.opt_title); |
327 }); | 362 }); |
328 | 363 |
329 /** | 364 /** |
330 * @param {string} inputText | 365 * @param {string} inputText |
331 * @param {function(!Document)|string} tester | 366 * @param {function(!Document)|string} tester |
332 * @param {!Marker|!Array<!Marker>} expectedMarkers | 367 * @param {string} expectedText |
333 * @param {string=} opt_title | 368 * @param {string=} opt_title |
334 */ | 369 */ |
335 function spellcheckTest(inputText, tester, expectedMarkers, opt_title) { | 370 function spellcheckTest(inputText, tester, expectedText, opt_title) { |
| 371 if (window.testRunner) |
| 372 window.testRunner.setMockSpellCheckerEnabled(true); |
| 373 |
336 if (spellcheckTestRunning) { | 374 if (spellcheckTestRunning) { |
337 testQueue.push({ | 375 testQueue.push({ |
338 inputText: inputText, tester: tester, | 376 inputText: inputText, tester: tester, |
339 expectedMarkers: expectedMarkers, opt_title: opt_title}); | 377 expectedText: expectedText, opt_title: opt_title}); |
340 return; | 378 return; |
341 } | 379 } |
342 | 380 |
343 invokeSpellcheckTest(inputText, tester, expectedMarkers, opt_title); | 381 invokeSpellcheckTest(inputText, tester, expectedText, opt_title); |
344 } | 382 } |
345 | 383 |
346 // Export symbols | 384 // Export symbols |
347 window.Marker = Marker; | |
348 window.spellingMarker = spellingMarker; | |
349 window.grammarMarker = grammarMarker; | |
350 window.spellcheck_test = spellcheckTest; | 385 window.spellcheck_test = spellcheckTest; |
351 })(); | 386 })(); |
OLD | NEW |