chromium/chrome/test/data/webui/chromeos/shortcut_customization/shortcuts_page_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.
import 'chrome://shortcut-customization/js/shortcuts_page.js';
import 'chrome://webui-test/chromeos/mojo_webui_test_support.js';

import {assert} from 'chrome://resources/js/assert.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {AcceleratorLookupManager} from 'chrome://shortcut-customization/js/accelerator_lookup_manager.js';
import {AcceleratorRowElement} from 'chrome://shortcut-customization/js/accelerator_row.js';
import {createFakeMojoAccelInfo, createFakeMojoLayoutInfo} from 'chrome://shortcut-customization/js/fake_data.js';
import {AcceleratorCategory, AcceleratorSource, AcceleratorSubcategory, MojoAcceleratorConfig, MojoLayoutInfo} from 'chrome://shortcut-customization/js/shortcut_types.js';
import {SHORTCUTS_APP_URL} from 'chrome://shortcut-customization/js/shortcut_utils.js';
import {ShortcutsPageElement} from 'chrome://shortcut-customization/js/shortcuts_page.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks, waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';

function initShortcutsPageElement(category: AcceleratorCategory):
    ShortcutsPageElement {
  const element =
      document.createElement('shortcuts-page') as ShortcutsPageElement;
  element.initialData = {category};
  document.body.appendChild(element);
  flush();
  return element;
}

/**
 * Initialize the AcceleratorLookupManager, and create fake accelerators equal
 * to `numFakeAccelerators`, with those accelerators split between two
 * subcategories based on the value of `numFirstSubCategoryAccelerators`.
 * @param numFakeAccelerators the number of fake accelerators to create.
 * @param numFirstSubCategoryAccelerators the number of accelerators in the
 *     first subcategory; the rest will be in the second subcategory. Should not
 *     exceed the value of `numFakeAccelerators`.
 * @returns the AcceleratorLookupManager instance.
 */
function initManagerWithFakeData(
    numFakeAccelerators: number,
    numFirstSubCategoryAccelerators: number): AcceleratorLookupManager {
  assert(numFirstSubCategoryAccelerators <= numFakeAccelerators);
  const manager = AcceleratorLookupManager.getInstance();
  const acceleratorConfig:
      MojoAcceleratorConfig = {[AcceleratorSource.kAsh]: {}};
  for (let i = 0; i < numFakeAccelerators; i++) {
    acceleratorConfig[AcceleratorCategory.kGeneral]![i] =
        [createFakeMojoAccelInfo()];
  }
  manager.setAcceleratorLookup(acceleratorConfig);

  const blankLayoutInfos =
      Array<MojoLayoutInfo>(numFakeAccelerators)
          .fill(createFakeMojoLayoutInfo(
              'description', /*action=*/ 0, AcceleratorCategory.kGeneral,
              AcceleratorSource.kAsh));
  const layoutInfos = blankLayoutInfos.map((layoutInfo, index) => {
    return {
      ...layoutInfo,
      subCategory: index < numFirstSubCategoryAccelerators ?
          AcceleratorSubcategory.kApps :
          AcceleratorSubcategory.kGeneralControls,
      action: index,
    };
  });
  manager.setAcceleratorLayoutLookup(layoutInfos);
  return manager;
}

/**
 * Check if any part of the given `element` is visible within the viewport.
 * Does not account for horizontal visibility.
 * @param element The element to check for visibility.
 * @returns True if any part of the element is visible within the viewport.
 */
function isVisibleVerticallyInViewport(element: HTMLElement): boolean {
  const rect = element.getBoundingClientRect();
  return rect.top < window.innerHeight && rect.bottom >= 0;
}

/**
 * Override the specified function and parameters for the given class to avoid
 * scroll animations that delay element property updates.
 *
 * This function has been adapted from `disableAnimationBehavior` in
 * chrome/test/data/webui/tab_search/tab_search_test_helper.ts.
 */
function disableScrollAnimation(element: any) {
  const originalFunction = element.prototype['scrollIntoView'];
  element.prototype['scrollIntoView'] = function(options: any) {
    const args = [];
    if (typeof options === 'object' && options !== null) {
      const noAnimationOptions = Object.assign({}, options);
      // Delete the `behavior` property from the args, since that's the property
      // that enables smooth scrolling.
      delete noAnimationOptions.behavior;

      args.push(noAnimationOptions);
    }
    originalFunction.apply(this, args);
  };
}

