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 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 |
| 217 /** |
| 218 * @param {!Test} testObject |
| 219 * @param {!Sample} sample, |
| 220 * @param {!Array<!Marker>} expectedMarkers |
| 221 * @param {number} remainingRetry |
| 222 * @param {number} retryInterval |
| 223 */ |
| 224 function verifyMarkers( |
| 225 testObject, sample, expectedMarkers, remainingRetry, retryInterval) { |
| 226 assert_not_equals( |
| 227 window.internals, undefined, |
| 228 'window.internals is required for running automated spellcheck tests.'); |
| 229 |
| 230 /** @type {!Array<!Marker>} */ |
| 231 const actualMarkers = extractAllMarkers(sample.document); |
| 232 try { |
| 233 assert_equals(actualMarkers.length, expectedMarkers.length, |
| 234 'Number of markers mismatch.'); |
| 235 actualMarkers.forEach( |
| 236 (marker, index) => marker.assertMatch(expectedMarkers[index])); |
| 237 testObject.done(); |
| 238 sample.remove(); |
| 239 } catch (error) { |
| 240 if (remainingRetry <= 0) |
| 241 throw error; |
| 242 |
| 243 // Force invoking idle time spellchecker in case it has not been run yet. |
| 244 if (window.testRunner) |
| 245 window.testRunner.runIdleTasks(() => {}); |
| 246 |
| 247 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger |
| 248 // something in JavaScript (e.g., a |Promise|), so that we can actively |
| 249 // know the completion of spellchecking instead of passively waiting for |
| 250 // markers to appear or disappear. |
| 251 testObject.step_timeout( |
| 252 () => verifyMarkers(testObject, sample, expectedMarkers, |
| 253 remainingRetry - 1, retryInterval), |
| 254 retryInterval); |
| 255 } |
| 256 } |
| 257 |
| 258 // Spellchecker gets triggered not only by text and selection change, but also |
| 259 // by focus change. For example, misspelling markers in <INPUT> disappear when |
| 260 // the window loses focus, even though the selection does not change. |
| 261 // Therefore, we disallow spellcheck tests from running simultaneously to |
| 262 // 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 |
| 264 // completion of the previous test. |
| 265 |
| 266 /** @type {boolean} */ |
| 267 var spellcheckTestRunning = false; |
| 268 /** @type {!Array<!Object>} */ |
| 269 const testQueue = []; |
| 270 |
| 271 /** |
| 272 * @param {string} inputText |
| 273 * @param {function(!Document)|string} tester |
| 274 * @param {!Marker|!Array<!Marker>} expectedMarkers |
| 275 * @param {string=} opt_title |
| 276 */ |
| 277 function invokeSpellcheckTest(inputText, tester, expectedMarkers, opt_title) { |
| 278 spellcheckTestRunning = true; |
| 279 |
| 280 /** @type {!Test} */ |
| 281 const testObject = async_test(opt_title, {isSpellcheckTest: true}); |
| 282 |
| 283 if (!(expectedMarkers instanceof Array)) |
| 284 expectedMarkers = [expectedMarkers] |
| 285 testObject.step(() => checkExpectedMarkers(expectedMarkers)); |
| 286 |
| 287 if (window.testRunner) |
| 288 window.testRunner.setMockSpellCheckerEnabled(true); |
| 289 |
| 290 // TODO(xiaochengh): Merge the following part with |assert_selection|. |
| 291 /** @type {!Sample} */ |
| 292 const sample = new Sample(inputText); |
| 293 if (typeof(tester) === 'function') { |
| 294 tester.call(window, sample.document); |
| 295 } else if (typeof(tester) === 'string') { |
| 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 } |
| 316 |
| 317 add_result_callback(testObj => { |
| 318 if (!testObj.properties.isSpellcheckTest) |
| 319 return; |
| 320 spellcheckTestRunning = false; |
| 321 /** @type {Object} */ |
| 322 const args = testQueue.shift(); |
| 323 if (args === undefined) |
| 324 return; |
| 325 invokeSpellcheckTest(args.inputText, args.tester, |
| 326 args.expectedMarkers, args.opt_title); |
| 327 }); |
| 328 |
| 329 /** |
| 330 * @param {string} inputText |
| 331 * @param {function(!Document)|string} tester |
| 332 * @param {!Marker|!Array<!Marker>} expectedMarkers |
| 333 * @param {string=} opt_title |
| 334 */ |
| 335 function spellcheckTest(inputText, tester, expectedMarkers, opt_title) { |
| 336 if (spellcheckTestRunning) { |
| 337 testQueue.push({ |
| 338 inputText: inputText, tester: tester, |
| 339 expectedMarkers: expectedMarkers, opt_title: opt_title}); |
| 340 return; |
| 341 } |
| 342 |
| 343 invokeSpellcheckTest(inputText, tester, expectedMarkers, opt_title); |
| 344 } |
| 345 |
| 346 // Export symbols |
| 347 window.Marker = Marker; |
| 348 window.spellingMarker = spellingMarker; |
| 349 window.grammarMarker = grammarMarker; |
| 350 window.spellcheck_test = spellcheckTest; |
| 351 })(); |
OLD | NEW |