chromium/ios/web/annotations/resources/text_click_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_click.ts.
 */

import {AnnotationsTapConsumer, TextClick} from '//ios/web/annotations/resources/text_click.js';
import {expectEq, FakeTaskTimer, load, TestSuite} from '//ios/web/annotations/resources/text_test_utils.js';

class TestTextClick extends TestSuite {
  // Mark:  AnnotationsTapConsumer

  tappedAnnotation?: HTMLElement;
  tappedCancel?: boolean;

  tapConsumer: AnnotationsTapConsumer =
      (annotation: HTMLElement, cancel: boolean): void => {
        this.tappedAnnotation = annotation;
        this.tappedCancel = cancel;
      };

  // Mark:  tests

  override setUp() {
    this.tappedAnnotation = undefined;
    this.tappedCancel = undefined;
  }

  // Tests that `tapConsumer` is called (and `tappedCancel` is true) when no DOM
  // mutation occurs during the bubbling of a click event.
  testTextClickNoMutations() {
    const decoratedHTML = '<div id="outer">' +
        '<chrome_annotation>Hello</chrome_annotation>' +
        '</div>';
    load(decoratedHTML);

    const annotation = document.querySelector('chrome_annotation')!;
    const timer = new FakeTaskTimer();
    const clicker = new TextClick(
        document.documentElement, this.tapConsumer, () => undefined, timer,
        /* mutationCheckDelay */ 50, annotation);
    clicker.start();

    const event = new Event('click', {bubbles: true, cancelable: true});
    annotation.dispatchEvent(event);

    expectEq(undefined, this.tappedAnnotation, 'tappedAnnotation after click:');
    timer.moveAhead(/* ms= */ 10, /* times= */ 4);  // -> 40ms total
    expectEq(undefined, this.tappedAnnotation, 'tappedAnnotation after 40ms:');
    timer.moveAhead(/* ms= */ 10, /* times= */ 2);  // -> 60ms total
    expectEq(annotation, this.tappedAnnotation, 'tappedAnnotation after 60ms:');
    expectEq(false, this.tappedCancel, 'tappedCancel after 60ms:');

    clicker.stop();
  }

  // Tests that a click event stopped with `stopImmediatePropagation` doesn't
  // trigger the `tapConsumer`.
  testTextClickStopped() {
    const decoratedHTML = '<div id="outer">' +
        '<chrome_annotation>Hello</chrome_annotation>' +
        '</div>';
    load(decoratedHTML);

    const outer = document.querySelector('#outer')!;
    outer.addEventListener('click', (event: Event) => {
      event.stopImmediatePropagation();
    });
    const annotation = document.querySelector('chrome_annotation')!;
    const timer = new FakeTaskTimer();
    const clicker = new TextClick(
        document.documentElement, this.tapConsumer, () => undefined, timer,
        /* mutationCheckDelay */ 50, annotation);
    clicker.start();

    const event = new Event('click', {bubbles: true, cancelable: true});
    annotation.dispatchEvent(event);

    expectEq(undefined, this.tappedAnnotation, 'tappedAnnotation after click:');
    timer.moveAhead(/* ms= */ 10, /* times= */ 10);  // -> 100ms total
    // Should not reach calling the AnnotationsTapConsumer.
    expectEq(undefined, this.tappedAnnotation, 'tappedAnnotation after 100ms:');
    expectEq(undefined, this.tappedCancel, 'tappedCancel after 100ms:');

    clicker.stop();
  }

  // Tests that a click event stopped with `preventDefault` does trigger the
  // `tapConsumer` but with `tappedCancel` set to true.
  testTextClickPrevented() {
    const decoratedHTML = '<div id="outer">' +
        '<chrome_annotation>Hello</chrome_annotation>' +
        '</div>';
    load(decoratedHTML);

    const outer = document.querySelector('#outer')!;
    outer.addEventListener('click', (event: Event) => {
      event.preventDefault();
    });
    const annotation = document.querySelector('chrome_annotation')!;
    const timer = new FakeTaskTimer();
    const clicker = new TextClick(
        document.documentElement, this.tapConsumer, () => undefined, timer,
        /* mutationCheckDelay */ 50, annotation);
    clicker.start();

    const event = new Event('click', {bubbles: true, cancelable: true});
    annotation.dispatchEvent(event);

    // Without delay, this event should be cancelled:
    expectEq(
        annotation, this.tappedAnnotation, 'tappedAnnotation after 100ms:');
    expectEq(true, this.tappedCancel, 'tappedCancel after 100ms:');

    clicker.stop();
  }

  // Tests that a click event stopped due to DOM mutation does trigger the
  // `tapConsumer` but with `tappedCancel` set to true.
  testTextClickWithMutationInsideTree() {
    const decoratedHTML = '<div id="outer">' +
        '<div id="mutate">I will mutate!' +
        '<chrome_annotation>Hello</chrome_annotation></div>' +
        '</div>';
    load(decoratedHTML);

    const annotation = document.querySelector('chrome_annotation')!;
    const timer = new FakeTaskTimer();
    const clicker = new TextClick(
        document.documentElement, this.tapConsumer, () => undefined, timer,
        /* mutationCheckDelay */ 50, annotation);
    clicker.start();

    const event = new Event('click', {bubbles: true, cancelable: true});
    annotation.dispatchEvent(event);

    expectEq(undefined, this.tappedAnnotation, 'tappedAnnotation after click:');
    timer.moveAhead(/* ms= */ 10, /* times= */ 2);  // -> 20ms total
    document.querySelector('#mutate')!.appendChild(
        document.createTextNode('Mutated!'));
    clicker.updateForTesting();
    timer.moveAhead(/* ms= */ 10, /* times= */ 4);  // -> 60ms total
    expectEq(annotation, this.tappedAnnotation, 'tappedAnnotation after 60ms:');
    expectEq(true, this.tappedCancel, 'tappedCancel after 60ms:');

    clicker.stop();
  }

  // Tests that a click event is not stopped by a DOM mutation outside of the
  // tree of the annotation.
  testTextClickWithMutationOutsideTree() {
    const decoratedHTML = '<div id="outer">' +
        '<div id="mutate">I will mutate!</div>' +
        '<chrome_annotation>Hello</chrome_annotation>' +
        '</div>';
    load(decoratedHTML);

    const annotation = document.querySelector('chrome_annotation')!;
    const timer = new FakeTaskTimer();
    const clicker = new TextClick(
        document.documentElement, this.tapConsumer, () => undefined, timer,
        /* mutationCheckDelay */ 50, annotation);
    clicker.start();

    const event = new Event('click', {bubbles: true, cancelable: true});
    annotation.dispatchEvent(event);

    expectEq(undefined, this.tappedAnnotation, 'tappedAnnotation after click:');
    timer.moveAhead(/* ms= */ 10, /* times= */ 2);  // -> 20ms total
    document.querySelector('#mutate')!.appendChild(
        document.createTextNode('Mutated!'));
    clicker.updateForTesting();
    timer.moveAhead(/* ms= */ 10, /* times= */ 4);  // -> 60ms total
    expectEq(annotation, this.tappedAnnotation, 'tappedAnnotation after 60ms:');
    expectEq(false, this.tappedCancel, 'tappedCancel after 60ms:');

    clicker.stop();
  }
}

export {TestTextClick}