chromium/chrome/test/data/webui/side_panel/read_anything/fake_tree_builder_test.ts

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';

import {assertEquals, assertThrows, assertTrue} from 'chrome-untrusted://webui-test/chai_assert.js';

import {FakeReadingMode} from './fake_reading_mode.js';
import type {FakeTree} from './fake_tree_builder.js';
import {FakeTreeBuilder} from './fake_tree_builder.js';

suite('FakeTreeBuilderTest', () => {
  let readingMode: FakeReadingMode;
  const errorMsg: string = ': expected false to be true';

  setup(() => {
    readingMode = new FakeReadingMode();
  });

  function buildTree(
      builder: FakeTreeBuilder, rootId: number, htmlTagNodes: number[],
      htmlTags: string[], textNodes: number[], text: string[]): FakeTree {
    builder.root(rootId);
    for (let i = 0; i < htmlTagNodes.length; i++) {
      builder.addTag(htmlTagNodes[i]!, rootId, htmlTags[i]!)
          .addText(textNodes[i]!, htmlTagNodes[i]!, text[i]!);
    }
    return builder.build(readingMode);
  }

  suite('fake tree builder', () => {
    let builder: FakeTreeBuilder;

    function assertNodeHasExpectedValues(
        tree: FakeTree, expectedId: number, expectedHtmlTag: string,
        expectedTextContent: string, expectedChildren: number[]) {
      assertTrue(tree.nodes.has(expectedId));
      const node = tree.nodes.get(expectedId)!;
      assertEquals(node.id, expectedId);

      assertEquals(expectedHtmlTag, node.htmlTag);
      assertEquals(expectedHtmlTag, readingMode.getHtmlTag(expectedId));

      assertEquals(expectedTextContent, node.textContent);
      assertEquals(expectedTextContent, readingMode.getTextContent(expectedId));

      const readingModeChildren = readingMode.getChildren(expectedId);
      assertEquals(expectedChildren.length, readingModeChildren.length);
      assertEquals(expectedChildren.length, node.children.length);
      for (let i = 0; i < expectedChildren.length; i++) {
        assertEquals(expectedChildren[i], readingModeChildren[i]!);
        assertEquals(expectedChildren[i], node.children[i]!);
      }
    }

    setup(() => {
      builder = new FakeTreeBuilder();
    });

    test('build without root node fails', () => {
      assertThrows(() => builder.build(readingMode), errorMsg);
    });

    test('add tag before parent fails', () => {
      assertThrows(() => builder.addTag(1, 0, 'span'), errorMsg);
    });

    test('add text before parent fails', () => {
      assertThrows(
          () => builder.addText(2, 1, 'just give me a reason'), errorMsg);
    });

    test('build tree with just root returns root with no children', () => {
      const rootId = 0;
      const tree = builder.root(rootId).build(readingMode);

      assertEquals(rootId, readingMode.rootId);
      assertNodeHasExpectedValues(tree, rootId, '#document', '', []);
    });

    suite('with valid tree', () => {
      const rootId = 10;
      const htmlTagNodes = [11, 13, 15, 17];
      const htmlTags = ['h1', 'p', 'h2', 'span'];
      const textNodes = [12, 14, 16, 18];
      const text = [
        'Verse 1',
        'right from the start you were a thief you stole my heart',
        'Chorus',
        'just give me a reason, just a little bit\'s enough',
      ];

      let tree: FakeTree;

      setup(() => {
        tree =
            buildTree(builder, rootId, htmlTagNodes, htmlTags, textNodes, text);
      });

      test('root of tree has document tag and html tag children', () => {
        assertEquals(rootId, readingMode.rootId);
        assertNodeHasExpectedValues(
            tree, rootId, '#document', '', htmlTagNodes);
      });

      test(
          'tags have html tags, no text content, and text nodes as children',
          () => {
            for (let i = 0; i < htmlTagNodes.length; i++) {
              assertNodeHasExpectedValues(
                  tree, htmlTagNodes[i]!, htmlTags[i]!, '', [textNodes[i]!]);
            }
          });

      test('text nodes have text content, no tags, and no children', () => {
        for (let i = 0; i < textNodes.length; i++) {
          assertNodeHasExpectedValues(tree, textNodes[i]!, '', text[i]!, []);
        }
      });
    });
  });

  suite('fake tree', () => {
    const rootId = 10;
    const htmlTagNodes = [11, 13, 15, 17, 19];
    const htmlTags = ['p', 'p', 'p', 'p', 'p'];
    const textNodes = [12, 14, 16, 18, 20];
    const text = [
      'Maria',
      'I\'ve just met a girl named Maria',
      'And suddenly that name',
      'Will never be the same',
      'To me',
    ];

    let tree: FakeTree;

    setup(() => {
      const builder = new FakeTreeBuilder();
      tree =
          buildTree(builder, rootId, htmlTagNodes, htmlTags, textNodes, text);
    });

    suite('setSelection', () => {
      test('non-existent nodes fails', () => {
        assertThrows(() => tree.setSelection(4, 0, 5, 7), errorMsg);
      });

      test('selection within one node succeeds', () => {
        tree.setSelection(14, 3, 14, 10);

        assertEquals(14, readingMode.startNodeId);
        assertEquals(14, readingMode.endNodeId);
        assertEquals(3, readingMode.startOffset);
        assertEquals(10, readingMode.endOffset);
      });

      test('selection across nodes succeeds', () => {
        tree.setSelection(14, 0, 16, 5);

        assertEquals(14, readingMode.startNodeId);
        assertEquals(16, readingMode.endNodeId);
        assertEquals(0, readingMode.startOffset);
        assertEquals(5, readingMode.endOffset);
      });
    });

    suite('setReadingHighlight', () => {
      test('non-existent nodes fails', () => {
        assertThrows(() => tree.setReadingHighlight(2, 1, 3, 2), errorMsg);
      });

      suite('when highlighting one node', () => {
        setup(() => {
          tree.setReadingHighlight(textNodes[1]!, 3, textNodes[1]!, 10);
        });

        test('indices for highlighted node are correct', () => {
          assertEquals(3, readingMode.getCurrentTextStartIndex(textNodes[1]!));
          assertEquals(10, readingMode.getCurrentTextEndIndex(textNodes[1]!));
        });

        test('indices for valid but not highlighted node are not found', () => {
          assertEquals(-1, readingMode.getCurrentTextStartIndex(textNodes[2]!));
          assertEquals(-1, readingMode.getCurrentTextEndIndex(textNodes[2]!));
        });

        test('indices for invalid node are not found', () => {
          assertEquals(-1, readingMode.getCurrentTextStartIndex(0));
          assertEquals(-1, readingMode.getCurrentTextEndIndex(0));
        });
      });

      suite('when highlighting across nodes', () => {
        setup(() => {
          tree.setReadingHighlight(textNodes[1]!, 4, textNodes[3]!, 5);
        });

        test('indices for first highlighted node are correct', () => {
          assertEquals(4, readingMode.getCurrentTextStartIndex(textNodes[1]!));
          assertEquals(
              readingMode.getCurrentTextEndIndex(textNodes[1]!),
              text[1]!.length);
        });

        test('indices for last highlighted node are correct', () => {
          assertEquals(0, readingMode.getCurrentTextStartIndex(textNodes[3]!));
          assertEquals(5, readingMode.getCurrentTextEndIndex(textNodes[3]!));
        });

        test('indices for invalid node are not found', () => {
          assertEquals(-1, readingMode.getCurrentTextStartIndex(0));
          assertEquals(-1, readingMode.getCurrentTextEndIndex(0));
        });
      });
    });

    suite('highlightNode', () => {
      test('non-existent nodes fails', () => {
        assertThrows(() => tree.highlightNode(0), errorMsg);
      });

      suite('when highlighting valid node', () => {
        setup(() => {
          tree.highlightNode(textNodes[2]!);
        });

        test('full node is highlighted', () => {
          assertEquals(0, readingMode.getCurrentTextStartIndex(textNodes[2]!));
          assertEquals(
              readingMode.getCurrentTextEndIndex(textNodes[2]!),
              text[2]!.length);
        });

        test('valid but not highlighted node has invalid indices', () => {
          assertEquals(-1, readingMode.getCurrentTextStartIndex(textNodes[3]!));
          assertEquals(-1, readingMode.getCurrentTextEndIndex(textNodes[3]!));
        });

        test('indices for invalid node are not found', () => {
          assertEquals(-1, readingMode.getCurrentTextStartIndex(0));
          assertEquals(-1, readingMode.getCurrentTextEndIndex(0));
        });
      });
    });
  });
});