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

Side by Side Diff: third_party/WebKit/LayoutTests/editing/assert_selection.js

Issue 2013603004: Introduce assert_selection() for editing tests (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: 2016-05-26T16:53:56 Created 4 years, 7 months 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 | « no previous file | third_party/WebKit/LayoutTests/editing/execCommand/createLink.html » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 |assert_selection(sample, tester, expectedText)| assertion
8 // to W3C test harness to write editing test cases easier.
9 //
10 // |sample| is an HTML fragment text which is inserted as |innerHTML|. It should
11 // have at least one focus boundary point marker "|" and at most one anchor
12 // boundary point marker "^".
13 //
14 // |tester| is either name with parameter of execCommand or function taking
15 // one parameter |Selection|.
16 //
17 // |expectedText| is an HTML fragment text containing at most one focus marker
18 // and anchor marker. If resulting selection is none, you don't need to have
19 // anchor and focus markers.
20 //
21 // Example:
22 // test(() => {
23 // assert_selection(
24 // '|foo',
25 // (selection) => selection.modify('extent', 'forward, 'character'),
26 // '<a href="http://bar">^f|oo</a>'
27 // });
28 //
29 // test(() => {
30 // assert_selection(
31 // 'x^y|z',
32 // 'bold', // execCommand name as a test
33 // 'x<b>y</b>z',
34 // 'Insert B tag');
35 // });
36 //
37 // test(() => {
38 // assert_selection(
39 // 'x^y|z',
40 // 'createLink http://foo', // execCommand name and parameter
41 // 'x<a href="http://foo/">y</a></b>z',
42 // 'Insert B tag');
43 // });
44 //
45 //
46
47 // TODO(yosin): Please use "clang-format -style=Chromium -i" for formatting
48 // this file.
49
50 (function() {
51 /**
52 * @param {!Node} node
53 * @return {boolean}
54 */
55 function isCharacterData(node) {
56 return node.nodeType === Node.TEXT_NODE ||
57 node.nodeType === Node.COMMENT_NODE;
58 }
59
60 /**
61 * @param {!Node} node
62 * @return {boolean}
63 */
64 function isElement(node) {
65 return node.nodeType === Node.ELEMENT_NODE;
66 }
67
68 /**
69 * @param {!Node} node
70 * @param {number} offset
71 */
72 function checkValidNodeAndOffset(node, offset) {
73 if (!node)
74 throw new Error('Node parameter should not be a null.');
75 if (offset < 0)
76 throw new Error(`Assumes ${offset} >= 0`);
77 if (isElement(node)) {
78 if (offset > node.childNodes.length)
79 throw new Error(`Bad offset ${offset} for ${node}`);
80 return;
81 }
82 if (isCharacterData(node)) {
83 if (offset > node.nodeValue.length)
84 throw new Error(`Bad offset ${offset} for ${node}`);
85 return;
86 }
87 throw new Error(`Invalid node: ${node}`);
88 }
89
90 class SampleSelection {
91 /** @public */
92 constructor() {
93 /** @type {?Node} */
94 this.anchorNode_ = null;
95 /** @type {number} */
96 this.anchorOffset_ = 0;
97 /** @type {?Node} */
98 this.focusNode_ = null;
99 /** @type {number} */
100 this.focusOffset_ = 0;
101 }
102
103 /**
104 * @public
105 * @param {!Node} node
106 * @param {number} offset
107 */
108 collapse(node, offset) {
109 checkValidNodeAndOffset(node, offset);
110 this.anchorNode_ = this.focusNode_ = node;
111 this.anchorOffset_ = this.focusOffset_ = offset;
112 }
113
114 /**
115 * @public
116 * @param {!Node} node
117 * @param {number} offset
118 */
119 extend(node, offset) {
120 checkValidNodeAndOffset(node, offset);
121 this.focusNode_ = node;
122 this.focusOffset_ = offset;
123 }
124
125 /** @public @return {?Node} */
126 get anchorNode() {
127 console.assert(!this.isNone, 'Selection should not be a none.');
128 return this.anchorNode_;
129 }
130 /** @public @return {number} */
131 get anchorOffset() {
132 console.assert(!this.isNone, 'Selection should not be a none.');
133 return this.anchorOffset_;
134 }
135 /** @public @return {?Node} */
136 get focusNode() {
137 console.assert(!this.isNone, 'Selection should not be a none.');
138 return this.focusNode_;
139 }
140 /** @public @return {number} */
141 get focusOffset() {
142 console.assert(!this.isNone, 'Selection should not be a none.');
143 return this.focusOffset_;
144 }
145
146 /**
147 * @public
148 * @return {boolean}
149 */
150 get isCollapsed() {
151 return this.anchorNode === this.focusNode &&
152 this.anchorOffset === this.focusOffset;
153 }
154
155 /**
156 * @public
157 * @return {boolean}
158 */
159 get isNone() { return this.anchorNode_ === null; }
160
161 /**
162 * @public
163 * @param {!Selection} domSeleciton
164 * @return {!SampleSelection}
165 */
166 static fromDOMSelection(domSelection) {
167 /** type {!SampleSelection} */
168 const selection = new SampleSelection();
169 selection.anchorNode_ = domSelection.anchorNode;
170 selection.anchorOffset_ = domSelection.anchorOffset;
171 selection.focusNode_ = domSelection.focusNode;
172 selection.focusOffset_ = domSelection.focusOffset;
173 return selection;
174 }
175
176 /** @override */
177 toString() {
178 if (this.isNone)
179 return 'SampleSelection()';
180 if (this.isCollapsed)
181 return `SampleSelection(${this.focusNode_}@${this.focusOffset_})`;
182 return `SampleSelection(anchor: ${this.anchorNode_}@${this.anchorOffset_}` +
183 `focus: ${this.focusNode_}@${this.focusOffset_}`;
184 }
185 }
186
187 // Extracts selection from marker "^" as anchor and "|" as focus from
188 // DOM tree and removes them.
189 class Parser {
190 /** @private */
191 constructor() {
192 /** @type {?Node} */
193 this.anchorNode_ = null;
194 /** @type {number} */
195 this.anchorOffset_ = 0;
196 /** @type {?Node} */
197 this.focusNode_ = null;
198 /** @type {number} */
199 this.focusOffset_ = 0;
200 }
201
202 /**
203 * @public
204 * @return {!SampleSelection}
205 */
206 get selection() {
207 const selection = new SampleSelection();
208 if (!this.anchorNode_ && !this.focusNode_)
209 return selection;
210 if (this.anchorNode_ && this.focusNode_) {
211 selection.collapse(this.anchorNode_, this.anchorOffset_);
212 selection.extend(this.focusNode_, this.focusOffset_);
213 return selection;
214 }
215 if (this.focusNode_) {
216 selection.collapse(this.focusNode_, this.focusOffset_);
217 return selection;
218 }
219 throw new Error('There is no focus marker');
220 }
221
222 /**
223 * @private
224 * @param {!CharacterData} node
225 * @param {number} nodeIndex
226 */
227 handleCharacterData(node, nodeIndex) {
228 /** @type {string} */
229 const text = node.nodeValue;
230 /** @type {number} */
231 const anchorOffset = text.indexOf('^');
232 /** @type {number} */
233 const focusOffset = text.indexOf('|');
234 /** @type {!Node} */
235 const parentNode = node.parentNode;
236 node.nodeValue = text.replace('^', '').replace('|', '');
237 if (node.nodeValue.length == 0) {
238 if (anchorOffset >= 0)
239 this.rememberSelectionAnchor(parentNode, nodeIndex);
240 if (focusOffset >= 0)
241 this.rememberSelectionFocus(parentNode, nodeIndex);
242 return;
243 }
244 if (anchorOffset >= 0 && focusOffset >= 0) {
245 if (anchorOffset > focusOffset) {
246 this.rememberSelectionAnchor(node, anchorOffset - 1);
247 this.rememberSelectionFocus(node, focusOffset);
248 return;
249 }
250 this.rememberSelectionAnchor(node, anchorOffset);
251 this.rememberSelectionFocus(node, focusOffset - 1);
252 return;
253 }
254 if (anchorOffset >= 0) {
255 this.rememberSelectionAnchor(node, anchorOffset);
256 return;
257 }
258 if (focusOffset < 0)
259 return;
260 this.rememberSelectionFocus(node, focusOffset);
261 }
262
263 /**
264 * @private
265 * @param {!Element} element
266 */
267 handleElementNode(element) {
268 /** @type {number} */
269 let childIndex = 0;
270 for (const child of Array.from(element.childNodes)) {
271 this.parseInternal(child, childIndex);
272 if (!child.parentNode)
273 continue;
274 ++childIndex;
275 }
276 }
277
278 /**
279 * @private
280 * @param {!Node} node
281 * @return {!SampleSelection}
282 */
283 parse(node) {
284 this.parseInternal(node, 0);
285 return this.selection;
286 }
287
288 /**
289 * @private
290 * @param {!Node} node
291 * @param {number} nodeIndex
292 */
293 parseInternal(node, nodeIndex) {
294 if (isElement(node))
295 return this.handleElementNode(node);
296 if (isCharacterData(node))
297 return this.handleCharacterData(node);
298 throw new Error(`Unexpected node ${node}`);
299 }
300
301 /**
302 * @private
303 * @param {!Node} node
304 * @param {number} offset
305 */
306 rememberSelectionAnchor(node, offset) {
307 checkValidNodeAndOffset(node, offset);
308 console.assert(
309 this.anchorNode_ === null, 'Anchor marker should be one.',
310 this.anchorNode_, this.anchorOffset_);
311 this.anchorNode_ = node;
312 this.anchorOffset_ = offset;
313 }
314
315 /**
316 * @private
317 * @param {!Node} node
318 * @param {number} offset
319 */
320 rememberSelectionFocus(node, offset) {
321 checkValidNodeAndOffset(node, offset);
322 console.assert(
323 this.focusNode_ === null, 'Focus marker should be one.',
324 this.focusNode_, this.focusOffset_);
325 this.focusNode_ = node;
326 this.focusOffset_ = offset;
327 }
328
329 /**
330 * @public
331 * @param {!Node} node
332 * @return {!SampleSelection}
333 */
334 static parse(node) { return (new Parser()).parse(node); }
335 }
336
337 /** @type {!Set<string>} */
338 const END_TAG_OMISSIBLE_NAMES = new Set(['br', 'hr', 'img', 'input', 'wbr']);
tkent 2016/05/26 08:23:14 Can we refer to HTML5_VOID_ELEMENTS defined in imp
339
340 class Serializer {
341 /**
342 * @public
343 * @param {!SampleSelection} selection
344 */
345 constructor(selection) {
346 /** @type {!SampleSelection} */
347 this.selection_ = selection;
348 /** @type {!Array<strings>} */
349 this.strings_ = [];
350 }
351
352 /**
353 * @private
354 * @param {string} string
355 */
356 emit(string) { this.strings_.push(string); }
357
358 /**
359 * @private
360 * @param {!CharacterData} node
361 */
362 handleCharacterData(node) {
363 /** @type {string} */
364 const text = node.nodeValue;
365 /** @type {number} */
366 const anchorOffset = this.selection_.anchorOffset;
367 /** @type {number} */
368 const focusOffset = this.selection_.focusOffset;
369 if (node === this.selection_.focusNode &&
370 node === this.selection_.anchorNode) {
371 if (anchorOffset === focusOffset) {
372 this.emit(text.substr(0, focusOffset));
373 this.emit('|');
374 this.emit(text.substr(focusOffset));
375 return;
376 }
377 if (anchorOffset < focusOffset) {
378 this.emit(text.substr(0, anchorOffset));
379 this.emit('^');
380 this.emit(text.substr(anchorOffset, focusOffset - anchorOffset));
381 this.emit('|');
382 this.emit(text.substr(focusOffset));
383 return;
384 }
385 this.emit(text.substr(0, focusOffset));
386 this.emit('|');
387 this.emit(text.substr(focusOffset, anchorOffset - focusOffset));
388 this.emit('^');
389 this.emit(text.substr(anchorOffset));
390 return;
391 }
392 if (node === this.selection_.anchorNode) {
393 this.emit(text.substr(0, anchorOffset));
394 this.emit('^');
395 this.emit(text.substr(anchorOffset));
396 return;
397 }
398 if (node === this.selection_.focusNode) {
399 this.emit(text.substr(0, focusOffset));
400 this.emit('|');
401 this.emit(text.substr(focusOffset));
402 return;
403 }
404 this.emit(text);
405 }
406
407 /**
408 * @private
409 * @param {!HTMLElement} element
410 */
411 handleElementNode(element) {
412 if (element === this.selection_.focusNode &&
413 nodeIndex === this.selection_.focusOffset) {
414 this.emit('|');
415 } else if (
416 element === this.selection_.anchorNode &&
417 nodeIndex === this.selection_.anchorOffset) {
418 this.emit('^');
419 }
420 /** @type {string} */
421 const tagName = element.tagName.toLowerCase();
422 this.emit(`<${tagName}`);
423 Array.from(element.attributes)
424 .sort((attr1, attr2) => attr1.name.localeCompare(attr2.name))
425 .forEach(attr => {
426 if (attr.value === '')
427 return this.emit(` ${attr.name}`);
428 const value = attr.value.replace(/&/g, '&amp;')
429 .replace(/\u0022/g, '&quot;')
430 .replace(/\u0027/g, '&apos;');
431 this.emit(` ${attr.name}="${value}"`);
432 });
433 this.emit('>');
434 if (element.childNodes.length === 0 &&
435 END_TAG_OMISSIBLE_NAMES.has(tagName)) {
436 return;
437 }
438 /** @type {number} */
439 let childIndex = 0;
440 for (const child of Array.from(element.childNodes)) {
441 this.serializeInternal(child, childIndex);
442 ++childIndex;
443 }
444 this.emit(`</${tagName}>`);
445 }
446
447 /**
448 * @public
449 * @param {!HTMLElement} element
450 */
451 serialize(element) {
452 if (this.selection_.isNone)
453 return node.outerHTML;
454 this.serializeInternal(element, 0);
455 return this.strings_.join('');
456 }
457
458 /**
459 * @private
460 * @param {!Node} node
461 * @param {number} nodeIndex
462 */
463 serializeInternal(node, nodeIndex) {
464 if (isElement(node))
465 return this.handleElementNode(node);
466 if (isCharacterData(node))
467 return this.handleCharacterData(node);
468 throw new Error(`Unexpected node ${node}`);
469 }
470 }
471
472 class Sample {
473 /**
474 * @public
475 * @param {string} sampleText
476 */
477 constructor(sampleText) {
478 /** @const @type {!HTMLIFame} */
479 this.iframe_ = document.createElement('iframe');
480 document.body.appendChild(this.iframe_);
481 /** @const @type {!HTMLDocument} */
482 this.document_ = this.iframe_.contentDocument;
483 /** @const @type {!Selection} */
484 this.selection_ = this.iframe_.contentWindow.getSelection();
485 this.load(sampleText);
486 }
487
488 /** @return {!HTMLDocument} */
489 get document() { return this.document_; }
490
491 /** @return {!Selection} */
492 get selection() { return this.selection_; }
493
494 /**
495 * @private
496 * @param {string} sampleText
497 */
498 load(sampleText) {
499 const anchorMarker = sampleText.indexOf('^');
500 const focusMarker = sampleText.indexOf('|');
501 if (focusMarker < 0) {
502 throw new Error(`You should specify caret position in "${sampleText}".`);
503 }
504 if (focusMarker != sampleText.lastIndexOf('|')) {
505 throw new Error(
506 `You should have at least one focus marker "|" in "${sampleText}".`);
507 }
508 if (anchorMarker != sampleText.lastIndexOf('^')) {
509 throw new Error(
510 `You should have at most one anchor marker "^" in "${sampleText}".`);
511 }
512 this.document_.body.innerHTML = sampleText;
513 /** @type {!SampleSelection} */
514 const selection = Parser.parse(this.document_.body);
515 if (selection.isNone)
516 return;
517 this.selection_.collapse(selection.anchorNode, selection.anchorOffset);
518 this.selection_.extend(selection.focusNode, selection.focusOffset);
519 }
520
521 /**
522 * @public
523 */
524 remove() { this.iframe_.remove(); }
525
526 /**
527 * @public
528 * @return {string}
529 */
530 serialize() {
531 /** @type {!SampleSelection} */
532 const selection = SampleSelection.fromDOMSelection(this.selection_);
533 /** @type {!Serializer} */
534 const serializer = new Serializer(selection);
535 return serializer.serialize(this.document_.body.firstChild);
536 }
537 }
538
539 function assembleDescription() {
540 const RE_TEST_FUNCTION =
541 new RegExp('at Object.test \\(.*?([^/]+?):(\\d+):(\\d+)\\)');
542 function getStack() {
543 let stack;
544 try {
545 throw new Error('get line number');
546 } catch (error) {
547 stack = error.stack.split('\n').slice(1);
548 }
549 return stack
550 }
551 for (const line of getStack()) {
552 const match = RE_TEST_FUNCTION.exec(line);
553 if (!match)
554 continue;
555 return `${match[1]}(${match[2]})`;
556 }
557 return '';
558 }
559
560 /**
561 * @param {string} beforeSample
562 * @param {function(!Selection)|string}
563 * @param {string} expectedText
564 * @param {string=} opt_description
565 */
566 function assertSelection(
567 inputText, tester, expectedText, opt_description = '') {
568 /** @type {string} */
569 const description =
570 opt_description === '' ? assembleDescription() : opt_description;
571 const sample = new Sample(inputText);
572 if (typeof(tester) === 'function') {
573 tester.call(window, sample.selection);
574 } else if (typeof(tester) === 'string') {
575 const strings = tester.split(' ');
576 sample.document.execCommand(strings[0], false, strings[1]);
577 } else {
578 throw new Error(`Invalid tester: ${tester}`);
579 }
580 /** @type {string} */
581 const actualText = sample.serialize();
582 // We keep sample HTML when assertion is false for ease of debugging test
583 // case.
584 if (actualText == expectedText)
585 sample.remove();
586 assert_equals(actualText, expectedText, description);
587 }
588
589 // Export symbols
590 window.Sample = Sample;
591 window.assert_selection = assertSelection;
592 })();
OLDNEW
« no previous file with comments | « no previous file | third_party/WebKit/LayoutTests/editing/execCommand/createLink.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698