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

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-26T19:25:56 Created 4 years, 6 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} domSelection
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 node.remove();
243 return;
244 }
245 if (anchorOffset >= 0 && focusOffset >= 0) {
246 if (anchorOffset > focusOffset) {
247 this.rememberSelectionAnchor(node, anchorOffset - 1);
248 this.rememberSelectionFocus(node, focusOffset);
249 return;
250 }
251 this.rememberSelectionAnchor(node, anchorOffset);
252 this.rememberSelectionFocus(node, focusOffset - 1);
253 return;
254 }
255 if (anchorOffset >= 0) {
256 this.rememberSelectionAnchor(node, anchorOffset);
257 return;
258 }
259 if (focusOffset < 0)
260 return;
261 this.rememberSelectionFocus(node, focusOffset);
262 }
263
264 /**
265 * @private
266 * @param {!Element} element
267 */
268 handleElementNode(element) {
269 /** @type {number} */
270 let childIndex = 0;
271 for (const child of Array.from(element.childNodes)) {
272 this.parseInternal(child, childIndex);
273 if (!child.parentNode)
274 continue;
275 ++childIndex;
276 }
277 }
278
279 /**
280 * @private
281 * @param {!Node} node
282 * @return {!SampleSelection}
283 */
284 parse(node) {
285 this.parseInternal(node, 0);
286 return this.selection;
287 }
288
289 /**
290 * @private
291 * @param {!Node} node
292 * @param {number} nodeIndex
293 */
294 parseInternal(node, nodeIndex) {
295 if (isElement(node))
296 return this.handleElementNode(node);
297 if (isCharacterData(node))
298 return this.handleCharacterData(node, nodeIndex);
299 throw new Error(`Unexpected node ${node}`);
300 }
301
302 /**
303 * @private
304 * @param {!Node} node
305 * @param {number} offset
306 */
307 rememberSelectionAnchor(node, offset) {
308 checkValidNodeAndOffset(node, offset);
309 console.assert(
310 this.anchorNode_ === null, 'Anchor marker should be one.',
311 this.anchorNode_, this.anchorOffset_);
312 this.anchorNode_ = node;
313 this.anchorOffset_ = offset;
314 }
315
316 /**
317 * @private
318 * @param {!Node} node
319 * @param {number} offset
320 */
321 rememberSelectionFocus(node, offset) {
322 checkValidNodeAndOffset(node, offset);
323 console.assert(
324 this.focusNode_ === null, 'Focus marker should be one.',
325 this.focusNode_, this.focusOffset_);
326 this.focusNode_ = node;
327 this.focusOffset_ = offset;
328 }
329
330 /**
331 * @public
332 * @param {!Node} node
333 * @return {!SampleSelection}
334 */
335 static parse(node) { return (new Parser()).parse(node); }
336 }
337
338 // TODO(yosin): Once we can import JavaScript file from scripts, we should
339 // import "imported/wpt/html/resources/common.js", since |HTML5_VOID_ELEMENTS|
340 // is defined in there.
341 /**
342 * @const @type {!Set<string>}
343 * only void (without end tag) HTML5 elements
344 */
345 const HTML5_VOID_ELEMENTS = new Set([
346 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input',
347 'keygen', 'link', 'meta', 'param', 'source','track', 'wbr' ]);
348
349 class Serializer {
350 /**
351 * @public
352 * @param {!SampleSelection} selection
353 */
354 constructor(selection) {
355 /** @type {!SampleSelection} */
356 this.selection_ = selection;
357 /** @type {!Array<strings>} */
358 this.strings_ = [];
359 }
360
361 /**
362 * @private
363 * @param {string} string
364 */
365 emit(string) { this.strings_.push(string); }
366
367 /**
368 * @private
369 * @param {!CharacterData} node
370 */
371 handleCharacterData(node) {
372 /** @type {string} */
373 const text = node.nodeValue;
374 /** @type {number} */
375 const anchorOffset = this.selection_.anchorOffset;
376 /** @type {number} */
377 const focusOffset = this.selection_.focusOffset;
378 if (node === this.selection_.focusNode &&
379 node === this.selection_.anchorNode) {
380 if (anchorOffset === focusOffset) {
381 this.emit(text.substr(0, focusOffset));
382 this.emit('|');
383 this.emit(text.substr(focusOffset));
384 return;
385 }
386 if (anchorOffset < focusOffset) {
387 this.emit(text.substr(0, anchorOffset));
388 this.emit('^');
389 this.emit(text.substr(anchorOffset, focusOffset - anchorOffset));
390 this.emit('|');
391 this.emit(text.substr(focusOffset));
392 return;
393 }
394 this.emit(text.substr(0, focusOffset));
395 this.emit('|');
396 this.emit(text.substr(focusOffset, anchorOffset - focusOffset));
397 this.emit('^');
398 this.emit(text.substr(anchorOffset));
399 return;
400 }
401 if (node === this.selection_.anchorNode) {
402 this.emit(text.substr(0, anchorOffset));
403 this.emit('^');
404 this.emit(text.substr(anchorOffset));
405 return;
406 }
407 if (node === this.selection_.focusNode) {
408 this.emit(text.substr(0, focusOffset));
409 this.emit('|');
410 this.emit(text.substr(focusOffset));
411 return;
412 }
413 this.emit(text);
414 }
415
416 /**
417 * @private
418 * @param {!HTMLElement} element
419 * @param {number} nodeIndex
420 */
421 handleElementNode(element, nodeIndex) {
422 if (element.parenNode === this.selection_.focusNode &&
423 nodeIndex === this.selection_.focusOffset) {
424 this.emit('|');
425 } else if (
426 element === this.selection_.anchorNode &&
427 nodeIndex === this.selection_.anchorOffset) {
428 this.emit('^');
429 }
430 /** @type {string} */
431 const tagName = element.tagName.toLowerCase();
432 this.emit(`<${tagName}`);
433 Array.from(element.attributes)
434 .sort((attr1, attr2) => attr1.name.localeCompare(attr2.name))
435 .forEach(attr => {
436 if (attr.value === '')
437 return this.emit(` ${attr.name}`);
438 const value = attr.value.replace(/&/g, '&amp;')
439 .replace(/\u0022/g, '&quot;')
440 .replace(/\u0027/g, '&apos;');
441 this.emit(` ${attr.name}="${value}"`);
442 });
443 this.emit('>');
444 if (element.childNodes.length === 0 &&
445 HTML5_VOID_ELEMENTS.has(tagName)) {
446 return;
447 }
448 /** @type {number} */
449 let childIndex = 0;
450 for (const child of Array.from(element.childNodes)) {
451 this.serializeInternal(child, childIndex);
452 ++childIndex;
453 }
454 this.emit(`</${tagName}>`);
455 }
456
457 /**
458 * @public
459 * @param {!HTMLElement} element
460 */
461 serialize(element) {
462 if (this.selection_.isNone)
463 return node.outerHTML;
464 this.serializeInternal(element, 0);
465 return this.strings_.join('');
466 }
467
468 /**
469 * @private
470 * @param {!Node} node
471 * @param {number} nodeIndex
472 */
473 serializeInternal(node, nodeIndex) {
474 if (isElement(node))
475 return this.handleElementNode(node, nodeIndex);
476 if (isCharacterData(node))
477 return this.handleCharacterData(node);
478 throw new Error(`Unexpected node ${node}`);
479 }
480 }
481
482 class Sample {
483 /**
484 * @public
485 * @param {string} sampleText
486 */
487 constructor(sampleText) {
488 /** @const @type {!HTMLIFame} */
489 this.iframe_ = document.createElement('iframe');
490 document.body.appendChild(this.iframe_);
491 /** @const @type {!HTMLDocument} */
492 this.document_ = this.iframe_.contentDocument;
493 /** @const @type {!Selection} */
494 this.selection_ = this.iframe_.contentWindow.getSelection();
495 this.selection_.document = this.document_;
496
497 this.load(sampleText);
498 }
499
500 /** @return {!HTMLDocument} */
501 get document() { return this.document_; }
502
503 /** @return {!Selection} */
504 get selection() { return this.selection_; }
505
506 /**
507 * @private
508 * @param {string} sampleText
509 */
510 load(sampleText) {
511 const anchorMarker = sampleText.indexOf('^');
512 const focusMarker = sampleText.indexOf('|');
513 if (focusMarker < 0) {
514 throw new Error(`You should specify caret position in "${sampleText}".`);
515 }
516 if (focusMarker != sampleText.lastIndexOf('|')) {
517 throw new Error(
518 `You should have at least one focus marker "|" in "${sampleText}".`);
519 }
520 if (anchorMarker != sampleText.lastIndexOf('^')) {
521 throw new Error(
522 `You should have at most one anchor marker "^" in "${sampleText}".`);
523 }
524 this.document_.body.innerHTML = sampleText;
525 /** @type {!SampleSelection} */
526 const selection = Parser.parse(this.document_.body);
527 if (selection.isNone)
528 return;
529 this.selection_.collapse(selection.anchorNode, selection.anchorOffset);
530 this.selection_.extend(selection.focusNode, selection.focusOffset);
531 }
532
533 /**
534 * @public
535 */
536 remove() { this.iframe_.remove(); }
537
538 /**
539 * @public
540 * @return {string}
541 */
542 serialize() {
543 /** @type {!SampleSelection} */
544 const selection = SampleSelection.fromDOMSelection(this.selection_);
545 /** @type {!Serializer} */
546 const serializer = new Serializer(selection);
547 return serializer.serialize(this.document_.body.firstChild);
548 }
549 }
550
551 function assembleDescription() {
552 const RE_TEST_FUNCTION =
553 new RegExp('at Object.test \\(.*?([^/]+?):(\\d+):(\\d+)\\)');
554 function getStack() {
555 let stack;
556 try {
557 throw new Error('get line number');
558 } catch (error) {
559 stack = error.stack.split('\n').slice(1);
560 }
561 return stack
562 }
563 for (const line of getStack()) {
564 const match = RE_TEST_FUNCTION.exec(line);
565 if (!match)
566 continue;
567 return `${match[1]}(${match[2]})`;
568 }
569 return '';
570 }
571
572 /**
573 * @param {string} inputText
574 * @param {function(!Selection)|string}
575 * @param {string} expectedText
576 * @param {string=} opt_description
577 */
578 function assertSelection(
579 inputText, tester, expectedText, opt_description = '') {
580 /** @type {string} */
581 const description =
582 opt_description === '' ? assembleDescription() : opt_description;
583 if (expectedText.indexOf('^') != expectedText.lastIndexOf('^')) {
584 throw new Error(
585 `You should have at most one anchor marker "^" in "${expectedText}".`) ;
586 }
587 if (expectedText.indexOf('|') != expectedText.lastIndexOf('|')) {
588 throw new Error(
589 `You should have at most one focus marker "|" in "${expectedText}".`);
590 }
591 const sample = new Sample(inputText);
592 if (typeof(tester) === 'function') {
593 tester.call(window, sample.selection);
594 } else if (typeof(tester) === 'string') {
595 const strings = tester.split(' ');
596 sample.document.execCommand(strings[0], false, strings[1]);
597 } else {
598 throw new Error(`Invalid tester: ${tester}`);
599 }
600 /** @type {string} */
601 const actualText = sample.serialize();
602 // We keep sample HTML when assertion is false for ease of debugging test
603 // case.
604 if (actualText == expectedText)
605 sample.remove();
606 assert_equals(actualText, expectedText, description);
607 }
608
609 // Export symbols
610 window.Sample = Sample;
611 window.assert_selection = assertSelection;
612 })();
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