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

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

Issue 2649853002: SAMPLE: Copy files (Closed)
Patch Set: 2017-01-23T17:13:41 Created 3 years, 10 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 | « gin/runner_renamed.cc ('k') | third_party/WebKit/Source/core/editing/EditingStyleUtilities.h » ('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, options)|
8 // assertion 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 // |options| is a string as description, undefined, or a dictionary containing:
22 // description: A description
23 // dumpAs: 'domtree' or 'flattree'. Default is 'domtree'.
24 // removeSampleIfSucceeded: A boolean. Default is true.
25 //
26 // Example:
27 // test(() => {
28 // assert_selection(
29 // '|foo',
30 // (selection) => selection.modify('extent', 'forward, 'character'),
31 // '<a href="http://bar">^f|oo</a>'
32 // });
33 //
34 // test(() => {
35 // assert_selection(
36 // 'x^y|z',
37 // 'bold', // execCommand name as a test
38 // 'x<b>y</b>z',
39 // 'Insert B tag');
40 // });
41 //
42 // test(() => {
43 // assert_selection(
44 // 'x^y|z',
45 // 'createLink http://foo', // execCommand name and parameter
46 // 'x<a href="http://foo/">y</a></b>z',
47 // 'Insert B tag');
48 // });
49 //
50 //
51
52 // TODO(yosin): Please use "clang-format -style=Chromium -i" for formatting
53 // this file.
54
55 (function() {
56 /** @enum{string} */
57 const DumpAs = {
58 DOM_TREE: 'domtree',
59 FLAT_TREE: 'flattree',
60 };
61
62 /** @const @type {string} */
63 const kTextArea = 'TEXTAREA';
64
65 class Traversal {
66 /**
67 * @param {!Node} node
68 * @return {Node}
69 */
70 firstChildOf(node) { throw new Error('You should implement firstChildOf'); }
71
72 /**
73 * @param {!Node} node
74 * @return {!Generator<Node>}
75 */
76 * childNodesOf(node) {
77 for (let child = this.firstChildOf(node); child !== null;
78 child = this.nextSiblingOf(child)) {
79 yield child;
80 }
81 }
82
83 /**
84 * @param {!DOMSelection} selection
85 * @return !SampleSelection
86 */
87 fromDOMSelection(selection) {
88 throw new Error('You should implement fromDOMSelection');
89 }
90
91 /**
92 * @param {!Node} node
93 * @return {Node}
94 */
95 nextSiblingOf(node) {
96 throw new Error('You should implement nextSiblingOf');
97 }
98 }
99
100 class DOMTreeTraversal extends Traversal {
101 /**
102 * @override
103 * @param {!Node} node
104 * @return {Node}
105 */
106 firstChildOf(node) { return node.firstChild; }
107
108 /**
109 * @param {!DOMSelection} selection
110 * @return !SampleSelection
111 */
112 fromDOMSelection(selection) {
113 return SampleSelection.fromDOMSelection(selection);
114 }
115
116 /**
117 * @param {!Node} node
118 * @return {Node}
119 */
120 nextSiblingOf(node) { return node.nextSibling; }
121 };
122
123 class FlatTreeTraversal extends Traversal {
124 /**
125 * @override
126 * @param {!Node} node
127 * @return {Node}
128 */
129 firstChildOf(node) { return internals.firstChildInFlatTree(node); }
130
131 /**
132 * @param {!DOMSelection} selection
133 * @return !SampleSelection
134 */
135 fromDOMSelection(selection) {
136 // TODO(yosin): We should return non-scoped selection rather than
137 // selection
138 // scoped in main tree.
139 return SampleSelection.fromDOMSelection(selection);
140 }
141
142 /**
143 * @param {!Node} node
144 * @return {Node}
145 */
146 nextSiblingOf(node) { return internals.nextSiblingInFlatTree(node); }
147 }
148
149 /**
150 * @param {!Node} node
151 * @return {boolean}
152 */
153 function isCharacterData(node) {
154 return node.nodeType === Node.TEXT_NODE ||
155 node.nodeType === Node.COMMENT_NODE;
156 }
157
158 /**
159 * @param {!Node} node
160 * @return {boolean}
161 */
162 function isElement(node) { return node.nodeType === Node.ELEMENT_NODE; }
163
164 /**
165 * @param {!Node} node
166 * @param {number} offset
167 */
168 function checkValidNodeAndOffset(node, offset) {
169 if (!node)
170 throw new Error('Node parameter should not be a null.');
171 if (offset < 0)
172 throw new Error(`Assumes ${offset} >= 0`);
173 if (isElement(node)) {
174 if (offset > node.childNodes.length)
175 throw new Error(`Bad offset ${offset} for ${node}`);
176 return;
177 }
178 if (isCharacterData(node)) {
179 if (offset > node.nodeValue.length)
180 throw new Error(`Bad offset ${offset} for ${node}`);
181 return;
182 }
183 throw new Error(`Invalid node: ${node}`);
184 }
185
186 class SampleSelection {
187 /** @public */
188 constructor() {
189 /** @type {?Node} */
190 this.anchorNode_ = null;
191 /** @type {number} */
192 this.anchorOffset_ = 0;
193 /** @type {?Node} */
194 this.focusNode_ = null;
195 /** @type {number} */
196 this.focusOffset_ = 0;
197 /** @type {HTMLElement} */
198 this.shadowHost_ = null;
199 }
200
201 /**
202 * @public
203 * @param {!Node} node
204 * @param {number} offset
205 */
206 collapse(node, offset) {
207 checkValidNodeAndOffset(node, offset);
208 this.anchorNode_ = this.focusNode_ = node;
209 this.anchorOffset_ = this.focusOffset_ = offset;
210 }
211
212 /**
213 * @public
214 * @param {!Node} node
215 * @param {number} offset
216 */
217 extend(node, offset) {
218 checkValidNodeAndOffset(node, offset);
219 this.focusNode_ = node;
220 this.focusOffset_ = offset;
221 }
222
223 /** @public @return {?Node} */
224 get anchorNode() {
225 console.assert(!this.isNone, 'Selection should not be a none.');
226 return this.anchorNode_;
227 }
228 /** @public @return {number} */
229 get anchorOffset() {
230 console.assert(!this.isNone, 'Selection should not be a none.');
231 return this.anchorOffset_;
232 }
233 /** @public @return {?Node} */
234 get focusNode() {
235 console.assert(!this.isNone, 'Selection should not be a none.');
236 return this.focusNode_;
237 }
238 /** @public @return {number} */
239 get focusOffset() {
240 console.assert(!this.isNone, 'Selection should not be a none.');
241 return this.focusOffset_;
242 }
243
244 /** @public @return {HTMLElement} */
245 get shadowHost() { return this.shadowHost_; }
246
247 /**
248 * @public
249 * @return {boolean}
250 */
251 get isCollapsed() {
252 return this.anchorNode === this.focusNode &&
253 this.anchorOffset === this.focusOffset;
254 }
255
256 /**
257 * @public
258 * @return {boolean}
259 */
260 get isNone() { return this.anchorNode_ === null; }
261
262 /**
263 * @public
264 * @param {!Selection} domSelection
265 * @return {!SampleSelection}
266 */
267 static fromDOMSelection(domSelection) {
268 /** type {!SampleSelection} */
269 const selection = new SampleSelection();
270 selection.anchorNode_ = domSelection.anchorNode;
271 selection.anchorOffset_ = domSelection.anchorOffset;
272 selection.focusNode_ = domSelection.focusNode;
273 selection.focusOffset_ = domSelection.focusOffset;
274
275 if (selection.anchorNode_ === null)
276 return selection;
277
278 const document = selection.anchorNode_.ownerDocument;
279 selection.shadowHost_ = (() => {
280 if (!document.activeElement)
281 return null;
282 if (document.activeElement.nodeName !== kTextArea)
283 return null;
284 const selectedNode =
285 selection.anchorNode.childNodes[selection.anchorOffset];
286 if (document.activeElement !== selectedNode)
287 return null;
288 return selectedNode;
289 })();
290 return selection;
291 }
292
293 /** @override */
294 toString() {
295 if (this.isNone)
296 return 'SampleSelection()';
297 if (this.isCollapsed)
298 return `SampleSelection(${this.focusNode_}@${this.focusOffset_})`;
299 return `SampleSelection(anchor: ${this.anchorNode_}@${this.anchorOffset_
300 }` +
301 `focus: ${this.focusNode_}@${this.focusOffset_}`;
302 }
303 }
304
305 // Extracts selection from marker "^" as anchor and "|" as focus from
306 // DOM tree and removes them.
307 class Parser {
308 /** @private */
309 constructor() {
310 /** @type {?Node} */
311 this.anchorNode_ = null;
312 /** @type {number} */
313 this.anchorOffset_ = 0;
314 /** @type {?Node} */
315 this.focusNode_ = null;
316 /** @type {number} */
317 this.focusOffset_ = 0;
318 }
319
320 /**
321 * @public
322 * @return {!SampleSelection}
323 */
324 get selection() {
325 const selection = new SampleSelection();
326 if (!this.anchorNode_ && !this.focusNode_)
327 return selection;
328 if (this.anchorNode_ && this.focusNode_) {
329 selection.collapse(this.anchorNode_, this.anchorOffset_);
330 selection.extend(this.focusNode_, this.focusOffset_);
331 return selection;
332 }
333 if (this.focusNode_) {
334 selection.collapse(this.focusNode_, this.focusOffset_);
335 return selection;
336 }
337 throw new Error('There is no focus marker');
338 }
339
340 /**
341 * @private
342 * @param {!CharacterData} node
343 * @param {number} nodeIndex
344 */
345 handleCharacterData(node, nodeIndex) {
346 /** @type {string} */
347 const text = node.nodeValue;
348 /** @type {number} */
349 const anchorOffset = text.indexOf('^');
350 /** @type {number} */
351 const focusOffset = text.indexOf('|');
352 /** @type {!Node} */
353 const parentNode = node.parentNode;
354 node.nodeValue = text.replace('^', '').replace('|', '');
355 if (node.nodeValue.length == 0) {
356 if (anchorOffset >= 0)
357 this.rememberSelectionAnchor(parentNode, nodeIndex);
358 if (focusOffset >= 0)
359 this.rememberSelectionFocus(parentNode, nodeIndex);
360 node.remove();
361 return;
362 }
363 if (anchorOffset >= 0 && focusOffset >= 0) {
364 if (anchorOffset > focusOffset) {
365 this.rememberSelectionAnchor(node, anchorOffset - 1);
366 this.rememberSelectionFocus(node, focusOffset);
367 return;
368 }
369 this.rememberSelectionAnchor(node, anchorOffset);
370 this.rememberSelectionFocus(node, focusOffset - 1);
371 return;
372 }
373 if (anchorOffset >= 0) {
374 this.rememberSelectionAnchor(node, anchorOffset);
375 return;
376 }
377 if (focusOffset < 0)
378 return;
379 this.rememberSelectionFocus(node, focusOffset);
380 }
381
382 /**
383 * @private
384 * @param {!Element} element
385 */
386 handleElementNode(element) {
387 /** @type {number} */
388 let childIndex = 0;
389 for (const child of Array.from(element.childNodes)) {
390 this.parseInternal(child, childIndex);
391 if (!child.parentNode)
392 continue;
393 ++childIndex;
394 }
395 }
396
397 /**
398 * @private
399 * @param {!Node} node
400 * @return {!SampleSelection}
401 */
402 parse(node) {
403 this.parseInternal(node, 0);
404 return this.selection;
405 }
406
407 /**
408 * @private
409 * @param {!Node} node
410 * @param {number} nodeIndex
411 */
412 parseInternal(node, nodeIndex) {
413 if (isElement(node))
414 return this.handleElementNode(node);
415 if (isCharacterData(node))
416 return this.handleCharacterData(node, nodeIndex);
417 throw new Error(`Unexpected node ${node}`);
418 }
419
420 /**
421 * @private
422 * @param {!Node} node
423 * @param {number} offset
424 */
425 rememberSelectionAnchor(node, offset) {
426 checkValidNodeAndOffset(node, offset);
427 console.assert(
428 this.anchorNode_ === null, 'Anchor marker should be one.',
429 this.anchorNode_, this.anchorOffset_);
430 this.anchorNode_ = node;
431 this.anchorOffset_ = offset;
432 }
433
434 /**
435 * @private
436 * @param {!Node} node
437 * @param {number} offset
438 */
439 rememberSelectionFocus(node, offset) {
440 checkValidNodeAndOffset(node, offset);
441 console.assert(
442 this.focusNode_ === null, 'Focus marker should be one.',
443 this.focusNode_, this.focusOffset_);
444 this.focusNode_ = node;
445 this.focusOffset_ = offset;
446 }
447
448 /**
449 * @public
450 * @param {!Node} node
451 * @return {!SampleSelection}
452 */
453 static parse(node) { return (new Parser()).parse(node); }
454 }
455
456 // TODO(yosin): Once we can import JavaScript file from scripts, we should
457 // import "imported/wpt/html/resources/common.js", since |HTML5_VOID_ELEMENTS|
458 // is defined in there.
459 /**
460 * @const @type {!Set<string>}
461 * only void (without end tag) HTML5 elements
462 */
463 const HTML5_VOID_ELEMENTS = new Set([
464 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input',
465 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
466 ]);
467
468 class Serializer {
469 /**
470 * @public
471 * @param {!SampleSelection} selection
472 * @param {!Traversal} traversal
473 */
474 constructor(selection, traversal) {
475 /** @type {!SampleSelection} */
476 this.selection_ = selection;
477 /** @type {!Array<strings>} */
478 this.strings_ = [];
479 /** @type {!Traversal} */
480 this.traversal_ = traversal;
481 }
482
483 /**
484 * @private
485 * @param {string} string
486 */
487 emit(string) { this.strings_.push(string); }
488
489 /**
490 * @private
491 * @param {!HTMLElement} parentNode
492 * @param {number} childIndex
493 */
494 handleSelection(parentNode, childIndex) {
495 if (this.selection_.isNone)
496 return;
497 if (this.selection_.shadowHost)
498 return;
499 if (parentNode === this.selection_.focusNode &&
500 childIndex === this.selection_.focusOffset) {
501 this.emit('|');
502 return;
503 }
504 if (parentNode === this.selection_.anchorNode &&
505 childIndex === this.selection_.anchorOffset) {
506 this.emit('^');
507 }
508 }
509
510 /**
511 * @private
512 * @param {!CharacterData} node
513 */
514 handleCharacterData(node) {
515 /** @type {string} */
516 const text = node.nodeValue;
517 if (this.selection_.isNone)
518 return this.emit(text);
519 /** @type {number} */
520 const anchorOffset = this.selection_.anchorOffset;
521 /** @type {number} */
522 const focusOffset = this.selection_.focusOffset;
523 if (node === this.selection_.focusNode &&
524 node === this.selection_.anchorNode) {
525 if (anchorOffset === focusOffset) {
526 this.emit(text.substr(0, focusOffset));
527 this.emit('|');
528 this.emit(text.substr(focusOffset));
529 return;
530 }
531 if (anchorOffset < focusOffset) {
532 this.emit(text.substr(0, anchorOffset));
533 this.emit('^');
534 this.emit(text.substr(anchorOffset, focusOffset - anchorOffset));
535 this.emit('|');
536 this.emit(text.substr(focusOffset));
537 return;
538 }
539 this.emit(text.substr(0, focusOffset));
540 this.emit('|');
541 this.emit(text.substr(focusOffset, anchorOffset - focusOffset));
542 this.emit('^');
543 this.emit(text.substr(anchorOffset));
544 return;
545 }
546 if (node === this.selection_.anchorNode) {
547 this.emit(text.substr(0, anchorOffset));
548 this.emit('^');
549 this.emit(text.substr(anchorOffset));
550 return;
551 }
552 if (node === this.selection_.focusNode) {
553 this.emit(text.substr(0, focusOffset));
554 this.emit('|');
555 this.emit(text.substr(focusOffset));
556 return;
557 }
558 this.emit(text);
559 }
560
561 /**
562 * @private
563 * @param {!HTMLElement} element
564 */
565 handleElementNode(element) {
566 /** @type {string} */
567 const tagName = element.tagName.toLowerCase();
568 this.emit(`<${tagName}`);
569 Array.from(element.attributes)
570 .sort((attr1, attr2) => attr1.name.localeCompare(attr2.name))
571 .forEach(attr => {
572 if (attr.value === '')
573 return this.emit(` ${attr.name}`);
574 const value = attr.value.replace(/&/g, '&amp;')
575 .replace(/\u0022/g, '&quot;')
576 .replace(/\u0027/g, '&apos;');
577 this.emit(` ${attr.name}="${value}"`);
578 });
579 this.emit('>');
580 if (element.nodeName === kTextArea)
581 return this.handleTextArea(element);
582 if (this.traversal_.firstChildOf(element) === null &&
583 HTML5_VOID_ELEMENTS.has(tagName)) {
584 return;
585 }
586 this.serializeChildren(element);
587 this.emit(`</${tagName}>`);
588 }
589
590 /**
591 * @private
592 * @param {!HTMLTextArea}
593 */
594 handleTextArea(textArea) {
595 /** @type {string} */
596 const value = textArea.value;
597 if (this.selection_.shadowHost !== textArea) {
598 this.emit(value);
599 } else {
600 /** @type {number} */
601 const start = textArea.selectionStart;
602 /** @type {number} */
603 const end = textArea.selectionEnd;
604 /** @type {boolean} */
605 const isBackward =
606 start < end && textArea.selectionDirection === 'backward';
607 const startMarker = isBackward ? '|' : '^';
608 const endMarker = isBackward ? '^' : '|';
609 this.emit(value.substr(0, start));
610 if (start < end) {
611 this.emit(startMarker);
612 this.emit(value.substr(start, end - start));
613 }
614 this.emit(endMarker);
615 this.emit(value.substr(end));
616 }
617 this.emit('</textarea>');
618 }
619
620 /**
621 * @public
622 * @param {!HTMLDocument} document
623 */
624 serialize(document) {
625 if (document.body)
626 this.serializeChildren(document.body);
627 else
628 this.serializeInternal(document.documentElement);
629 return this.strings_.join('');
630 }
631
632 /**
633 * @private
634 * @param {!HTMLElement} element
635 */
636 serializeChildren(element) {
637 if (this.traversal_.firstChildOf(element) === null) {
638 this.handleSelection(element, 0);
639 return;
640 }
641
642 /** @type {number} */
643 let childIndex = 0;
644 for (let child of this.traversal_.childNodesOf(element)) {
645 this.handleSelection(element, childIndex);
646 this.serializeInternal(child, childIndex);
647 ++childIndex;
648 }
649 this.handleSelection(element, childIndex);
650 }
651
652 /**
653 * @private
654 * @param {!Node} node
655 */
656 serializeInternal(node) {
657 if (isElement(node))
658 return this.handleElementNode(node);
659 if (isCharacterData(node))
660 return this.handleCharacterData(node);
661 throw new Error(`Unexpected node ${node}`);
662 }
663 }
664
665 /**
666 * @this {!DOMSelection}
667 * @param {string} html
668 * @param {string=} opt_text
669 */
670 function setClipboardData(html, opt_text) {
671 assert_not_equals(
672 window.internals, undefined,
673 'This test requests clipboard access from JavaScript.');
674 function computeTextData() {
675 if (opt_text !== undefined)
676 return opt_text;
677 const element = document.createElement('div');
678 element.innerHTML = html;
679 return element.textContent;
680 }
681 function copyHandler(event) {
682 const clipboardData = event.clipboardData;
683 clipboardData.setData('text/plain', computeTextData());
684 clipboardData.setData('text/html', html);
685 event.preventDefault();
686 }
687 document.addEventListener('copy', copyHandler);
688 document.execCommand('copy');
689 document.removeEventListener('copy', copyHandler);
690 }
691
692 class Sample {
693 /**
694 * @public
695 * @param {string} sampleText
696 */
697 constructor(sampleText) {
698 /** @const @type {!HTMLIFame} */
699 this.iframe_ = document.createElement('iframe');
700 if (!document.body)
701 document.body = document.createElement('body');
702 document.body.appendChild(this.iframe_);
703 /** @const @type {!HTMLDocument} */
704 this.document_ = this.iframe_.contentDocument;
705 /** @const @type {!Selection} */
706 this.selection_ = this.iframe_.contentWindow.getSelection();
707 this.selection_.document = this.document_;
708 this.selection_.document.offsetLeft = this.iframe_.offsetLeft;
709 this.selection_.document.offsetTop = this.iframe_.offsetTop;
710 this.selection_.setClipboardData = setClipboardData;
711
712 // Set focus to sample IFRAME to make |eventSender| and
713 // |testRunner.execCommand()| to work on sample rather than main frame.
714 this.iframe_.focus();
715 this.load(sampleText);
716 }
717
718 /** @return {!HTMLDocument} */
719 get document() { return this.document_; }
720
721 /** @return {!Selection} */
722 get selection() { return this.selection_; }
723
724 /**
725 * @private
726 * @param {string} sampleText
727 */
728 load(sampleText) {
729 const anchorMarker = sampleText.indexOf('^');
730 const focusMarker = sampleText.indexOf('|');
731 if (focusMarker < 0 && anchorMarker >= 0) {
732 throw new Error(
733 `You should specify caret position in "${sampleText}".`);
734 }
735 if (focusMarker != sampleText.lastIndexOf('|')) {
736 throw new Error(
737 `You should have at least one focus marker "|" in "${sampleText
738 }".`);
739 }
740 if (anchorMarker != sampleText.lastIndexOf('^')) {
741 throw new Error(
742 `You should have at most one anchor marker "^" in "${sampleText
743 }".`);
744 }
745 if (anchorMarker >= 0 && focusMarker >= 0 &&
746 (anchorMarker + 1 === focusMarker ||
747 anchorMarker - 1 === focusMarker)) {
748 throw new Error(
749 `You should have focus marker and should not have anchor marker if a nd only if selection is a caret in "${sampleText
750 }".`);
751 }
752 this.document_.body.innerHTML = sampleText;
753 /** @type {!SampleSelection} */
754 const selection = Parser.parse(this.document_.body);
755 if (selection.isNone)
756 return;
757 if (this.loadSelectionInTextArea(selection))
758 return;
759 this.selection_.collapse(selection.anchorNode, selection.anchorOffset);
760 this.selection_.extend(selection.focusNode, selection.focusOffset);
761 }
762
763 /**
764 * @private
765 * @param {!SampleSelection} selection
766 * @return {boolean} Returns true if selection is in TEXTAREA.
767 */
768 loadSelectionInTextArea(selection) {
769 /** @type {Node} */
770 const enclosingNode = selection.anchorNode.parentNode;
771 if (selection.focusNode.parentNode !== enclosingNode)
772 return false;
773 if (enclosingNode.nodeName !== kTextArea)
774 return false;
775 if (selection.anchorNode !== selection.focusNode)
776 throw new Error('Selection in TEXTAREA should be in same Text node.');
777 enclosingNode.focus();
778 if (selection.anchorOffset < selection.focusOffset) {
779 enclosingNode.setSelectionRange(
780 selection.anchorOffset, selection.focusOffset);
781 return true;
782 }
783 enclosingNode.setSelectionRange(
784 selection.focusOffset, selection.anchorOffset, 'backward');
785 return true;
786 }
787
788 /**
789 * @public
790 */
791 remove() { this.iframe_.remove(); }
792
793 /**
794 * @public
795 * @param {!Traversal} traversal
796 * @return {string}
797 */
798 serialize(traversal) {
799 /** @type {!SampleSelection} */
800 const selection = traversal.fromDOMSelection(this.selection_);
801 /** @type {!Serializer} */
802 const serializer = new Serializer(selection, traversal);
803 return serializer.serialize(this.document_);
804 }
805 }
806
807 function assembleDescription() {
808 function getStack() {
809 let stack;
810 try {
811 throw new Error('get line number');
812 } catch (error) {
813 stack = error.stack.split('\n').slice(1);
814 }
815 return stack
816 }
817
818 const RE_IN_ASSERT_SELECTION = new RegExp('assert_selection\\.js');
819 for (const line of getStack()) {
820 const match = RE_IN_ASSERT_SELECTION.exec(line);
821 if (!match) {
822 const RE_LAYOUTTESTS = new RegExp('LayoutTests.*');
823 return RE_LAYOUTTESTS.exec(line);
824 }
825 }
826 return '';
827 }
828
829 /**
830 * @param {string} expectedText
831 */
832 function checkExpectedText(expectedText) {
833 /** @type {number} */
834 const anchorOffset = expectedText.indexOf('^');
835 /** @type {number} */
836 const focusOffset = expectedText.indexOf('|');
837 if (anchorOffset != expectedText.lastIndexOf('^')) {
838 throw new Error(
839 `You should have at most one anchor marker "^" in "${expectedText
840 }".`);
841 }
842 if (focusOffset != expectedText.lastIndexOf('|')) {
843 throw new Error(
844 `You should have at most one focus marker "|" in "${expectedText}".`);
845 }
846 if (anchorOffset >= 0 && focusOffset < 0) {
847 throw new Error(
848 `You should have a focus marker "|" in "${expectedText}".`);
849 }
850 if (anchorOffset >= 0 && focusOffset >= 0 &&
851 (anchorOffset + 1 === focusOffset ||
852 anchorOffset - 1 === focusOffset)) {
853 throw new Error(
854 `You should have focus marker and should not have anchor marker if and only if selection is a caret in "${expectedText
855 }".`);
856 }
857 }
858
859 /**
860 * @param {string} str1
861 * @param {string} str2
862 * @return {string}
863 */
864 function commonPrefixOf(str1, str2) {
865 for (let index = 0; index < str1.length; ++index) {
866 if (str1[index] !== str2[index])
867 return str1.substr(0, index);
868 }
869 return str1;
870 }
871
872 /**
873 * @param {string} inputText
874 * @param {function(!Selection)|string}
875 * @param {string} expectedText
876 * @param {Object=} opt_options
877 * @return {!Sample}
878 */
879 function assertSelection(inputText, tester, expectedText, opt_options = {}) {
880 const kDescription = 'description';
881 const kDumpAs = 'dumpAs';
882 const kRemoveSampleIfSucceeded = 'removeSampleIfSucceeded';
883 /** @type {!Object} */
884 const options = typeof(opt_options) === 'string' ?
885 {description: opt_options} :
886 opt_options;
887 /** @type {string} */
888 const description =
889 kDescription in options ? options[kDescription] : assembleDescription();
890 /** @type {boolean} */
891 const removeSampleIfSucceeded = kRemoveSampleIfSucceeded in options ?
892 !!options[kRemoveSampleIfSucceeded] :
893 true;
894 /** @type {DumpAs} */
895 const dumpAs = options[kDumpAs] || DumpAs.DOM_TREE;
896
897 checkExpectedText(expectedText);
898 const sample = new Sample(inputText);
899 if (typeof(tester) === 'function') {
900 tester.call(window, sample.selection);
901 } else if (typeof(tester) === 'string') {
902 const strings = tester.split(/ (.+)/);
903 sample.document.execCommand(strings[0], false, strings[1]);
904 } else {
905 throw new Error(`Invalid tester: ${tester}`);
906 }
907
908 /** @type {!Traversal} */
909 const traversal = (() => {
910 switch (dumpAs) {
911 case DumpAs.DOM_TREE:
912 return new DOMTreeTraversal();
913 case DumpAs.FLAT_TREE:
914 if (!window.internals)
915 throw new Error('This test requires window.internals.');
916 return new FlatTreeTraversal();
917 default:
918 throw `${kDumpAs} must be one of ` +
919 `{${Object.values(DumpAs).join(', ')}}` +
920 ` instead of '${dumpAs}'`;
921 }
922 })();
923
924 /** @type {string} */
925 const actualText = sample.serialize(traversal);
926 // We keep sample HTML when assertion is false for ease of debugging test
927 // case.
928 if (actualText === expectedText) {
929 if (removeSampleIfSucceeded)
930 sample.remove();
931 return sample;
932 }
933 throw new Error(
934 `${description}\n` +
935 `\t expected ${expectedText},\n` +
936 `\t but got ${actualText},\n` +
937 `\t sameupto ${commonPrefixOf(expectedText, actualText)}`);
938 }
939
940 // Export symbols
941 window.Sample = Sample;
942 window.assert_selection = assertSelection;
943 })();
OLDNEW
« no previous file with comments | « gin/runner_renamed.cc ('k') | third_party/WebKit/Source/core/editing/EditingStyleUtilities.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698