Chromium Code Reviews| 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 class Marker { | |
| 47 /** | |
| 48 * @public | |
| 49 * @param {number} location | |
| 50 * @param {number} length | |
| 51 * @param {string=} opt_type | |
| 52 * @param {string=} opt_description | |
| 53 */ | |
| 54 constructor(location, length, opt_type, opt_description) { | |
| 55 /** @type {number} */ | |
| 56 this.location_ = location; | |
| 57 /** @type {number} */ | |
| 58 this.length_ = length; | |
| 59 /** @type {string} */ | |
| 60 this.type_ = opt_type || 'spelling'; | |
| 61 /** @type {string|null} */ | |
|
yosin_UTC9
2016/10/24 06:58:32
s/string|null/?string/
Xiaocheng
2016/10/24 07:18:56
Done.
| |
| 62 this.description_ = opt_description ? opt_description : null; | |
|
yosin_UTC9
2016/10/24 06:58:32
How about using empty string as placeholder? If so
Xiaocheng
2016/10/24 07:18:56
Then we need empty string check instead of null ch
| |
| 63 } | |
| 64 | |
| 65 /** @return {number} */ | |
| 66 get location() { return this.location_; } | |
| 67 | |
| 68 /** @return {number} */ | |
| 69 get length() { return this.length_; } | |
| 70 | |
| 71 /** @return {string} */ | |
| 72 get type() { return this.type_; } | |
| 73 | |
| 74 /** @return {string|null} */ | |
|
yosin_UTC9
2016/10/24 06:58:32
s/string|null/?string/
Xiaocheng
2016/10/24 07:18:56
Done.
| |
| 75 get description() { return this.description_; } | |
| 76 | |
| 77 /** | |
| 78 * @public | |
| 79 */ | |
| 80 assertValid() { | |
| 81 // TODO(xiaochengh): Add proper assert descriptions when needed. | |
| 82 assert_true(Number.isInteger(this.location_)); | |
| 83 assert_greater_than_equal(this.location_, 0); | |
| 84 assert_true(Number.isInteger(this.length_)); | |
| 85 assert_greater_than(this.length_, 0); | |
| 86 assert_true(this.type_ === 'spelling' || this.type_ === 'grammar'); | |
|
yosin_UTC9
2016/10/24 06:58:32
Let's have const kSpelling = 'spelling' and const
Xiaocheng
2016/10/24 07:18:56
Done.
| |
| 87 if (this.description_ === null) | |
| 88 return; | |
| 89 assert_true(typeof this.description_ === 'string'); | |
| 90 } | |
| 91 | |
| 92 /** | |
| 93 * @public | |
| 94 * @param {!Marker} expected | |
| 95 */ | |
| 96 assertMatch(expected) { | |
| 97 assert_equals(this.location, expected.location, | |
| 98 'Marker locations mismatch.'); | |
| 99 assert_equals(this.length, expected.length, 'Marker lengths mismatch.'); | |
| 100 assert_equals(this.type, expected.type, 'Marker types mismatch.'); | |
| 101 if (expected.description === null) | |
| 102 return; | |
| 103 assert_equals(this.description, expected.description, | |
| 104 'Marker descriptions mismatch'); | |
| 105 } | |
| 106 } | |
| 107 | |
| 108 /** | |
| 109 * @param {number} location | |
| 110 * @param {number} length | |
| 111 * @param {string=} opt_description | |
| 112 * @return {!Marker} | |
| 113 */ | |
| 114 function spellingMarker(location, length, opt_description) { | |
| 115 return new Marker(location, length, 'spelling', opt_description); | |
| 116 } | |
| 117 | |
| 118 /** | |
| 119 * @param {number} location | |
| 120 * @param {number} length | |
| 121 * @param {string=} opt_description | |
| 122 * @return {!Marker} | |
| 123 */ | |
| 124 function grammarMarker(location, length, opt_description) { | |
| 125 return new Marker(location, length, 'grammar', opt_description); | |
| 126 } | |
| 127 | |
| 128 /** | |
| 129 * @param {!Array<!Marker>} expectedMarkers | |
| 130 */ | |
| 131 function checkExpectedMarkers(expectedMarkers) { | |
| 132 expectedMarkers.forEach(marker => marker.assertValid()); | |
| 133 expectedMarkers.sort((a, b) => a.location - b.location); | |
| 134 expectedMarkers.reduce((lastMarker, currentMarker) => { | |
| 135 assert_less_than( | |
| 136 lastMarker.location + lastMarker.length, currentMarker.location, | |
| 137 'Marker ranges should be disjoint.'); | |
| 138 return currentMarker; | |
| 139 }); | |
| 140 } | |
| 141 | |
| 142 /** | |
| 143 * @param {!Node} node | |
| 144 * @param {string} type | |
| 145 * @param {!Array<!Marker>} markers | |
| 146 */ | |
| 147 function extractMarkersOfType(node, type, markers) { | |
| 148 /** @type {!HTMLBodyElement} */ | |
| 149 const body = node.ownerDocument.body; | |
| 150 /** @type {number} */ | |
| 151 const markerCount = window.internals.markerCountForNode(node, type); | |
| 152 for (var i = 0; i < markerCount; ++i) { | |
| 153 /** @type {!Range} */ | |
| 154 const markerRange = window.internals.markerRangeForNode(node, type, i); | |
| 155 /** @type {string} */ | |
| 156 const description = window.internals.markerDescriptionForNode(node, type, i) ; | |
| 157 /** @type {number} */ | |
| 158 const location = window.internals.locationFromRange(body, markerRange); | |
| 159 /** @type {number} */ | |
| 160 const length = window.internals.lengthFromRange(body, markerRange); | |
| 161 | |
| 162 markers.push(new Marker(location, length, type, description)); | |
| 163 } | |
| 164 } | |
| 165 | |
| 166 /** | |
| 167 * @param {!Node} node | |
| 168 * @param {!Array<!Marker>} markers | |
| 169 */ | |
| 170 function extractAllMarkersRecursivelyTo(node, markers) { | |
| 171 extractMarkersOfType(node, 'spelling', markers); | |
| 172 extractMarkersOfType(node, 'grammar', markers); | |
| 173 node.childNodes.forEach( | |
| 174 child => extractAllMarkersRecursivelyTo(child, markers)); | |
| 175 } | |
| 176 | |
| 177 /** | |
| 178 * @param {!Document} doc | |
| 179 * @return {!Array<!Marker>} | |
| 180 */ | |
| 181 function extractAllMarkers(doc) { | |
| 182 /** @type {!Array<!Marker>} */ | |
| 183 const markers = []; | |
| 184 extractAllMarkersRecursivelyTo(doc.body, markers); | |
| 185 markers.sort((a, b) => a.location - b.location); | |
|
yosin_UTC9
2016/10/24 06:58:32
Please avoid using one letter variable name.
Xiaocheng
2016/10/24 07:18:56
The comparison function is isolated out as |functi
| |
| 186 return markers; | |
| 187 } | |
| 188 | |
| 189 /** | |
| 190 * @param {!Test} testObject | |
| 191 * @param {!Sample} sample, | |
| 192 * @param {!Array<!Marker>} expectedMarkers | |
| 193 * @param {number} remainingRetry | |
| 194 * @param {number} retryInterval | |
| 195 */ | |
| 196 function verifyMarkers( | |
| 197 testObject, sample, expectedMarkers, remainingRetry, retryInterval) { | |
| 198 assert_not_equals( | |
| 199 window.internals, undefined, | |
| 200 'window.internals is required for running automated spellcheck tests.'); | |
| 201 | |
| 202 /** @type {!Array<!Marker>} */ | |
| 203 const actualMarkers = extractAllMarkers(sample.document); | |
| 204 try { | |
| 205 assert_equals(actualMarkers.length, expectedMarkers.length, | |
| 206 'Number of markers mismatch.'); | |
| 207 actualMarkers.forEach( | |
| 208 (marker, index) => marker.assertMatch(expectedMarkers[index])); | |
| 209 testObject.done(); | |
| 210 sample.remove(); | |
| 211 } catch (e) { | |
|
yosin_UTC9
2016/10/24 06:58:32
Please avoid using one letter variable name.
Xiaocheng
2016/10/24 07:18:56
Done.
| |
| 212 if (remainingRetry > 0) { | |
| 213 // Force invoking idle time spellchecker if it was not run yet. | |
| 214 if (window.testRunner) | |
| 215 window.testRunner.runIdleTasks(() => {}); | |
| 216 | |
| 217 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger | |
| 218 // something in JavaScript (e.g., a |Promise|), so that we can actively | |
| 219 // know the completion of spellchecking instead of passively waiting for | |
| 220 // markers to appear or disappear. | |
| 221 testObject.step_timeout( | |
| 222 () => verifyMarkers(testObject, sample, expectedMarkers, | |
| 223 remainingRetry - 1, retryInterval), | |
| 224 retryInterval); | |
| 225 } else { | |
| 226 throw e; | |
|
yosin_UTC9
2016/10/24 06:58:32
We prefer early-return style. e.g.
if (remainingR
Xiaocheng
2016/10/24 07:18:56
Done.
| |
| 227 } | |
| 228 } | |
| 229 } | |
| 230 | |
| 231 // Spellchecker gets triggered not only by text and selection change, but also | |
| 232 // by focus change. For example, misspelling markers in <INPUT> disappear when | |
| 233 // the window loses focus, even though the selection does not change. | |
| 234 // Therefore, we disallow spellcheck tests from running simultaneously to | |
| 235 // prevent interference among them. If we call spellcheck_test while another | |
| 236 // test is running, the new test will be added into testQueue waiting for the | |
| 237 // completion of the previous test. | |
| 238 | |
| 239 /** @type {boolean} */ | |
| 240 var spellcheckTestRunning = false; | |
| 241 /** @type {!Array<!Object>} */ | |
| 242 const testQueue = []; | |
| 243 | |
| 244 /** | |
| 245 * @param {string} inputText | |
| 246 * @param {function(!Document)|string} tester | |
| 247 * @param {!Marker|!Array<!Marker>} expectedMarkers | |
| 248 * @param {string=} opt_title | |
| 249 */ | |
| 250 function invokeSpellcheckTest(inputText, tester, expectedMarkers, opt_title) { | |
| 251 spellcheckTestRunning = true; | |
| 252 | |
| 253 /** @type {!Test} */ | |
| 254 const testObject = async_test(opt_title, {isSpellcheckTest: true}); | |
| 255 | |
| 256 if (!(expectedMarkers instanceof Array)) | |
| 257 expectedMarkers = [expectedMarkers] | |
| 258 testObject.step(() => checkExpectedMarkers(expectedMarkers)); | |
| 259 | |
| 260 if (window.testRunner) | |
| 261 window.testRunner.setMockSpellCheckerEnabled(true); | |
| 262 | |
| 263 /** @type {!Sample} */ | |
| 264 const sample = new Sample(inputText); | |
| 265 if (typeof(tester) === 'function') { | |
| 266 tester.call(window, sample.document); | |
| 267 } else if (typeof(tester) === 'string') { | |
| 268 const strings = tester.split(/ (.+)/); | |
| 269 sample.document.execCommand(strings[0], false, strings[1]); | |
| 270 } else { | |
| 271 testObject.step(() => assert_unreached(`Invalid tester: ${tester}`)); | |
| 272 } | |
| 273 | |
| 274 /** @type {number} */ | |
| 275 const maxRetry = 10; | |
|
yosin_UTC9
2016/10/24 06:58:32
s/maxRetry/kMaxRetry/
Xiaocheng
2016/10/24 07:18:56
Done.
| |
| 276 /** @type {number} */ | |
| 277 const retryInterval = 50; | |
|
yosin_UTC9
2016/10/24 06:58:32
s/retryInterval/kRetryInterval/
Xiaocheng
2016/10/24 07:18:56
Done.
| |
| 278 | |
| 279 // TODO(xiaochengh): We should make SpellCheckRequester::didCheck trigger | |
| 280 // something in JavaScript (e.g., a |Promise|), so that we can actively know | |
| 281 // the completion of spellchecking instead of passively waiting for markers to | |
| 282 // appear or disappear. | |
| 283 testObject.step_timeout( | |
| 284 () => verifyMarkers(testObject, sample, expectedMarkers, | |
| 285 maxRetry, retryInterval), | |
| 286 retryInterval); | |
| 287 } | |
| 288 | |
| 289 add_result_callback(testObj => { | |
| 290 if (testObj.properties.isSpellcheckTest !== true) | |
|
yosin_UTC9
2016/10/24 06:58:32
Can we write |!testObj.properties.isSpellCheckTest
Xiaocheng
2016/10/24 07:18:56
Done.
| |
| 291 return; | |
| 292 spellcheckTestRunning = false; | |
| 293 /** @type {Object} */ | |
| 294 const args = testQueue.shift(); | |
| 295 if (args === undefined) | |
| 296 return; | |
| 297 invokeSpellcheckTest(args.inputText, args.tester, | |
| 298 args.expectedMarkers, args.opt_title); | |
| 299 }); | |
| 300 | |
| 301 /** | |
| 302 * @param {string} inputText | |
| 303 * @param {function(!Document)|string} tester | |
| 304 * @param {!Marker|!Array<!Marker>} expectedMarkers | |
| 305 * @param {string=} opt_title | |
| 306 */ | |
| 307 function spellcheckTest(inputText, tester, expectedMarkers, opt_title) { | |
| 308 if (spellcheckTestRunning) { | |
| 309 testQueue.push({ | |
| 310 inputText: inputText, tester: tester, | |
| 311 expectedMarkers: expectedMarkers, opt_title: opt_title}); | |
| 312 return; | |
| 313 } | |
| 314 | |
| 315 invokeSpellcheckTest(inputText, tester, expectedMarkers, opt_title); | |
| 316 } | |
| 317 | |
| 318 // Export symbols | |
| 319 window.Marker = Marker; | |
| 320 window.spellingMarker = spellingMarker; | |
| 321 window.grammarMarker = grammarMarker; | |
| 322 window.spellcheck_test = spellcheckTest; | |
| 323 })(); | |
| OLD | NEW |