OLD | NEW |
---|---|
(Empty) | |
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 | |
3 // found in the LICENSE file. | |
4 | |
5 'use strict'; | |
6 | |
7 // This file provides | |
8 // |spellcheck_test(sample, tester, expectedMarkers, opt_title)| asynchronous | |
9 // test to W3C test harness for easier writing of editing test cases. | |
10 // | |
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 | |
13 // boundary point marker "^" indicating the initial selection. | |
14 // | |
15 // |tester| is either name with parameter of execCommand or function taking | |
16 // one parameter |Document|. | |
17 // | |
18 // |expectedMarkers| is either a |Marker| or a |Marker| array, where each | |
19 // |Marker| is an |Object| with the following properties: | |
20 // - |location| and |length| are integers indicating the range of the marked | |
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 // | |
27 // |opt_title| is an optional string giving the title of the test case. | |
28 // | |
29 // Examples: | |
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 | |
43 (function() { | |
44 const Sample = window.Sample; | |
45 | |
46 /** @type {string} */ | |
47 const kSpelling = 'spelling'; | |
48 /** @type {string} */ | |
49 const kGrammar = 'grammar'; | |
50 | |
51 class Marker { | |
52 /** | |
53 * @public | |
54 * @param {number} location | |
55 * @param {number} length | |
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 assert_equals(this.location, expected.location, | |
106 'Marker locations mismatch.'); | |
107 assert_equals(this.length, expected.length, 'Marker lengths mismatch.'); | |
108 assert_equals(this.type, expected.type, 'Marker types mismatch.'); | |
109 if (expected.ignoreDescription) | |
110 return; | |
111 assert_equals(this.description, expected.description, | |
112 'Marker descriptions mismatch'); | |
113 } | |
114 } | |
115 | |
116 /** | |
117 * @param {number} location | |
118 * @param {number} length | |
119 * @param {string=} opt_description | |
120 * @return {!Marker} | |
121 */ | |
122 function spellingMarker(location, length, opt_description) { | |
123 return new Marker(location, length, kSpelling, opt_description); | |
124 } | |
125 | |
126 /** | |
127 * @param {number} location | |
128 * @param {number} length | |
129 * @param {string=} opt_description | |
130 * @return {!Marker} | |
131 */ | |
132 function grammarMarker(location, length, opt_description) { | |
133 return new Marker(location, length, kGrammar, opt_description); | |
134 } | |
135 | |
136 /** | |
137 * @param {!Marker} marker1 | |
138 * @param {!Marker} marker2 | |
139 * @return {number} | |
140 */ | |
141 function markerComparison(marker1, marker2) { | |
142 return marker1.location - marker2.location; | |
143 } | |
144 | |
145 /** | |
146 * @param {!Array<!Marker>} expectedMarkers | |
147 */ | |
148 function checkExpectedMarkers(expectedMarkers) { | |
149 if (!expectedMarkers.length) | |
yosin_UTC9
2016/10/24 07:54:14
Better to write: |expectedMarkers.length === 0|
Xiaocheng
2016/10/24 08:29:23
Done.
| |
150 return; | |
151 expectedMarkers.forEach(marker => marker.assertValid()); | |
152 expectedMarkers.sort(markerComparison); | |
153 expectedMarkers.reduce((lastMarker, currentMarker) => { | |
154 assert_less_than( | |
155 lastMarker.location + lastMarker.length, currentMarker.location, | |
156 'Marker ranges should be disjoint.'); | |
157 return currentMarker; | |
158 }); | |
159 } | |
160 | |
161 /** | |
162 * @param {!Node} node | |
163 * @param {string} type | |
164 * @param {!Array<!Marker>} markers | |
165 */ | |
166 function extractMarkersOfType(node, type, markers) { | |
167 /** @type {!HTMLBodyElement} */ | |
168 const body = node.ownerDocument.body; | |
169 /** @type {number} */ | |
170 const markerCount = window.internals.markerCountForNode(node, type); | |
171 for (var i = 0; i < markerCount; ++i) { | |
yosin_UTC9
2016/10/24 07:54:14
nit: s/var/let/
Xiaocheng
2016/10/24 08:29:24
Done.
| |
172 /** @type {!Range} */ | |
173 const markerRange = window.internals.markerRangeForNode(node, type, i); | |
174 /** @type {string} */ | |
175 const description = window.internals.markerDescriptionForNode(node, type, i) ; | |
176 /** @type {number} */ | |
177 const location = window.internals.locationFromRange(body, markerRange); | |
178 /** @type {number} */ | |
179 const length = window.internals.lengthFromRange(body, markerRange); | |
180 | |
181 markers.push(new Marker(location, length, type, description)); | |
182 } | |
183 } | |
184 | |
185 /** | |
186 * @param {!Node} node | |
187 * @param {!Array<!Marker>} markers | |
188 */ | |
189 function extractAllMarkersRecursivelyTo(node, markers) { | |
190 extractMarkersOfType(node, kSpelling, markers); | |
191 extractMarkersOfType(node, kGrammar, markers); | |
192 node.childNodes.forEach( | |
193 child => extractAllMarkersRecursivelyTo(child, markers)); | |
194 } | |
195 | |
196 /** | |
197 * @param {!Document} doc | |
198 * @return {!Array<!Marker>} | |
199 */ | |
200 function extractAllMarkers(doc) { | |
201 /** @type {!Array<!Marker>} */ | |
202 const markers = []; | |
203 extractAllMarkersRecursivelyTo(doc.body, markers); | |
204 markers.sort(markerComparison); | |
205 return markers; | |
206 } | |
207 | |
208 /** | |
209 * @param {!Test} testObject | |
210 * @param {!Sample} sample, | |
211 * @param {!Array<!Marker>} expectedMarkers | |
212 * @param {number} remainingRetry | |
213 * @param {number} retryInterval | |
214 */ | |
215 function verifyMarkers( | |
216 testObject, sample, expectedMarkers, remainingRetry, retryInterval) { | |
217 assert_not_equals( | |
218 window.internals, undefined, | |
219 'window.internals is required for running automated spellcheck tests.'); | |
220 | |
221 /** @type {!Array<!Marker>} */ | |
222 const actualMarkers = extractAllMarkers(sample.document); | |
223 try { | |
224 assert_equals(actualMarkers.length, expectedMarkers.length, | |
225 'Number of markers mismatch.'); | |
226 actualMarkers.forEach( | |
227 (marker, index) => marker.assertMatch(expectedMarkers[index])); | |
228 testObject.done(); | |
229 sample.remove(); | |
230 } catch (error) { | |
231 if (remainingRetry <= 0) | |
232 throw error; | |
233 | |
234 // Force invoking idle time spellchecker in case it has not been run yet. | |
235 if (window.testRunner) | |
236 window.testRunner.runIdleTasks(() => {}); | |
237 | |
238 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger | |
239 // something in JavaScript (e.g., a |Promise|), so that we can actively | |
240 // know the completion of spellchecking instead of passively waiting for | |
241 // markers to appear or disappear. | |
242 testObject.step_timeout( | |
243 () => verifyMarkers(testObject, sample, expectedMarkers, | |
244 remainingRetry - 1, retryInterval), | |
245 retryInterval); | |
246 } | |
247 } | |
248 | |
249 // Spellchecker gets triggered not only by text and selection change, but also | |
250 // by focus change. For example, misspelling markers in <INPUT> disappear when | |
251 // the window loses focus, even though the selection does not change. | |
252 // Therefore, we disallow spellcheck tests from running simultaneously to | |
253 // prevent interference among them. If we call spellcheck_test while another | |
254 // test is running, the new test will be added into testQueue waiting for the | |
255 // completion of the previous test. | |
256 | |
257 /** @type {boolean} */ | |
258 var spellcheckTestRunning = false; | |
259 /** @type {!Array<!Object>} */ | |
260 const testQueue = []; | |
261 | |
262 /** | |
263 * @param {string} inputText | |
264 * @param {function(!Document)|string} tester | |
265 * @param {!Marker|!Array<!Marker>} expectedMarkers | |
266 * @param {string=} opt_title | |
267 */ | |
268 function invokeSpellcheckTest(inputText, tester, expectedMarkers, opt_title) { | |
269 spellcheckTestRunning = true; | |
270 | |
271 /** @type {!Test} */ | |
272 const testObject = async_test(opt_title, {isSpellcheckTest: true}); | |
273 | |
274 if (!(expectedMarkers instanceof Array)) | |
275 expectedMarkers = [expectedMarkers] | |
276 testObject.step(() => checkExpectedMarkers(expectedMarkers)); | |
277 | |
278 if (window.testRunner) | |
279 window.testRunner.setMockSpellCheckerEnabled(true); | |
280 | |
281 /** @type {!Sample} */ | |
282 const sample = new Sample(inputText); | |
283 if (typeof(tester) === 'function') { | |
yosin_UTC9
2016/10/24 07:54:14
How about moving this if-else-if fragment to |Samp
Xiaocheng
2016/10/24 08:29:23
This part is slightly different from assert_select
| |
284 tester.call(window, sample.document); | |
285 } else if (typeof(tester) === 'string') { | |
286 const strings = tester.split(/ (.+)/); | |
287 sample.document.execCommand(strings[0], false, strings[1]); | |
288 } else { | |
289 testObject.step(() => assert_unreached(`Invalid tester: ${tester}`)); | |
290 } | |
291 | |
292 /** @type {number} */ | |
293 const kMaxRetry = 10; | |
294 /** @type {number} */ | |
295 const kRetryInterval = 50; | |
296 | |
297 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger | |
298 // something in JavaScript (e.g., a |Promise|), so that we can actively know | |
299 // the completion of spellchecking instead of passively waiting for markers to | |
300 // appear or disappear. | |
301 testObject.step_timeout( | |
302 () => verifyMarkers(testObject, sample, expectedMarkers, | |
303 kMaxRetry, kRetryInterval), | |
304 kRetryInterval); | |
305 } | |
306 | |
307 add_result_callback(testObj => { | |
308 if (!testObj.properties.isSpellcheckTest) | |
309 return; | |
310 spellcheckTestRunning = false; | |
311 /** @type {Object} */ | |
312 const args = testQueue.shift(); | |
313 if (args === undefined) | |
314 return; | |
315 invokeSpellcheckTest(args.inputText, args.tester, | |
316 args.expectedMarkers, args.opt_title); | |
317 }); | |
318 | |
319 /** | |
320 * @param {string} inputText | |
321 * @param {function(!Document)|string} tester | |
322 * @param {!Marker|!Array<!Marker>} expectedMarkers | |
323 * @param {string=} opt_title | |
324 */ | |
325 function spellcheckTest(inputText, tester, expectedMarkers, opt_title) { | |
326 if (spellcheckTestRunning) { | |
327 testQueue.push({ | |
328 inputText: inputText, tester: tester, | |
329 expectedMarkers: expectedMarkers, opt_title: opt_title}); | |
330 return; | |
331 } | |
332 | |
333 invokeSpellcheckTest(inputText, tester, expectedMarkers, opt_title); | |
334 } | |
335 | |
336 // Export symbols | |
337 window.Marker = Marker; | |
338 window.spellingMarker = spellingMarker; | |
339 window.grammarMarker = grammarMarker; | |
340 window.spellcheck_test = spellcheckTest; | |
341 })(); | |
OLD | NEW |