suite('ShortcutsPageTest', function() {
  let shortcutsPageElement: ShortcutsPageElement|null = null;

  let manager: AcceleratorLookupManager|null = null;

  teardown(() => {
    if (manager) {
      manager.reset();
    }
    if (shortcutsPageElement) {
      shortcutsPageElement.remove();
    }
    shortcutsPageElement = null;

    // Reset the scroll position to the top of the page since the tests depend
    // on it.
    window.scrollTo({top: 0});
  });

  test('AcceleratorSubsections render correctly', () => {
    const numFakeAccelerators = 10;
    const numFirstCategoryAccelerators = 4;
    manager = initManagerWithFakeData(
        numFakeAccelerators, numFirstCategoryAccelerators);

    shortcutsPageElement =
        initShortcutsPageElement(AcceleratorCategory.kGeneral);
    flush();
    const subsections = shortcutsPageElement.shadowRoot?.querySelectorAll(
        'accelerator-subsection');
    assertTrue(!!subsections);
    assertEquals(2, subsections.length);

    const rowsForFirstSubsection =
        subsections.item(0).shadowRoot?.querySelectorAll('accelerator-row');
    assertTrue(!!rowsForFirstSubsection);
    assertEquals(numFirstCategoryAccelerators, rowsForFirstSubsection.length);

    const rowsForSecondSubsection =
        subsections.item(1).shadowRoot?.querySelectorAll('accelerator-row');
    assertTrue(!!rowsForSecondSubsection);
    assertEquals(
        numFakeAccelerators - numFirstCategoryAccelerators,
        rowsForSecondSubsection.length);
  });

  test('ScrollIntoView works when route changes', async () => {
    // Create enough accelerators so that the last accelerator is out of view.
    const numFakeAccelerators = 40;
    const numFirstCategoryAccelerators = 10;
    manager = initManagerWithFakeData(
        numFakeAccelerators, numFirstCategoryAccelerators);

    shortcutsPageElement =
        initShortcutsPageElement(AcceleratorCategory.kGeneral);
    flush();
    const subsections = shortcutsPageElement.shadowRoot?.querySelectorAll(
        'accelerator-subsection');
    assertTrue(!!subsections);

    const rowsForSecondSubsection =
        subsections.item(1).shadowRoot?.querySelectorAll('accelerator-row');
    assertTrue(!!rowsForSecondSubsection);
    const lastAcceleratorRow =
        rowsForSecondSubsection.item(rowsForSecondSubsection.length - 1);

    // The AcceleratorRow is at the bottom of the page, so it's hidden.
    assertFalse(isVisibleVerticallyInViewport(lastAcceleratorRow));

    // Disable smooth scroll so that the scroll completes instantly.
    disableScrollAnimation(AcceleratorRowElement);
    shortcutsPageElement.setScrollTimeoutForTesting(/*timeout=*/ 0);

    // Trigger onRouteChanged as if the user had selected a SearchResultRow.
    shortcutsPageElement.onRouteChanged(new URL(`${SHORTCUTS_APP_URL}?action=${
        lastAcceleratorRow.action}&category=${AcceleratorCategory.kGeneral}`));

    await flushTasks();
    // After `onRouteChanged`, the AcceleratorRow is now visible.
    assertTrue(isVisibleVerticallyInViewport(lastAcceleratorRow));
    // TODO(longbowei): Add test to verify lastAcceleratorRow is focused.
  });

  test('ScrollIntoView works when page changes', async () => {
    // Create enough accelerators so that the last accelerator is out of view.
    const numFakeAccelerators = 40;
    manager = initManagerWithFakeData(numFakeAccelerators, 10);

    shortcutsPageElement =
        initShortcutsPageElement(AcceleratorCategory.kGeneral);
    flush();
    const subsections = shortcutsPageElement.shadowRoot?.querySelectorAll(
        'accelerator-subsection');
    assertTrue(!!subsections);

    const rowsForSecondSubsection =
        subsections.item(1).shadowRoot?.querySelectorAll('accelerator-row');
    assertTrue(!!rowsForSecondSubsection);
    const lastAcceleratorRow =
        rowsForSecondSubsection.item(rowsForSecondSubsection.length - 1);

    // The AcceleratorRow is at the bottom of the page, so it's hidden.
    assertFalse(isVisibleVerticallyInViewport(lastAcceleratorRow));

    // Disable smooth scroll so that the scroll completes instantly.
    disableScrollAnimation(AcceleratorRowElement);
    shortcutsPageElement.setScrollTimeoutForTesting(/*timeout=*/ 0);

    // Update the URL of the app and trigger onNavigationPageChanged as if the
    // user had selected a SearchResultRow.
    window.history.pushState(
        {}, '',
        `${SHORTCUTS_APP_URL}?action=${lastAcceleratorRow.action}&category=${
            AcceleratorCategory.kGeneral}`);
    shortcutsPageElement.onNavigationPageChanged({isActive: true});

    await waitAfterNextRender(shortcutsPageElement);

    // After `onNavigationPageChanged`, the AcceleratorRow is now
    // visible.
    assertTrue(isVisibleVerticallyInViewport(lastAcceleratorRow));
    // TODO(longbowei): Add test to verify lastAcceleratorRow is focused.
  });
});