Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(92)

Side by Side Diff: third_party/WebKit/LayoutTests/editing/spelling/spellcheck_test.js

Issue 2450733002: Make spellcheck_test easier to use (Closed)
Patch Set: Wed Oct 26 11:19:55 JST 2016 Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « third_party/WebKit/LayoutTests/editing/spelling/spellcheck_test.html ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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, '&amp;')
73 get location() { return this.location_; } 208 .replace(/\u0022/g, '&quot;')
74 209 .replace(/\u0027/g, '&apos;');
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 })();
OLDNEW
« no previous file with comments | « third_party/WebKit/LayoutTests/editing/spelling/spellcheck_test.html ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698