chromium/chrome/test/data/pdf/viewer_thumbnail_bar_test.ts

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

import type {ViewerThumbnailBarElement, ViewerThumbnailElement} from 'chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/pdf_viewer_wrapper.js';
import {ChangePageOrigin, PAINTED_ATTRIBUTE, PluginController} from 'chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/pdf_viewer_wrapper.js';
import {keyDownOn} from 'chrome://webui-test/keyboard_mock_interactions.js';
import {eventToPromise, microtasksFinished, whenAttributeIs} from 'chrome://webui-test/test_util.js';

function createThumbnailBar(): ViewerThumbnailBarElement {
  document.body.innerHTML = '';
  const thumbnailBar = document.createElement('viewer-thumbnail-bar');
  thumbnailBar.inTest = true;
  document.body.appendChild(thumbnailBar);
  return thumbnailBar;
}

function getTestThumbnailBarHeight(): number {
  // Create a viewer-thumbnail element to get the standard height.
  document.body.innerHTML = '';
  const sizerThumbnail = document.createElement('viewer-thumbnail');
  sizerThumbnail.pageNumber = 1;
  document.body.appendChild(sizerThumbnail);
  return sizerThumbnail.offsetHeight;
}

function keydown(element: HTMLElement, key: string) {
  keyDownOn(element, 0, [], key);
}

function whenThumbnailPainted(thumbnail: ViewerThumbnailElement):
    Promise<void> {
  return whenAttributeIs(thumbnail, PAINTED_ATTRIBUTE, '');
}

function whenThumbnailCleared(thumbnail: ViewerThumbnailElement):
    Promise<void> {
  return whenAttributeIs(thumbnail, PAINTED_ATTRIBUTE, null);
}

