chromium/ios/web/annotations/resources/text_decoration_test.ts

// 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 {HTMLElementWithSymbolIndex, TextWithSymbolIndex} from '//ios/web/annotations/resources/text_dom_utils.js';
import {expectEq, load, TestSuite} from '//ios/web/annotations/resources/text_test_utils.js';

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('#d2') as HTMLElementWithSymbolIndex;
    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);
  }
}

export {TestTextDecoration}