Chromium Code Reviews| 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 |