| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview Tests for text_decorator.ts. |
| */ |
| |
| import type {TextViewportAnnotation} from '//ios/web/annotations/resources/text_annotation_list.js'; |
| import {TextAnnotationList} from '//ios/web/annotations/resources/text_annotation_list.js'; |
| import {TextDecorator} from '//ios/web/annotations/resources/text_decorator.js'; |
| import {TextChunk, TextSection} from '//ios/web/annotations/resources/text_extractor.js'; |
| import {TextStyler} from '//ios/web/annotations/resources/text_styler.js'; |
| import {expectEq, load, TestSuite} from '//ios/web/annotations/resources/text_test_utils.js'; |
| |
| class InertTextStyler extends TextStyler { |
| override style(_parentNode: Node, _element: HTMLElement, _type: string): |
| void {} |
| } |
| |
| export class TestTextDecorator extends TestSuite { |
| // Mark: Utils |
| |
| annotation( |
| start: number, end: number, text: string, type?: string, |
| data?: string): TextViewportAnnotation { |
| return {start, end, text, type: type ?? 'WORD', data: data ?? '#' + text}; |
| } |
| |
| // Mark: tests |
| |
| // Test case 3 (see text_decorator.ts), the normal flow without collision with |
| // in-flight chunks or mutation by external 3p. |
| // All annotations are limited to one element |
| testTextDecoratorFlowInElement() { |
| const html = '<div id="d1">abcde</div>' + |
| '<div id="d2">fghij</div>' + |
| '<div id="d3">klmno</div>' + |
| '<div id="d4">pqrst</div>' + |
| '<div id="d5">uvwxy</div>'; |
| load(html); |
| const d1 = document.querySelector('#d1')!; |
| const d2 = document.querySelector('#d2')!; |
| const d3 = document.querySelector('#d3')!; |
| const d4 = document.querySelector('#d4')!; |
| const d5 = document.querySelector('#d5')!; |
| |
| // Let's pretend the extracted text is: |
| // 'de fghij klmno pqrst uvw' |
| // 111111111122222 |
| // 0123456789012345678901234 |
| // with a prefix = 'de fg' and suffix is 't uvw'. Offset in first node is 3 |
| // and the visible range of 'h...t' is [5, 20). |
| const sections = [ |
| new TextSection(d1.childNodes[0] as Text, 0), // first section |
| new TextSection(d2.childNodes[0] as Text, 3), |
| new TextSection(d3.childNodes[0] as Text, 9), |
| new TextSection(d4.childNodes[0] as Text, 15), |
| new TextSection(d5.childNodes[0] as Text, 21), |
| ]; |
| const chunk = new TextChunk(3, 5, 20); |
| chunk.add(sections, 'de fghij klmno pqrst uvw'); |
| |
| // Let's pretend the annotation are 'e f', 'ghi', 'mno p', 'r', 't' and |
| // 'vw', where 'e f' and 'vw'are outside the visible range. |
| const list = new TextAnnotationList( |
| [ |
| this.annotation(1, 4, 'e f'), // Should be ignored |
| this.annotation(4, 7, 'ghi'), |
| this.annotation(11, 16, 'mno p'), // Across elements, ignored |
| this.annotation(17, 18, 'r'), this.annotation(19, 20, 't'), |
| this.annotation(22, 24, 'vw'), // Should be ignored |
| ], |
| chunk.visibleStart, chunk.visibleEnd); |
| |
| const styler = new InertTextStyler(); |
| const decorator = new TextDecorator(styler); |
| |
| decorator.decorateChunk(chunk, list); |
| |
| expectEq(3, list.successes); |
| expectEq(3, list.failures); |
| expectEq(3, list.cancelled.length); |
| |
| const decoratedHTML = '<div id="d1">abcde</div>' + |
| '<div id="d2">f<chrome_annotation>ghi</chrome_annotation>j</div>' + |
| '<div id="d3">klmno</div>' + |
| '<div id="d4">pq' + |
| '<chrome_annotation>r</chrome_annotation>s' + |
| '<chrome_annotation>t</chrome_annotation></div>' + |
| '<div id="d5">uvwxy</div>'; |
| expectEq(decoratedHTML, document.body.innerHTML); |
| |
| decorator.removeAllDecorations(); |
| expectEq(html, document.body.innerHTML); |
| } |
| |
| // Test case 3 (see text_decorator.ts), the normal flow without collision with |
| // in-flight chunks or mutation by external 3p. |
| // All annotations are limited to one element |
| testTextDecoratorFlowAcrossElement() { |
| const html = '<div id="d1">abcde</div>' + |
| '<div id="d2">fghij</div>' + |
| '<div id="d3">klmno</div>' + |
| '<div id="d4">pqrst</div>' + |
| '<div id="d5">uvwxy</div>'; |
| load(html); |
| const d1 = document.querySelector('#d1')!; |
| const d2 = document.querySelector('#d2')!; |
| const d3 = document.querySelector('#d3')!; |
| const d4 = document.querySelector('#d4')!; |
| const d5 = document.querySelector('#d5')!; |
| |
| // Let's pretend the extracted text is: |
| // 'de fghij klmno pqrst uvw' |
| // 111111111122222 |
| // 0123456789012345678901234 |
| // with a prefix = 'de fg' and suffix is 't uvw'. Offset in first node is 3 |
| // and the visible range of 'h...t' is [5, 20). |
| const sections = [ |
| new TextSection(d1.childNodes[0] as Text, 0), // first section |
| new TextSection(d2.childNodes[0] as Text, 3), |
| new TextSection(d3.childNodes[0] as Text, 9), |
| new TextSection(d4.childNodes[0] as Text, 15), |
| new TextSection(d5.childNodes[0] as Text, 21), |
| ]; |
| const chunk = new TextChunk(3, 5, 20); |
| chunk.add(sections, 'de fghij klmno pqrst uvw'); |
| |
| // Let's pretend the annotation are 'e f', 'ghi', 'mno p', 'r', 't' and |
| // 'vw', where 'e f' and 'vw'are outside the visible range. |
| const list = new TextAnnotationList( |
| [ |
| this.annotation(1, 4, 'e f', 'address'), // Should be ignored |
| this.annotation(4, 7, 'ghi', 'address'), |
| this.annotation( |
| 11, 16, 'mno p', 'address'), // Across elements, ignored |
| this.annotation(17, 18, 'r', 'address'), |
| this.annotation(19, 20, 't', 'address'), |
| this.annotation(22, 24, 'vw', 'address'), // Should be ignored |
| ], |
| chunk.visibleStart, chunk.visibleEnd); |
| |
| const styler = new InertTextStyler(); |
| const decorator = new TextDecorator(styler); |
| |
| decorator.decorateChunk(chunk, list); |
| |
| expectEq(4, list.successes); |
| expectEq(2, list.failures); |
| expectEq(2, list.cancelled.length); |
| |
| const decoratedHTML = '<div id="d1">abcde</div>' + |
| '<div id="d2">f<chrome_annotation>ghi</chrome_annotation>j</div>' + |
| '<div id="d3">kl<chrome_annotation>mno</chrome_annotation></div>' + |
| '<div id="d4"><chrome_annotation>p</chrome_annotation>q' + |
| '<chrome_annotation>r</chrome_annotation>s' + |
| '<chrome_annotation>t</chrome_annotation></div>' + |
| '<div id="d5">uvwxy</div>'; |
| expectEq(decoratedHTML, document.body.innerHTML); |
| |
| decorator.removeAllDecorations(); |
| expectEq(html, document.body.innerHTML); |
| } |
| |
| testTextDecoratorEdgeCases() { |
| const html = '<div id="d1">abcde</div>' + |
| '<div id="d2">fghij</div>' + |
| '<div id="d3">klmno</div>' + |
| '<div id="d4">pqrst</div>' + |
| '<div id="d5">uvwxy</div>'; |
| load(html); |
| const d1 = document.querySelector('#d1')!; |
| const d2 = document.querySelector('#d2')!; |
| const d3 = document.querySelector('#d3')!; |
| const d4 = document.querySelector('#d4')!; |
| const d5 = document.querySelector('#d5')!; |
| |
| const sections = [ |
| new TextSection(d1.childNodes[0] as Text, 0), |
| new TextSection(d2.childNodes[0] as Text, 6), |
| new TextSection(d3.childNodes[0] as Text, 12), |
| new TextSection(d4.childNodes[0] as Text, 18), |
| new TextSection(d5.childNodes[0] as Text, 24), |
| ]; |
| const chunk = new TextChunk(0, 0, 29); |
| // 11111111112222222222 |
| // 012345678901234567890123456789 |
| chunk.add(sections, 'abcde fghij klmno pqrst uvwxy'); |
| |
| const list = new TextAnnotationList([ |
| this.annotation( |
| 3, 14, 'de fghij kl', 'address'), // spans over a full node. |
| this.annotation(17, 20, 'zzz', 'other'), // Text doesn't match. |
| this.annotation(27, 28, 'x', 'other'), // Side by side with 'y' |
| this.annotation(28, 29, 'y', 'address'), // End of text. |
| ]); |
| |
| const styler = new InertTextStyler(); |
| const decorator = new TextDecorator(styler); |
| decorator.decorateChunk(chunk, list); |
| |
| expectEq(3, list.successes, 'successes: '); |
| expectEq(1, list.failures, 'failures: '); |
| expectEq(1, list.cancelled.length); |
| |
| const decoratedHTML = |
| '<div id="d1">abc<chrome_annotation>de</chrome_annotation></div>' + |
| '<div id="d2"><chrome_annotation>fghij</chrome_annotation></div>' + |
| '<div id="d3"><chrome_annotation>kl</chrome_annotation>mno</div>' + |
| '<div id="d4">pqrst</div>' + |
| '<div id="d5">uvw<chrome_annotation>x</chrome_annotation>' + |
| '<chrome_annotation>y</chrome_annotation></div>'; |
| expectEq(decoratedHTML, document.body.innerHTML); |
| |
| // Remove by type 'address', then 'other', |
| decorator.removeDecorationsOfType('address'); |
| const partialRemoveHTML = '<div id="d1">abcde</div>' + |
| '<div id="d2">fghij</div>' + |
| '<div id="d3">klmno</div>' + |
| '<div id="d4">pqrst</div>' + |
| '<div id="d5">uvw<chrome_annotation>x</chrome_annotation>y</div>'; |
| expectEq(partialRemoveHTML, document.body.innerHTML); |
| decorator.removeDecorationsOfType('other'); |
| expectEq(html, document.body.innerHTML); |
| // Check that removing all, doesn't corrupt the DOM. |
| decorator.removeAllDecorations(); |
| expectEq(html, document.body.innerHTML); |
| } |
| |
| // Test case 1 and 2 (see text_decorator.ts), the normal flow with collision |
| // with in-flight chunks. |
| testTextDecoratorInflightCollision() { |
| const html = '<div id="d1">abcdefghijklmnopqrstuvwxyz</div>'; |
| load(html); |
| const d1 = document.querySelector('#d1')!; |
| |
| const sections = [ |
| new TextSection(d1.childNodes[0] as Text, 0), |
| ]; |
| const chunk = new TextChunk(0, 0, 26); |
| // 11111111112222222 |
| // 012345678901234567890123456 |
| chunk.add(sections, 'abcdefghijklmnopqrstuvwxyz'); |
| |
| let list = new TextAnnotationList([ |
| this.annotation(3, 6, 'def', 'A'), // first annotation. |
| ]); |
| |
| const styler = new InertTextStyler(); |
| const decorator = new TextDecorator(styler); |
| decorator.decorateChunk(chunk, list); |
| expectEq(1, list.successes, 'successes 1: '); |
| expectEq(0, list.failures, 'failures 1: '); |
| expectEq(0, list.cancelled.length); |
| |
| const decoratedHTML = '<div id="d1">abc' + |
| '<chrome_annotation>def</chrome_annotation>' + |
| 'ghijklmnopqrstuvwxyz</div>'; |
| expectEq(decoratedHTML, document.body.innerHTML); |
| |
| // Now pretend another in-flight chunk has an annotation on the same node |
| // which is now replaced. Let's reuse chunk since it is unmutable. |
| list = new TextAnnotationList([ |
| this.annotation(20, 23, 'uvw', 'A'), // second annotation. |
| ]); |
| decorator.decorateChunk(chunk, list); |
| expectEq(1, list.successes, 'successes 2: '); |
| expectEq(0, list.failures, 'failures 2: '); |
| expectEq(0, list.cancelled.length); |
| |
| const redecoratedHTML = '<div id="d1">abc' + |
| '<chrome_annotation>def</chrome_annotation>' + |
| 'ghijklmnopqrst' + |
| '<chrome_annotation>uvw</chrome_annotation>' + |
| 'xyz</div>'; |
| expectEq(redecoratedHTML, document.body.innerHTML); |
| |
| decorator.removeAllDecorations(); |
| expectEq(html, document.body.innerHTML); |
| } |
| |
| // Test case 2 (see text_decorator.ts) with two div second annotation at edge |
| // (get new child node) |
| testTextDecoratorCollision() { |
| const html = '<div id="d1">abcdefghijklm</div>' + |
| '<div id="d2">nopqrstuvwxyz</div>'; |
| load(html); |
| const d1 = document.querySelector('#d1')!; |
| const d2 = document.querySelector('#d2')!; |
| |
| let sections = [ |
| new TextSection(d1.childNodes[0] as Text, 0), |
| ]; |
| let chunk = new TextChunk(0, 0, 26); |
| // 11111111112222222 |
| // 012345678901234567890123456 |
| chunk.add(sections, 'abcdefghijklmnopqrstuvwxyz'); |
| |
| let list = new TextAnnotationList([ |
| this.annotation(3, 6, 'def', 'A'), // first annotation. |
| ]); |
| |
| const styler = new InertTextStyler(); |
| const decorator = new TextDecorator(styler); |
| decorator.decorateChunk(chunk, list); |
| expectEq(1, list.successes, 'successes 1: '); |
| expectEq(0, list.failures, 'failures 1: '); |
| expectEq(0, list.cancelled.length); |
| |
| const decoratedHTML = '<div id="d1">abc' + |
| '<chrome_annotation>def</chrome_annotation>' + |
| 'ghijklm</div>' + |
| '<div id="d2">nopqrstuvwxyz</div>'; |
| expectEq(decoratedHTML, document.body.innerHTML); |
| |
| // Now pretend another chunk has an annotation partly on a replacement node. |
| sections = [ |
| new TextSection(d1.childNodes[2] as Text, 0), |
| new TextSection(d2.childNodes[0] as Text, 7), |
| ]; |
| chunk = new TextChunk(0, 0, 20); |
| // 11111111112 |
| // 012345678901234567890 |
| chunk.add(sections, 'ghijklmnopqrstuvwxyz'); |
| list = new TextAnnotationList([ |
| this.annotation(5, 9, 'lmno', 'address'), // second annotation. |
| ]); |
| decorator.decorateChunk(chunk, list); |
| expectEq(1, list.successes, 'successes 2: '); |
| expectEq(0, list.failures, 'failures 2: '); |
| expectEq(0, list.cancelled.length); |
| |
| const redecoratedHTML = '<div id="d1">abc' + |
| '<chrome_annotation>def</chrome_annotation>' + |
| 'ghijk' + |
| '<chrome_annotation>lm</chrome_annotation></div>' + |
| '<div id="d2">' + |
| '<chrome_annotation>no</chrome_annotation>' + |
| 'pqrstuvwxyz</div>'; |
| expectEq(redecoratedHTML, document.body.innerHTML); |
| |
| decorator.removeAllDecorations(); |
| expectEq(html, document.body.innerHTML); |
| } |
| |
| testTextDecoratorDeadNode() { |
| const html = '<div id="d1">abcdefghijklm</div>' + |
| '<div id="d2">nopqrstuvwxyz</div>'; |
| load(html); |
| let d1: Node|null = document.querySelector('#d1')!; |
| const d2 = document.querySelector('#d2')!; |
| |
| const sections = [ |
| new TextSection(d1!.childNodes[0] as Text, 0), |
| new TextSection(d2.childNodes[0] as Text, 13), |
| ]; |
| const chunk = new TextChunk(0, 0, 26); |
| // 11111111112222222 |
| // 012345678901234567890123456 |
| chunk.add(sections, 'abcdefghijklmnopqrstuvwxyz'); |
| |
| const list = new TextAnnotationList([ |
| this.annotation(11, 15, 'lmno', 'address'), |
| ]); |
| |
| // Remove from DOM |
| (d1 as HTMLElement).remove(); |
| d1 = null; |
| |
| const styler = new InertTextStyler(); |
| const decorator = new TextDecorator(styler); |
| decorator.decorateChunk(chunk, list); |
| expectEq(1, list.successes, 'successes 1: '); |
| expectEq(0, list.failures, 'failures 1: '); |
| expectEq(0, list.cancelled.length); |
| |
| const decoratedHTML = '<div id="d2">' + |
| '<chrome_annotation>no</chrome_annotation>' + |
| 'pqrstuvwxyz</div>'; |
| expectEq(decoratedHTML, document.body.innerHTML); |
| |
| decorator.removeAllDecorations(); |
| |
| const newHTML = '<div id="d2">nopqrstuvwxyz</div>'; |
| expectEq(newHTML, document.body.innerHTML); |
| } |
| } |