chromium/chrome/test/data/webui/history/history_product_specifications_list_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://history/history.js';

import {ensureLazyLoaded, ShoppingBrowserProxyImpl} from 'chrome://history/history.js';
import type {CrButtonElement, CrCheckboxElement, ProductSpecificationsListsElement} from 'chrome://history/history.js';
import {ShoppingPageCallbackRouter} from 'chrome://history/history.js';
import {getDeepActiveElement} from 'chrome://resources/js/util.js';
import {assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {pressAndReleaseKeyOn} from 'chrome://webui-test/keyboard_mock_interactions.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {TestMock} from 'chrome://webui-test/test_mock.js';


suite('ProductSpecificationsListTest', () => {
  const shoppingServiceApi = TestMock.fromClass(ShoppingBrowserProxyImpl);
  let productSpecificationsList: ProductSpecificationsListsElement;

  const callbackRouter = new ShoppingPageCallbackRouter();
  const callbackRouterRemote = callbackRouter.$.bindNewPipeAndPassRemote();

  function createProductSpecsList(): ProductSpecificationsListsElement {
    productSpecificationsList =
        document.createElement('product-specifications-lists');
    document.body.appendChild(productSpecificationsList);
    return productSpecificationsList;
  }

  function initProductSets() {
    shoppingServiceApi.setResultFor(
        'getAllProductSpecificationsSets', Promise.resolve({
          sets: [
            {
              name: 'example1',
              uuid: {value: 'ex1'},
              urls: [{url: 'dot com 1'}],
            },
            {
              name: 'example2',
              uuid: {value: 'ex2'},
              urls: [{url: 'dot com 2a'}, {url: 'dot com 2b'}],
            },
            {
              name: 'example3',
              uuid: {value: 'ex3'},
              urls: [{url: 'dot com 3a'}, {url: 'dot com 3b'}],
            },
            {
              name: 'example4',
              uuid: {value: 'ex4'},
              urls: [{url: 'dot com 4a'}, {url: 'dot com 4b'}],
            },
          ],
        }));
  }

  function initProductSpecsState() {
    shoppingServiceApi.setResultFor(
        'getProductSpecificationsFeatureState', Promise.resolve({
          state: {
            isSyncingTabCompare: true,
            canLoadFullPageUi: true,
            canManageSets: true,
            canFetchData: true,
            isAllowedForEnterprise: true,
          },
        }));
  }

  setup(function() {
    shoppingServiceApi.reset();
    shoppingServiceApi.setResultFor('getCallbackRouter', callbackRouter);
    ShoppingBrowserProxyImpl.setInstance(shoppingServiceApi);
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    initProductSets();
    initProductSpecsState();
    createProductSpecsList();
    return Promise.all([flushTasks]).then(() => {
      shoppingServiceApi.whenCalled('getAllProductSpecificationsSets');
      shoppingServiceApi.whenCalled('getProductSpecificationsFeatureState');
    });
  });

  test('load', async () => {
    const items = productSpecificationsList.shadowRoot!.querySelectorAll(
        'product-specifications-item');
    assertEquals(4, items.length);
    const firstItem = items[0]!.item;
    assertEquals('example1', firstItem.name);
    assertEquals('ex1', firstItem.uuid.value);
    assertDeepEquals([{url: 'dot com 1'}], firstItem.urls);

    const secondItem = items[1]!.item;
    assertEquals('example2', secondItem.name);
    assertEquals('ex2', secondItem.uuid.value);
    assertDeepEquals(
        [{url: 'dot com 2a'}, {url: 'dot com 2b'}], secondItem.urls);

    assertDeepEquals(
        {
          name: 'example3',
          uuid: {value: 'ex3'},
          urls: [{url: 'dot com 3a'}, {url: 'dot com 3b'}],
        },
        items[2]!.item);
    assertDeepEquals(
        {
          name: 'example4',
          uuid: {value: 'ex4'},
          urls: [{url: 'dot com 4a'}, {url: 'dot com 4b'}],
        },
        items[3]!.item);
  });

  test('displays correct header', async () => {
    const items = productSpecificationsList.shadowRoot!.querySelectorAll(
        'product-specifications-item');
    assertEquals(4, items.length);

    const cardTitleHeader = productSpecificationsList.shadowRoot!.querySelector(
        '#card-title-header');
    assertTrue(!!cardTitleHeader);
    const heading = cardTitleHeader!.textContent;
    assertTrue(!!heading);
    assertEquals('Comparison tables', heading.trim());
  });


  test(
      'selecting product set adds uuid to selected items list',
      async function() {
        const items = productSpecificationsList.shadowRoot!.querySelectorAll(
            'product-specifications-item');
        assertDeepEquals(new Set(), productSpecificationsList.selectedItems);

        const secondItem = items[1]!;
        const checkbox = secondItem.$.checkbox as CrCheckboxElement;
        checkbox.click();

        await checkbox.updateComplete;
        assertDeepEquals(
            new Set(['ex2']), productSpecificationsList.selectedItems);
      });

  test('consuming menu event opens menu', async function() {
    await ensureLazyLoaded();
    const menu = productSpecificationsList.$.sharedMenu.get();
    assertFalse(menu.open);

    const items = productSpecificationsList.shadowRoot!.querySelectorAll(
        'product-specifications-item');
    assertEquals(4, items.length);

    const secondItem = items[1]!;
    secondItem.dispatchEvent(new CustomEvent('item-menu-open', {
      bubbles: true,
      composed: true,
      detail: {target: secondItem, uuid: {value: 'ex2'}},
    }));
    assertTrue(menu.open);

    const button = menu.querySelector('button');
    assertTrue(!!button);
    const buttonText = button!.textContent;
    assertTrue(!!buttonText);
    assertEquals('Remove from tables', buttonText.trim());
  });

  test('clicking remove on menu removes the correct uuid', async function() {
    await ensureLazyLoaded();
    const items = productSpecificationsList.shadowRoot!.querySelectorAll(
        'product-specifications-item');
    assertEquals(4, items.length);
    const firstItem = items[0]!;
    firstItem.dispatchEvent(new CustomEvent('item-menu-open', {
      bubbles: true,
      composed: true,
      detail: {target: firstItem, uuid: {value: 'ex1'}},
    }));

    const menu = productSpecificationsList.$.sharedMenu.get();
    const button = menu.querySelector('button');
    assertTrue(!!button);
    button.click();
    assertEquals(
        1, shoppingServiceApi.getCallCount('deleteProductSpecificationsSet'));
    assertDeepEquals(
        {value: 'ex1'},
        shoppingServiceApi.getArgs('deleteProductSpecificationsSet')[0]);
  });

  test('delete dialog renders', async function() {
    await ensureLazyLoaded();
    const dialog = productSpecificationsList.$.deleteItemDialog.get();
    assertFalse(dialog.open);

    productSpecificationsList.deleteSelectedWithPrompt();

    assertTrue(dialog.open);
    const title = dialog.querySelector('#title');
    const body = dialog.querySelector('#body');
    const cancelButton = dialog.querySelector('.cancel-button');
    const actionButton = dialog.querySelector('.action-button');
    assertTrue(!!title);
    assertTrue(!!body);
    assertTrue(!!cancelButton);
    assertTrue(!!actionButton);
  });

  test('delete dialog cancel button closes dialog', async function() {
    await ensureLazyLoaded();
    const dialog = productSpecificationsList.$.deleteItemDialog.get();
    productSpecificationsList.deleteSelectedWithPrompt();
    const cancelButton = dialog.querySelector<HTMLElement>('.cancel-button');
    assertTrue(!!cancelButton);

    cancelButton.click();

    assertFalse(dialog.open);
  });

  test('delete dialog action button calls delete', async function() {
    await ensureLazyLoaded();

    const items = productSpecificationsList.shadowRoot!.querySelectorAll(
        'product-specifications-item');
    const checkbox0 = items[0]!.$.checkbox as CrCheckboxElement;
    checkbox0.click();
    await checkbox0.updateComplete;
    const checkbox1 = items[1]!.$.checkbox as CrCheckboxElement;
    checkbox1.click();
    await checkbox1.updateComplete;
    assertDeepEquals(
        new Set(['ex1', 'ex2']), productSpecificationsList.selectedItems);

    const dialog = productSpecificationsList.$.deleteItemDialog.get();
    productSpecificationsList.deleteSelectedWithPrompt();
    const actionButton = dialog.querySelector<HTMLElement>('.action-button');
    assertTrue(!!actionButton);
    actionButton.click();

    assertEquals(
        2, shoppingServiceApi.getCallCount('deleteProductSpecificationsSet'));
    assertDeepEquals(
        {value: 'ex1'},
        shoppingServiceApi.getArgs('deleteProductSpecificationsSet')[0]);
    assertDeepEquals(
        {value: 'ex2'},
        shoppingServiceApi.getArgs('deleteProductSpecificationsSet')[1]);
  });

  test('focus with arrow keys', async () => {
    const items = productSpecificationsList.shadowRoot!.querySelectorAll(
        'product-specifications-item');

    const focusGrid = productSpecificationsList.getFocusGridForTesting();
    assertTrue(!!focusGrid);
    assertEquals(4, focusGrid.rows.length);

    const focusedCheckbox = items[0]!.$.checkbox.getFocusableElement();
    focusedCheckbox.focus();
    assertEquals(focusedCheckbox, getDeepActiveElement());
    assertTrue(focusGrid.rows[0]!.isActive());

    // Press the down arrow to focus on the checkbox in the row below.
    pressAndReleaseKeyOn(focusedCheckbox, 40, [], 'ArrowDown');
    const nextFocusedCheckbox = items[1]!.$.checkbox.getFocusableElement();
    assertEquals(nextFocusedCheckbox, getDeepActiveElement());
    assertTrue(focusGrid.rows[1]!.isActive());
    assertFalse(focusGrid.rows[0]!.isActive());

    // Press the right arrow to focus on the link in the same row.
    pressAndReleaseKeyOn(nextFocusedCheckbox, 39, [], 'ArrowRight');
    const focusedLink = items[1]!.$.link;
    assertEquals(focusedLink, getDeepActiveElement());
    assertTrue(focusGrid.rows[1]!.isActive());
    assertFalse(focusGrid.rows[0]!.isActive());
  });


  test('update list in response to observers', async () => {
    callbackRouterRemote.onProductSpecificationsSetUpdated({
      name: 'example1',
      urls: [{url: 'dot com 1'}, {url: 'dot com 2'}],
      uuid: {value: 'ex1'},
    });
    callbackRouterRemote.onProductSpecificationsSetRemoved({value: 'ex2'});
    callbackRouterRemote.onProductSpecificationsSetAdded({
      name: 'example5',
      urls: [{url: 'dot com 5'}, {url: 'dot com 6'}],
      uuid: {value: 'ex5'},
    });
    await flushTasks();

    const items = productSpecificationsList.shadowRoot!.querySelectorAll(
        'product-specifications-item');
    assertEquals(4, items.length);
    assertDeepEquals(
        {
          name: 'example1',
          uuid: {value: 'ex1'},
          urls: [{url: 'dot com 1'}, {url: 'dot com 2'}],
        },
        items[0]!.item);
    assertDeepEquals(
        {
          name: 'example3',
          uuid: {value: 'ex3'},
          urls: [{url: 'dot com 3a'}, {url: 'dot com 3b'}],
        },
        items[1]!.item);
    assertDeepEquals(
        {
          name: 'example5',
          uuid: {value: 'ex5'},
          urls: [{url: 'dot com 5'}, {url: 'dot com 6'}],
        },
        items[3]!.item);
  });

  test('shift checkbox causes multi select', async function() {
    await ensureLazyLoaded();
    const items = productSpecificationsList.shadowRoot!.querySelectorAll(
        'product-specifications-item');
    assertDeepEquals(new Set(), productSpecificationsList.selectedItems);

    const firstItem = items[1]!;
    firstItem.dispatchEvent(new CustomEvent('product-spec-item-select', {
      bubbles: true,
      composed: true,
      detail: {
        checked: true,
        shiftKey: false,
        uuid: 'ex2',
        index: 1,
      },
    }));

    const lastItem = items[3]!;
    lastItem.dispatchEvent(new CustomEvent('product-spec-item-select', {
      bubbles: true,
      composed: true,
      detail: {
        checked: true,
        shiftKey: true,
        uuid: 'ex4',
        index: 3,
      },
    }));

    await flushTasks();
    assertDeepEquals(
        new Set(['ex2', 'ex3', 'ex4']),
        productSpecificationsList.selectedItems);
  });

  test('search term changed', async function() {
    await ensureLazyLoaded();
    const items = productSpecificationsList.shadowRoot!.querySelectorAll(
        'product-specifications-item');
    assertEquals(4, items.length);
    initProductSets();

    productSpecificationsList.searchTerm = 'example2';
    await flushTasks();
    const newItems = productSpecificationsList.shadowRoot!.querySelectorAll(
        'product-specifications-item');

    assertEquals(1, newItems!.length);
    assertDeepEquals('example2', newItems[0]!.item.name);
  });

  test('empty message renders when list empty', async function() {
    // Reset shoppingAPI to return no product sets.
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    shoppingServiceApi.reset();
    shoppingServiceApi.setResultFor(
        'getAllProductSpecificationsSets', Promise.resolve({sets: []}));
    productSpecificationsList =
        document.createElement('product-specifications-lists');
    document.body.appendChild(productSpecificationsList);
    await ensureLazyLoaded();
    await flushTasks();

    const items = productSpecificationsList.shadowRoot!.querySelectorAll(
        'product-specifications-item');
    assertEquals(0, items.length);
    const emptyMessage = productSpecificationsList.shadowRoot!.querySelector(
        '.centered-message');
    assertTrue(!!emptyMessage);
  });

  test('sync button click and message when not synced', async function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    shoppingServiceApi.reset();
    initProductSets();
    shoppingServiceApi.setResultFor(
        'getProductSpecificationsFeatureState', Promise.resolve({
          state: {
            isSyncingTabCompare: false,
            canLoadFullPageUi: true,
            canManageSets: true,
            canFetchData: true,
            isAllowedForEnterprise: true,
            isSignedIn: false,
          },
        }));
    shoppingServiceApi.setResultFor(
        'getAllProductSpecificationsSets', Promise.resolve({sets: []}));
    shoppingServiceApi.setResultFor('getCallbackRouter', callbackRouter);

    productSpecificationsList =
        document.createElement('product-specifications-lists');
    document.body.appendChild(productSpecificationsList);
    await ensureLazyLoaded();
    await flushTasks();

    const items = productSpecificationsList.shadowRoot!.querySelectorAll(
        'product-specifications-item');
    assertEquals(0, items.length);
    const imageTextContainer =
        productSpecificationsList.shadowRoot!.querySelector<HTMLElement>(
            '#sync-or-error-message-picture-and-text-container');
    assertTrue(!!imageTextContainer);
    assertFalse(imageTextContainer.hidden);
    const syncButton =
        productSpecificationsList.shadowRoot!.querySelector<CrButtonElement>(
            '#turn-on-sync-button');
    assertTrue(!!syncButton);

    syncButton.click();
    shoppingServiceApi.whenCalled('showSyncSetupFlow');
  });

  test('error message displays', async function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    shoppingServiceApi.reset();
    initProductSets();
    shoppingServiceApi.setResultFor(
        'getProductSpecificationsFeatureState', Promise.resolve({
          state: {
            isSyncingTabCompare: true,
            canLoadFullPageUi: true,
            canManageSets: false,
            canFetchData: false,
            isAllowedForEnterprise: false,
          },
        }));
    shoppingServiceApi.setResultFor(
        'getAllProductSpecificationsSets', Promise.resolve({sets: []}));
    shoppingServiceApi.setResultFor('getCallbackRouter', callbackRouter);

    productSpecificationsList =
        document.createElement('product-specifications-lists');
    document.body.appendChild(productSpecificationsList);
    await ensureLazyLoaded();
    await flushTasks();
    shoppingServiceApi.whenCalled('getProductSpecificationsFeatureState');

    const items = productSpecificationsList.shadowRoot!.querySelectorAll(
        'product-specifications-item');
    assertEquals(0, items.length);
    const imageTextContainer =
        productSpecificationsList.shadowRoot!.querySelector<HTMLElement>(
            '#sync-or-error-message-picture-and-text-container');
    assertTrue(!!imageTextContainer);
    assertFalse(imageTextContainer.hidden);
    const errorMessage =
        productSpecificationsList.shadowRoot!.querySelector<HTMLElement>(
            '#error-message');
    assertTrue(!!errorMessage);
    assertFalse(errorMessage.hidden);
  });
});