| // 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_decoration.ts. |
| */ |
| |
| import {createChromeAnnotation, isDecorationNode, originalNodeDecorationId, replacementNodeDecorationId, TextDecoration} from '//ios/web/annotations/resources/text_decoration.js'; |
| import type {HTMLElementWithSymbolIndex, TextWithSymbolIndex} from '//ios/web/annotations/resources/text_dom_utils.js'; |
| import {expectEq, load, TestSuite} from '//ios/web/annotations/resources/text_test_utils.js'; |
| |
| export class TestTextDecoration extends TestSuite { |
| // Checks that applying a decoration works properly for the page html and the |
| // Symbol tags. |
| testTextDecorationReplacement() { |
| const originalHTML = '<div id="d1">Hello</div>' + |
| '<div id="d2">Small</div>' + |
| '<div id="d3">World</div>'; |
| const decoratedHTML = '<div id="d1">Hello</div>' + |
| '<div id="d2"><chrome_annotation>Small</chrome_annotation></div>' + |
| '<div id="d3">World</div>'; |
| load(originalHTML); |
| const body = document.body; |
| const d2 = document.querySelector<HTMLElementWithSymbolIndex>('#d2')!; |
| const originalTextNode = d2.childNodes[0] as TextWithSymbolIndex; |
| const replacement = |
| createChromeAnnotation(2, 'Small', 'SIZE', 'Small', 'external-key'); |
| |
| expectEq(undefined, originalTextNode[originalNodeDecorationId]); |
| expectEq(undefined, replacement[replacementNodeDecorationId]); |
| const decoration = new TextDecoration(1, originalTextNode, [replacement]); |
| expectEq(undefined, originalTextNode[originalNodeDecorationId]); |
| expectEq(undefined, replacement[replacementNodeDecorationId]); |
| expectEq(false, isDecorationNode(originalTextNode)); |
| expectEq(false, isDecorationNode(replacement)); |
| |
| expectEq(originalHTML, body.innerHTML); |
| decoration.apply(); |
| expectEq(true, decoration.live); |
| expectEq(decoratedHTML, body.innerHTML); |
| expectEq(1, originalTextNode[originalNodeDecorationId]); |
| expectEq(1, replacement[replacementNodeDecorationId]); |
| expectEq(true, isDecorationNode(originalTextNode)); |
| expectEq(true, isDecorationNode(replacement)); |
| |
| // Check for no tagging on parent node. |
| expectEq(undefined, d2[originalNodeDecorationId]); |
| expectEq(undefined, d2[replacementNodeDecorationId]); |
| |
| decoration.restore(); |
| expectEq(false, decoration.live); |
| expectEq(originalHTML, body.innerHTML); |
| expectEq(undefined, originalTextNode[originalNodeDecorationId]); |
| expectEq(undefined, replacement[replacementNodeDecorationId]); |
| expectEq(false, isDecorationNode(originalTextNode)); |
| expectEq(false, isDecorationNode(replacement)); |
| } |
| |
| // Tests replacing a node with multiple nodes. |
| testTextDecorationComplexReplacements() { |
| const originalHTML = '<div id="d1">Hello</div>' + |
| '<div id="d2">Small</div>' + |
| '<div id="d3">World</div>'; |
| const decoratedHTML = '<div id="d1">Hello</div>' + |
| '<div id="d2">S<chrome_annotation>mal</chrome_annotation>l</div>' + |
| '<div id="d3">World</div>'; |
| load(originalHTML); |
| const body = document.body; |
| const d2 = document.querySelector('#d2')!; |
| const originalTextNode = d2.childNodes[0] as TextWithSymbolIndex; |
| const prefix = document.createTextNode('S'); |
| const replacement = |
| createChromeAnnotation(2, 'mal', 'SIZE', 'mal', 'external-key'); |
| const postfix = document.createTextNode('l'); |
| |
| const decoration = |
| new TextDecoration(1, originalTextNode, [prefix, replacement, postfix]); |
| decoration.apply(); |
| expectEq(true, decoration.live); |
| expectEq(decoratedHTML, body.innerHTML); |
| expectEq(1, originalTextNode[originalNodeDecorationId]); |
| expectEq(1, replacement[replacementNodeDecorationId]); |
| expectEq(true, isDecorationNode(originalTextNode)); |
| expectEq(true, isDecorationNode(replacement)); |
| |
| decoration.restore(); |
| expectEq(false, decoration.live); |
| expectEq(originalHTML, body.innerHTML); |
| expectEq(undefined, originalTextNode[originalNodeDecorationId]); |
| expectEq(undefined, replacement[replacementNodeDecorationId]); |
| expectEq(false, isDecorationNode(originalTextNode)); |
| expectEq(false, isDecorationNode(replacement)); |
| } |
| |
| // Tests counting and removing decorations by type. |
| testTextDecorationTypes() { |
| const originalHTML = '<div id="d1">Hello World</div>'; |
| const decoratedHTML = '<div id="d1">H' + |
| '<chrome_annotation>ell</chrome_annotation>' + |
| 'o W' + |
| '<chrome_annotation>orld</chrome_annotation>' + |
| '</div>'; |
| load(originalHTML); |
| const body = document.body; |
| const d1 = document.querySelector('#d1')!; |
| const originalTextNode = d1.childNodes[0] as Text; |
| const replacementTextNode1 = document.createTextNode('H'); |
| const replacement2 = |
| createChromeAnnotation(2, 'ell', '@ELL', 'ell', 'external-key'); |
| const replacementTextNode3 = document.createTextNode('o W'); |
| const replacement4 = |
| createChromeAnnotation(2, 'orld', '@ORLD', 'orld', 'external-key'); |
| |
| const decoration = new TextDecoration(1, originalTextNode, [ |
| replacementTextNode1, |
| replacement2, |
| replacementTextNode3, |
| replacement4, |
| ]); |
| decoration.apply(); |
| expectEq(decoratedHTML, body.innerHTML); |
| expectEq(1, decoration.replacementsOfType('@ELL')); |
| expectEq(1, decoration.replacementsOfType('@ORLD')); |
| |
| const noOrldHTML = '<div id="d1">H' + |
| '<chrome_annotation>ell</chrome_annotation>' + |
| 'o World' + |
| '</div>'; |
| decoration.removeReplacementsOfType('@ORLD'); |
| expectEq(noOrldHTML, body.innerHTML); |
| |
| decoration.restore(); |
| expectEq(originalHTML, body.innerHTML); |
| } |
| |
| // Tests merging replacements, before and after the decoration is live. |
| testTextDecorationReplaceLive() { |
| const originalHTML = '<div id="d1">Hello World</div>'; |
| const decoratedHTML = '<div id="d1">J' + |
| '<chrome_annotation>ell</chrome_annotation>' + |
| 'o W' + |
| '<chrome_annotation>orld</chrome_annotation>' + |
| '</div>'; |
| load(originalHTML); |
| const body = document.body; |
| const d1 = document.querySelector('#d1')!; |
| const originalTextNode = d1.childNodes[0] as Text; |
| const replacementTextNode1 = document.createTextNode('H'); |
| const replacement2 = |
| createChromeAnnotation(2, 'ell', '@ELL', 'ell', 'external-key'); |
| const replacementTextNode3 = document.createTextNode('o W'); |
| const replacement4 = |
| createChromeAnnotation(2, 'orld', '@ORLD', 'orld', 'external-key'); |
| |
| const decoration = new TextDecoration(1, originalTextNode, [ |
| replacementTextNode1, |
| replacement2, |
| replacementTextNode3, |
| replacement4, |
| ]); |
| |
| // Before live. |
| decoration.replaceReplacementNode( |
| replacementTextNode1, [document.createTextNode('J')]); |
| decoration.apply(); |
| expectEq(decoratedHTML, body.innerHTML); |
| |
| // After live. |
| decoration.replaceReplacementNode( |
| replacementTextNode3, [document.createTextNode('y M')]); |
| |
| const liveDecoratedHTML = '<div id="d1">J' + |
| '<chrome_annotation>ell</chrome_annotation>' + |
| 'y M' + |
| '<chrome_annotation>orld</chrome_annotation>' + |
| '</div>'; |
| expectEq(liveDecoratedHTML, body.innerHTML); |
| |
| // The original test should come back on `restore`. |
| decoration.restore(); |
| expectEq(originalHTML, body.innerHTML); |
| } |
| |
| // Tests proper DOM cleanup when an annotation is reported corrupted. |
| testTextDecorationCorrupted() { |
| const originalHTML = '<div id="d1">Hello World</div>'; |
| load(originalHTML); |
| const body = document.body; |
| const d1 = document.querySelector('#d1')!; |
| const originalTextNode = d1.childNodes[0] as Text; |
| const replacementTextNode1 = document.createTextNode('H'); |
| const replacement2 = |
| createChromeAnnotation(2, 'ell', '@ELL', 'ell', 'external-key'); |
| const replacementTextNode3 = document.createTextNode('o W'); |
| const replacement4 = |
| createChromeAnnotation(2, 'orld', '@ORLD', 'orld', 'external-key'); |
| |
| const decoration = new TextDecoration(1, originalTextNode, [ |
| replacementTextNode1, |
| replacement2, |
| replacementTextNode3, |
| replacement4, |
| ]); |
| decoration.apply(); |
| |
| // Something happened and decoration is corrupted, calling |
| // `cleanupAfterCorruption` should drop this decoration without restoring |
| // `originalTextNode`. The web engine will merge neighbour text nodes. |
| decoration.cleanupAfterCorruption(); |
| expectEq(originalHTML, body.innerHTML); |
| } |
| } |