| // 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_dom_observer.ts. |
| */ |
| |
| import {replacementNodeDecorationId} from '//ios/web/annotations/resources/text_decoration.js'; |
| import type {CountedIntersectionObserver} from '//ios/web/annotations/resources/text_dom_observer.js'; |
| import {TextDomObserver} from '//ios/web/annotations/resources/text_dom_observer.js'; |
| import type {HTMLElementWithSymbolIndex} from '//ios/web/annotations/resources/text_dom_utils.js'; |
| import {expectEq, load, TestSuite} from '//ios/web/annotations/resources/text_test_utils.js'; |
| |
| export class TestTextDomObserver extends TestSuite implements |
| CountedIntersectionObserver { |
| corrupted: Node[] = []; |
| |
| // Mark: TextDecorationNodeRemovedConsumer |
| decorationNodeRemovedConsumer = (node: Node): void => { |
| this.corrupted.push(node); |
| }; |
| |
| // Mark: CountedIntersectionObserver |
| observed = new Map<string, number>(); |
| |
| observe(node: Node): void { |
| const element = node.parentElement; |
| if (element) { |
| this.observed.set(element.id, (this.observed.get(element.id) || 0) + 1); |
| } |
| } |
| |
| unobserve(node: Node): void { |
| const element = node.parentElement!; |
| if (element) { |
| this.observed.set(element.id, (this.observed.get(element.id) || 0) - 1); |
| } |
| } |
| |
| // Mark: Tests |
| |
| observer = new TextDomObserver( |
| document.documentElement, this, this.decorationNodeRemovedConsumer); |
| |
| override setUp(): void { |
| this.observed.clear(); |
| this.corrupted.length = 0; |
| this.observer.start(); |
| } |
| |
| override tearDown(): void { |
| this.observer.stop(); |
| } |
| |
| // TODO(crbug.com/40936184): add test for shadowRoot. |
| |
| // Tests the observer works correctly when nodes are added or removed and when |
| // text is mutated. |
| testTextDomObserverFlow() { |
| load( |
| '<div id="d0">' + |
| '<div id="d1">Hello</div>' + |
| '<div id="d2">to <span id="b0">this</span> nice</div>' + |
| '<div id="d3">World</div>' + |
| '</div>'); |
| this.observer.updateForTesting(); |
| expectEq(this.observed.size, 4); |
| |
| // Each observed entry should hold the number of text nodes under it. |
| expectEq(this.observed.get('d1'), 1); |
| expectEq(this.observed.get('d2'), 2); |
| expectEq(this.observed.get('d3'), 1); |
| expectEq(this.observed.get('b0'), 1); |
| |
| // Add a node. |
| const parent = document.querySelector('#d3'); |
| const node = document.createElement('span'); |
| node.id = 's0'; |
| node.textContent = '!'; |
| parent?.appendChild(node); |
| this.observer.updateForTesting(); |
| expectEq(this.observed.get('d3'), 1); |
| expectEq(this.observed.get('s0'), 1); |
| |
| // Remove a node. |
| document.querySelector('#d1')?.remove(); |
| this.observer.updateForTesting(); |
| expectEq(this.observed.get('d1'), 0); |
| |
| document.querySelector('#d2')?.remove(); |
| this.observer.updateForTesting(); |
| expectEq(this.observed.get('b0'), 0); |
| expectEq(this.observed.get('d2'), 0); |
| |
| // Change a node. |
| document.querySelector('#d3')!.textContent = 'Bye Bye!'; |
| this.observer.updateForTesting(); |
| expectEq(this.observed.get('d3'), 2); |
| } |
| |
| // Tests the NodeRemovedConsumer is called when 3p corrupts an annotation. |
| testTextDecorationNodeRemovedConsumer() { |
| load( |
| '<div id="d0">' + |
| '<div id="d1">Hello</div>' + |
| '<CHROME_ANNOTATION id="d2">to the</CHROME_ANNOTATION>' + |
| '<div id="d3">World</div>' + |
| '</div>'); |
| this.observer.updateForTesting(); |
| expectEq(this.observed.size, 2); |
| |
| // Make annotation 'decorated'. |
| const annotation = |
| document.querySelector<HTMLElementWithSymbolIndex>('#d2')!; |
| annotation[replacementNodeDecorationId] = 'd2'; |
| |
| // Now have 3p remove it without going through decorator. |
| annotation.remove(); |
| this.observer.updateForTesting(); |
| |
| // Expect it to be reported as corrupted. |
| expectEq(this.corrupted.length, 1); |
| expectEq(this.corrupted[0], annotation); |
| } |
| } |