chromium/chrome/test/data/webui/commerce/product_specifications/table_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://compare/table.js';

import type {TableElement} from 'chrome://compare/table.js';
import {WindowProxy} from 'chrome://compare/window_proxy.js';
import {BrowserProxyImpl} from 'chrome://resources/cr_components/commerce/browser_proxy.js';
import type {CrAutoImgElement} from 'chrome://resources/cr_elements/cr_auto_img/cr_auto_img.js';
import {getFaviconForPageURL} from 'chrome://resources/js/icon.js';
import {assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks, waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
import {TestMock} from 'chrome://webui-test/test_mock.js';
import {eventToPromise, isVisible} from 'chrome://webui-test/test_util.js';

import {$$, assertNotStyle, assertStyle, installMock} from './test_support.js';

suite('ProductSpecificationsTableTest', () => {
  let tableElement: TableElement;
  let windowProxy: TestMock<WindowProxy>;
  const shoppingServiceApi = TestMock.fromClass(BrowserProxyImpl);

  setup(async () => {
    shoppingServiceApi.reset();
    BrowserProxyImpl.setInstance(shoppingServiceApi);
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    windowProxy = installMock(WindowProxy);
    windowProxy.setResultFor('onLine', true);
    tableElement = document.createElement('product-specifications-table');
    document.body.appendChild(tableElement);
  });

  test('column count correct', async () => {
    // Arrange / Act.
    tableElement.columns = [
      {
        selectedItem:
            {title: 'title', url: 'https://example.com/1', imageUrl: ''},
        productDetails: [],

      },
      {
        selectedItem:
            {title: 'title2', url: 'https://example.com/2', imageUrl: ''},
        productDetails: [],
      },
    ];
    await waitAfterNextRender(tableElement);

    // Assert.
    const columns = tableElement.shadowRoot!.querySelectorAll('.col');
    assertEquals(2, columns.length);
    assertEquals('2', tableElement.style.getPropertyValue('--num-columns'));

    const detailContainers =
        tableElement.shadowRoot!.querySelectorAll('.detail-container');
    assertEquals(0, detailContainers.length);
  });

  test('images are displayed', async () => {
    // Arrange / Act.
    tableElement.columns = [
      {
        selectedItem: {
          title: 'item1',
          url: 'https://example.com/1',
          imageUrl: 'https://foo.com/image',
        },
        productDetails: [],
      },
      {
        selectedItem: {
          title: 'item2',
          url: 'https://example.com/2',
          imageUrl: 'https://bar.com/image',
        },
        productDetails: [],
      },
    ];
    await waitAfterNextRender(tableElement);

    // Assert.
    const images =
        tableElement.shadowRoot!.querySelectorAll<CrAutoImgElement>('.col img');
    assertEquals(2, images.length);
    assertEquals(
        tableElement.columns[0]!.selectedItem.imageUrl, images[0]!.autoSrc);
    assertEquals(
        tableElement.columns[1]!.selectedItem.imageUrl, images[1]!.autoSrc);

    // Ensure the favicon isn't showing.
    const faviconMainImage = $$<HTMLElement>(tableElement, '.favicon');
    assertFalse(!!faviconMainImage);
  });

  test('fallback images are displayed', async () => {
    // Arrange / Act.
    tableElement.columns = [
      {
        selectedItem: {
          title: 'item1',
          url: 'https://example.com/',
          // Intentionally leave this URL empty so the fallback is used.
          imageUrl: '',
        },
        productDetails: [],
      },
    ];
    await waitAfterNextRender(tableElement);

    // Assert.
    const faviconMainImage = $$<HTMLElement>(tableElement, '.favicon');
    assertTrue(!!faviconMainImage);
    assertEquals(
        getFaviconForPageURL('https://example.com/', false, '', 32),
        faviconMainImage.style.backgroundImage);

    // Ensure the alternate image tag isn't showing.
    const crAutoImg = $$<CrAutoImgElement>(tableElement, '.col img');
    assertFalse(!!crAutoImg);
  });

  test('product rows show the correct data', async () => {
    // Arrange.
    const productDetails1 = [
      {
        title: 'foo',
        content: {
          attributes: [{label: '', value: 'fooDescription1'}],
          summary: [{
            text: 'fooSummary',
            urls: [
              {
                url: {url: 'http://example.com/citation1'},
                title: '',
                faviconUrl: {url: ''},
                thumbnailUrl: {url: ''},
              },
              {
                url: {url: 'http://example.com/citation2'},
                title: '',
                faviconUrl: {url: ''},
                thumbnailUrl: {url: ''},
              },
            ],
          }],
        },
      },
      {
        title: 'bar',
        content: {
          attributes: [{label: '', value: 'N/A'}],
          summary: [{
            text: 'barSummary',
            urls: [],
          }],
        },
      },
    ];
    const productDetails2 = [
      {
        title: 'foo',
        content: {
          attributes: [{label: 'label', value: 'fooDescription2'}],
          summary: [{
            text: 'fooSummary2',
            urls: [
              {
                url: {url: 'http://example.com/citation1'},
                title: '',
                faviconUrl: {url: ''},
                thumbnailUrl: {url: ''},
              },
            ],
          }],
        },
      },
      {
        title: 'bar',
        content: {
          attributes: [{label: '', value: 'barDescription2'}],
          summary: [{
            text: 'barSummary2',
            urls: [],
          }],
        },
      },
    ];
    // Act.
    tableElement.columns = [
      {
        productDetails: productDetails1,
        selectedItem: {title: '', url: 'https://foo.com', imageUrl: ''},
      },
      {
        productDetails: productDetails2,
        selectedItem: {title: '', url: 'https://bar.com', imageUrl: ''},
      },
    ];
    await waitAfterNextRender(tableElement);

    // Assert.
    const titles =
        tableElement.shadowRoot!.querySelectorAll('.detail-title span');
    assertEquals(4, titles.length);
    // Titles should only show in the first column.
    assertNotStyle(titles[0]!, 'visibility', 'hidden');
    assertNotStyle(titles[1]!, 'visibility', 'hidden');
    assertStyle(titles[2]!, 'visibility', 'hidden');
    assertStyle(titles[3]!, 'visibility', 'hidden');
    assertTrue(!!titles[0]!.textContent);
    assertEquals(productDetails1[0]!.title, titles[0]!.textContent.trim());
    assertTrue(!!titles[1]!.textContent);
    assertEquals(productDetails1[1]!.title, titles[1]!.textContent.trim());

    const descriptions =
        tableElement.shadowRoot!.querySelectorAll('description-section');
    assertEquals(4, descriptions.length);
    assertDeepEquals(productDetails1[0]!.content, descriptions[0]!.description);
    assertDeepEquals(productDetails1[1]!.content, descriptions[1]!.description);
    assertDeepEquals(productDetails2[0]!.content, descriptions[2]!.description);
    assertDeepEquals(productDetails2[1]!.content, descriptions[3]!.description);
  });

  test('product rows show "text" section', async () => {
    // Arrange.
    const productDetails = [
      {
        title: 'price',
        content: '$100',
      },
    ];
    // Act.
    tableElement.columns = [
      {
        productDetails: productDetails,
        selectedItem: {title: '', url: 'https://foo.com', imageUrl: ''},
      },
    ];
    await waitAfterNextRender(tableElement);

    // Assert.
    const text = $$(tableElement, '.detail-text');
    assertTrue(!!text);
    // Titles should only show in the first column.
    assertNotStyle(text, 'visibility', 'hidden');
    assertTrue(!!text!.textContent);
    assertEquals(productDetails[0]!.content, text!.textContent.trim());
  });

  test('fires url change event', async () => {
    // Arrange
    tableElement.columns = [
      {
        selectedItem: {title: 'title', url: 'https://foo.com', imageUrl: ''},
        productDetails: [],
      },
      {
        selectedItem: {title: 'title2', url: 'https://bar.com', imageUrl: ''},
        productDetails: [],
      },
    ];
    await waitAfterNextRender(tableElement);

    // Act
    const productSelector = $$(tableElement, 'product-selector');
    assertTrue(!!productSelector);
    const eventPromise = eventToPromise('url-change', tableElement);
    productSelector!.dispatchEvent(new CustomEvent('selected-url-change', {
      detail: {
        url: 'https://foo.com',
      },
    }));

    // Assert.
    const event = await eventPromise;
    assertTrue(!!event);
    assertEquals('https://foo.com', event.detail.url);
    assertEquals(0, event.detail.index);
  });

  test('fires url remove event', async () => {
    // Arrange
    tableElement.columns = [
      {
        selectedItem: {title: 'title', url: 'https://foo.com', imageUrl: ''},
        productDetails: [],
      },
      {
        selectedItem: {title: 'title2', url: 'https://bar.com', imageUrl: ''},
        productDetails: [],
      },
    ];
    await waitAfterNextRender(tableElement);
    const productSelector = $$(tableElement, 'product-selector');
    assertTrue(!!productSelector);
    const eventPromise = eventToPromise('url-remove', tableElement);
    productSelector!.dispatchEvent(new CustomEvent('remove-url'));

    // Assert.
    const event = await eventPromise;
    assertTrue(!!event);
    assertEquals(0, event.detail.index);
  });

  test('opens tab when `openTabButton` is clicked', async () => {
    // Arrange
    const testUrl = 'https://example.com';
    tableElement.columns = [
      {
        selectedItem: {
          title: 'title',
          url: testUrl,
          imageUrl: 'https://example.com/image',
        },
        productDetails: [],
      },
      {
        selectedItem: {
          title: 'title2',
          url: 'https://example.com/2',
          imageUrl: 'https://example.com/2/image',
        },
        productDetails: [],
      },
    ];
    await waitAfterNextRender(tableElement);

    // Act
    const openTabButton = $$<HTMLElement>(tableElement, '.open-tab-button');
    assertTrue(!!openTabButton);
    openTabButton!.click();

    // Assert.
    assertEquals(1, shoppingServiceApi.getCallCount('switchToOrOpenTab'));
    assertEquals(
        testUrl, shoppingServiceApi.getArgs('switchToOrOpenTab')[0].url);
  });

  test('shows open tab button when hovered', async () => {
    // Arrange
    tableElement.columns = [
      {
        selectedItem: {
          title: 'title',
          url: 'https://example.com',
          imageUrl: 'https://example.com/image',
        },
        productDetails: [
          {
            title: 'foo',
            content: {
              attributes: [{label: '', value: 'fooDescription'}],
              summary: [{
                text: 'fooSummary',
                urls: [],
              }],
            },
          },
          {
            title: 'bar',
            content: {
              attributes: [{label: '', value: 'barDescription'}],
              summary: [{
                text: 'barSummary',
                urls: [],
              }],
            },
          },
        ],
      },
      {
        selectedItem: {
          title: 'title2',
          url: 'https://example.com/2',
          imageUrl: 'https://example.com/2/image',
        },
        productDetails: [
          {
            title: 'foo',
            content: {
              attributes: [{label: '', value: 'fooDescription1'}],
              summary: [{
                text: 'fooSummary1',
                urls: [],
              }],
            },
          },
        ],
      },
    ];
    await flushTasks();
    const columns = tableElement.shadowRoot!.querySelectorAll('.col');
    assertEquals(2, columns.length);
    const openTabButton1 =
        columns[0]!.querySelector<HTMLElement>('.open-tab-button');
    const openTabButton2 =
        columns[1]!.querySelector<HTMLElement>('.open-tab-button');
    assertTrue(!!openTabButton1);
    assertTrue(!!openTabButton2);
    tableElement.$.table.dispatchEvent(new PointerEvent('pointerleave'));
    assertFalse(isVisible(openTabButton1));
    assertFalse(isVisible(openTabButton2));

    // Act/Assert
    columns[0]!.dispatchEvent(new PointerEvent('pointerenter'));
    assertTrue(isVisible(openTabButton1));
    assertFalse(isVisible(openTabButton2));

    columns[1]!.dispatchEvent(new PointerEvent('pointerenter'));
    assertFalse(isVisible(openTabButton1));
    assertTrue(isVisible(openTabButton2));

    tableElement.$.table.dispatchEvent(new PointerEvent('pointerleave'));
    assertFalse(isVisible(openTabButton1));
    assertFalse(isVisible(openTabButton2));
  });

  test(
      'clicking `openTabButton` while offline fires ' +
          '`unavailable-action-attempted` event',
      async () => {
        // Arrange
        tableElement.columns = [
          {
            selectedItem: {
              title: 'title',
              url: 'https://example.com',
              imageUrl: 'https://example.com/image',
            },
            productDetails: [],
          },
          {
            selectedItem: {
              title: 'title2',
              url: 'https://example.com/2',
              imageUrl: 'https://example.com/2/image',
            },
            productDetails: [],
          },
        ];
        await waitAfterNextRender(tableElement);

        // Act
        windowProxy.setResultFor('onLine', false);
        const openTabButton = $$<HTMLElement>(tableElement, '.open-tab-button');
        assertTrue(!!openTabButton);
        const eventPromise =
            eventToPromise('unavailable-action-attempted', tableElement);
        openTabButton.click();

        // Assert
        const event = await eventPromise;
        assertTrue(!!event);
        assertEquals(0, shoppingServiceApi.getCallCount('switchToOrOpenTab'));
      });

  test('descriptions hidden if empty or N/A', async () => {
    // Arrange
    tableElement.columns = [
      {
        selectedItem: {
          title: 'title',
          url: 'https://example.com',
          imageUrl: 'https://example.com/image',
        },
        productDetails: [{
          title: 'foo',
          content: {
            attributes: [],
            summary: [{
              text: 'foo1',
              urls: [],
            }],
          },
        }],
      },
      {
        selectedItem: {
          title: 'title2',
          url: 'https://example.com/2',
          imageUrl: 'https://example.com/2/image',
        },
        productDetails: [{
          title: 'foo',
          content: {
            attributes: [{label: '', value: 'N/A'}],
            summary: [{
              text: 'foo2',
              urls: [],
            }],
          },
        }],
      },
    ];
    await waitAfterNextRender(tableElement);
    const descriptions =
        tableElement.shadowRoot!.querySelectorAll('description-text');
    assertEquals(0, descriptions.length);
  });

  test('summaries hidden if empty or N/A', async () => {
    // Arrange
    tableElement.columns = [
      {
        selectedItem: {
          title: 'title',
          url: 'https://example.com',
          imageUrl: 'https://example.com/image',
        },
        productDetails: [{
          title: 'foo',
          content: {
            attributes: [{label: '', value: 'foo1'}],
            summary: [{
              text: 'N/A',
              urls: [],
            }],
          },
        }],
      },
      {
        selectedItem: {
          title: 'title2',
          url: 'https://example.com/2',
          imageUrl: 'https://example.com/2/image',
        },
        productDetails: [{
          title: 'foo',
          content: {
            attributes: [{label: '', value: 'foo2'}],
            summary: [],
          },
        }],
      },
    ];
    await waitAfterNextRender(tableElement);
    const descriptions =
        tableElement.shadowRoot!.querySelectorAll('description-section');
    descriptions.forEach(description => {
      assertEquals(0, description.description.summary.length);
    });
  });

  test('details hidden if no valid summaries or descriptions', async () => {
    // Arrange
    tableElement.columns = [
      {
        selectedItem: {
          title: 'title',
          url: 'https://example.com',
          imageUrl: 'https://example.com/image',
        },
        productDetails: [{
          title: 'foo',
          content: {
            attributes: [],
            summary: [{
              text: 'N/A',
              urls: [],
            }],
          },
        }],
      },
      {
        selectedItem: {
          title: 'title2',
          url: 'https://example.com/2',
          imageUrl: 'https://example.com/2/image',
        },
        productDetails: [{
          title: 'foo',
          content: {
            attributes: [{label: '', value: 'N/A'}],
            summary: [{
              text: 'N/A',
              urls: [],
            }],
          },
        }],
      },
    ];
    await waitAfterNextRender(tableElement);
    const details =
        tableElement.shadowRoot!.querySelectorAll('.detail-container');
    assertEquals(2, details.length);
    assertFalse(isVisible((details[0]!)));
    assertFalse(isVisible((details[1]!)));
  });

  suite('Buying options', () => {
    setup(async () => {
      tableElement.columns = [
        {
          selectedItem: {
            title: 'title',
            url: 'https://example.com',
            imageUrl: 'https://example.com/image',
          },
          productDetails: [
            {
              title: 'foo',
              content: 'summary1',
            },
            {title: null, content: {jackpotUrl: 'https://example.com/jackpot'}},
          ],
        },
        {
          selectedItem: {
            title: 'title2',
            url: 'https://example.com/2',
            imageUrl: 'https://example.com/2/image',
          },
          productDetails: [
            {title: 'foo', content: 'summary2'},
            {title: null, content: {jackpotUrl: ''}},
          ],
        },
      ];
      await waitAfterNextRender(tableElement);
    });

    test('detail title is hidden if it is `null`', async () => {
      const titles = tableElement.shadowRoot!.querySelectorAll('.detail-title');
      assertEquals(2, titles.length);
    });

    test('buying options are visible if any are available', async () => {
      const buyingOptions =
          tableElement.shadowRoot!.querySelectorAll('buying-options-section');
      assertEquals(1, buyingOptions.length);
      assertEquals('https://example.com/jackpot', buyingOptions[0]!.jackpotUrl);
    });

    test('buying options are hidden if none are available', async () => {
      assertTrue(!!tableElement.columns[0]);
      tableElement.columns = [
        {
          selectedItem: {
            title: 'title',
            url: 'https://example.com',
            imageUrl: 'https://example.com/image',
          },
          productDetails: [
            {title: null, content: {jackpotUrl: ''}},
          ],
        },
        {
          selectedItem: {
            title: 'title2',
            url: 'https://example.com/2',
            imageUrl: 'https://example.com/2/image',
          },
          productDetails: [
            {title: null, content: {jackpotUrl: ''}},
          ],
        },
      ];
      await flushTasks();

      const buyingOptions =
          tableElement.shadowRoot!.querySelectorAll('buying-options-section');
      assertEquals(0, buyingOptions.length);
    });

    test('`grid-row` populates correctly', async () => {
      // Arrange
      tableElement.columns = [
        {
          selectedItem: {
            title: 'title',
            url: 'https://example.com',
            imageUrl: 'https://example.com/image',
          },
          productDetails: [{
            title: 'foo',
            content: {attributes: [{label: '', value: 'foo1'}], summary: []},
          }],
        },
        {
          selectedItem: {
            title: 'title2',
            url: 'https://example.com/2',
            imageUrl: 'https://example.com/2/image',
          },
          productDetails: [{
            title: 'foo',
            content: {attributes: [{label: '', value: 'foo2'}], summary: []},
          }],
        },
      ];
      await waitAfterNextRender(tableElement);
      const columns = tableElement.shadowRoot!.querySelectorAll('.col');
      assertEquals(2, columns.length);
      assertStyle(columns[0]!, 'grid-row', 'span 3');
      assertStyle(columns[1]!, 'grid-row', 'span 3');
    });
  });

  suite('DragAndDrop', () => {
    test('hides open tab button when dragging', async () => {
      // Arrange
      tableElement.columns = [
        {
          selectedItem: {
            title: 'title',
            url: 'https://example.com',
            imageUrl: 'https://example.com/image',
          },
          productDetails: [
            {
              title: 'foo',
              content: {
                attributes: [{label: '', value: 'd1'}],
                summary: [],
              },
            },
          ],
        },
      ];
      await flushTasks();
      const column = $$<HTMLElement>(tableElement, '.col');
      assertTrue(!!column);
      const openTabButton =
          column!.querySelector<HTMLElement>('.open-tab-button');
      assertTrue(!!openTabButton);
      tableElement.$.table.dispatchEvent(new PointerEvent('pointerleave'));
      assertFalse(isVisible(openTabButton));

      // Act/Assert
      column!.dispatchEvent(new PointerEvent('pointerenter'));
      assertTrue(isVisible(openTabButton));

      tableElement.draggingColumn = column!;
      assertFalse(isVisible(openTabButton));
    });

    test('sets `is-first-column` attribute correctly', async () => {
      // Arrange / Act.
      tableElement.columns = [
        {
          selectedItem:
              {title: 'title', url: 'https://example.com/1', imageUrl: ''},
          productDetails: [
            {
              title: 'foo',
              content: {
                attributes: [{label: '', value: 'd1'}],
                summary: [],
              },
            },
          ],
        },
        {
          selectedItem:
              {title: 'title2', url: 'https://example.com/2', imageUrl: ''},
          productDetails: [
            {title: 'foo', content: null},
          ],
        },
      ];
      await waitAfterNextRender(tableElement);

      // Assert.
      const columns =
          tableElement.shadowRoot!.querySelectorAll<HTMLElement>('.col');
      assertEquals(2, columns.length);
      assertTrue(columns[0]!.hasAttribute('is-first-column'));
      assertFalse(columns[1]!.hasAttribute('is-first-column'));

      tableElement.draggingColumn = columns[0]!;
      // Attribute toggling should be handled by drag and drop manager.
      assertFalse(columns[0]!.hasAttribute('is-first-column'));
      assertFalse(columns[1]!.hasAttribute('is-first-column'));
    });
  });

  test('sets `scroll-snap-align` property correctly', async () => {
    // Arrange / Act.
    tableElement.columns = [
      {
        selectedItem:
            {title: 'title', url: 'https://example.com/1', imageUrl: ''},
        productDetails: [
          {
            title: 'foo',
            content: {
              attributes: [{label: '', value: 'd1'}],
              summary: [],
            },
          },
        ],
      },
      {
        selectedItem:
            {title: 'title2', url: 'https://example.com/2', imageUrl: ''},
        productDetails: [
          {title: 'foo', content: null},
        ],
      },
    ];
    await waitAfterNextRender(tableElement);

    // Assert.
    const columns =
        tableElement.shadowRoot!.querySelectorAll<HTMLElement>('.col');
    assertEquals(2, columns.length);
    assertStyle(columns[0]!, 'scroll-snap-align', 'start');
    assertStyle(columns[1]!, 'scroll-snap-align', 'start');

    tableElement.draggingColumn = columns[0]!;
    assertStyle(columns[0]!, 'scroll-snap-align', 'none');
    assertStyle(columns[1]!, 'scroll-snap-align', 'none');
  });
});