// Unit tests for the viewer-thumbnail-bar element.
const tests = [
  // Test that the thumbnail bar has the correct number of thumbnails and
  // correspond to the right pages.
  async function testThumbnails() {
    const testDocLength = 10;
    const thumbnailBar = createThumbnailBar();
    thumbnailBar.docLength = testDocLength;

    await microtasksFinished();

    // Test that the correct number of viewer-thumbnail elements was created.
    const thumbnails =
        thumbnailBar.shadowRoot!.querySelectorAll('viewer-thumbnail');
    chrome.test.assertEq(testDocLength, thumbnails.length);

    function testNavigateThumbnail(
        thumbnail: ViewerThumbnailElement,
        expectedPageIndex: number): Promise<void> {
      const whenChanged = eventToPromise('change-page', thumbnailBar);
      thumbnail.getClickTarget().click();
      return whenChanged.then(e => {
        chrome.test.assertEq(expectedPageIndex, e.detail.page);
        chrome.test.assertEq(ChangePageOrigin.THUMBNAIL, e.detail.origin);
      });
    }

    // Test that each thumbnail has the correct page number and navigates to
    // the corresponding page.
    for (let i = 0; i < thumbnails.length; i++) {
      const thumbnail = thumbnails[i]!;
      chrome.test.assertEq(i + 1, thumbnail.pageNumber);
      await testNavigateThumbnail(thumbnail, i);
    }
    chrome.test.succeed();
  },
  async function testTriggerPaint() {
    const thumbnailBarHeight = getTestThumbnailBarHeight();

    // Clear HTML for just the thumbnail bar.
    const testDocLength = 8;
    const thumbnailBar = createThumbnailBar();
    thumbnailBar.docLength = testDocLength;

    // Set the height to one thumbnail. One thumbnail should be visible and
    // another should be hidden by intersecting the observer.
    thumbnailBar.style.height = `${thumbnailBarHeight}px`;
    thumbnailBar.style.display = 'block';

    // Remove any padding from the scroller.
    const scroller = thumbnailBar.$.thumbnails!;
    scroller.style.padding = '';

    await microtasksFinished();

    const thumbnails =
        thumbnailBar.shadowRoot!.querySelectorAll('viewer-thumbnail');

    // Only two thumbnails should be "painted" upon load.
    const whenRequestedPaintingFirst = [
      whenThumbnailPainted(thumbnails[0]!),
      whenThumbnailPainted(thumbnails[1]!),
    ];
    await Promise.all(whenRequestedPaintingFirst);

    chrome.test.assertEq(testDocLength, thumbnails.length);
    for (let i = 0; i < thumbnails.length; i++) {
      chrome.test.assertEq(i < 2, thumbnails[i]!.isPainted());
    }

    // Test that scrolling to the sixth thumbnail triggers 'paint-thumbnail'
    // for thumbnails 3 through 7. When on the sixth thumbnail, five
    // thumbnails above and one thumbnail below should also be painted because
    // of the 500% top and 100% bottom root margins.
    const whenRequestedPaintingNext = [];
    for (let i = 2; i < 7; i++) {
      whenRequestedPaintingNext.push(whenThumbnailPainted(thumbnails[i]!));
    }
    const thumbnailHeight = thumbnailBarHeight + 24;  // Including padding.
    scroller.scrollTop = 5 * thumbnailHeight;
    await Promise.all(whenRequestedPaintingNext);

    // First seven thumbnails should be painted.
    for (let i = 0; i < thumbnails.length; i++) {
      chrome.test.assertEq(i < 7, thumbnails[i]!.isPainted());
    }

    // Test that scrolling down to the eighth thumbnail will clear the
    // thumbnails outside the root margin, namely the first two. A paint
    // should also be triggered for the eighth thumbnail.
    const whenRequestedPaintingLast = [
      whenThumbnailPainted(thumbnails[7]!),
      whenThumbnailCleared(thumbnails[0]!),
      whenThumbnailCleared(thumbnails[1]!),
    ];
    scroller.scrollTop = 7 * thumbnailHeight;
    await Promise.all(whenRequestedPaintingLast);

    // Only first two thumbnails should not be painted.
    for (let i = 0; i < thumbnails.length; i++) {
      chrome.test.assertEq(i > 1, thumbnails[i]!.isPainted());
    }
    chrome.test.succeed();
  },
  async function testThumbnailForwardFocus() {
    const testDocLength = 10;
    const thumbnailBar = createThumbnailBar();
    thumbnailBar.docLength = testDocLength;

    await microtasksFinished();

    function waitForwardFocus(pageNumber: number): Promise<void> {
      // Reset focus.
      thumbnailBar.blur();

      const toThumbnail = thumbnailBar.getThumbnailForPage(pageNumber)!;
      const whenActiveThumbnailFocused = eventToPromise('focus', toThumbnail);

      // Calling focus() on `thumbnailBar` in this test doesn't trigger the
      // event listener, but manually dispatching an event does.
      thumbnailBar.dispatchEvent(new FocusEvent('focus'));
      return whenActiveThumbnailFocused;
    }

    // When there's no active page, focus should forward to the first
    // thumbnail.
    await waitForwardFocus(1);

    // When there's an active page, focus should forward to the thumbnail of
    // the active page.
    let activePage = 3;
    thumbnailBar.activePage = activePage;
    await waitForwardFocus(activePage);

    activePage = 10;
    thumbnailBar.activePage = activePage;
    await waitForwardFocus(activePage);
    chrome.test.succeed();
  },
  async function testThumbnailUpDownFocus() {
    const testDocLength = 2;
    const thumbnailBar = createThumbnailBar();
    thumbnailBar.docLength = testDocLength;

    await microtasksFinished();

    thumbnailBar.activePage = 1;
    let whenChanged = eventToPromise('change-page', thumbnailBar);
    keydown(thumbnailBar, 'ArrowDown');
    let event = await whenChanged;

    // The event contains the zero-based page index.
    chrome.test.assertEq(1, event.detail.page);

    thumbnailBar.activePage = 2;
    whenChanged = eventToPromise('change-page', thumbnailBar);
    keydown(thumbnailBar, 'ArrowUp');
    event = await whenChanged;

    // The event contains the zero-based page index.
    chrome.test.assertEq(0, event.detail.page);

    chrome.test.succeed();
  },
  async function testThumbnailLeftRightSelect() {
    const testDocLength = 2;
    const thumbnailBar = createThumbnailBar();
    thumbnailBar.docLength = testDocLength;

    await microtasksFinished();

    thumbnailBar.activePage = 1;
    let whenChanged = eventToPromise('change-page', thumbnailBar);
    keydown(thumbnailBar, 'ArrowRight');
    let event = await whenChanged;

    // The event contains the zero-based page index.
    chrome.test.assertEq(1, event.detail.page);

    thumbnailBar.activePage = 2;
    whenChanged = eventToPromise('change-page', thumbnailBar);
    keydown(thumbnailBar, 'ArrowLeft');
    event = await whenChanged;

    // The event contains the zero-based page index.
    chrome.test.assertEq(0, event.detail.page);

    chrome.test.succeed();
  },
  async function testReactToNoPlugin() {
    const thumbnailBar = createThumbnailBar();
    thumbnailBar.docLength = 1;

    // Deactivate the PluginController, causing the thumbnails to hide.
    const pluginController = PluginController.getInstance();
    pluginController.isActive = false;

    await microtasksFinished();

    const scroller = thumbnailBar.$.thumbnails;
    chrome.test.assertTrue(scroller.hidden);

    const thumbnail =
        thumbnailBar.shadowRoot!.querySelector('viewer-thumbnail')!;

    const whenPaintTriggered = whenThumbnailPainted(thumbnail).then(() => {
      // The thumbnail shouldn't paint when the controller is inactive.
      if (!pluginController.isActive) {
        chrome.test.fail();
      }
    });

    // Give the test a chance to fail.
    await microtasksFinished();

    // The thumbnail should paint when reactivating the plugin.
    pluginController.isActive = true;
    await microtasksFinished();
    chrome.test.assertFalse(scroller.hidden);
    await whenPaintTriggered;

    // The thumbnail should clear when deactivating the plugin.
    pluginController.isActive = false;
    await microtasksFinished();
    chrome.test.assertTrue(scroller.hidden);
    await whenThumbnailCleared(thumbnail);
    chrome.test.succeed();
  },
];

chrome.test.runTests(tests);