chromium/chrome/test/data/webui/cr_elements/cr_selectable_mixin_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.

// clang-format off
import {CrSelectableMixin} from 'chrome://resources/cr_elements/cr_selectable_mixin.js';
import {html, CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import {getTrustedHtml} from 'chrome://webui-test/trusted_html.js';
import {assertEquals, assertTrue, assertNull} from 'chrome://webui-test/chai_assert.js';
import {eventToPromise} from 'chrome://webui-test/test_util.js';
// clang-format on

suite('cr-scrollable-mixin', function() {
  const TestElementBase = CrSelectableMixin(CrLitElement);

  class TestElement extends TestElementBase {
    static get is() {
      return 'test-element';
    }

    override render() {
      return html`
         <slot></slot>
       `;
    }
  }

  customElements.define(TestElement.is, TestElement);

  let element: TestElement;

  setup(function() {
    document.body.innerHTML = getTrustedHtml(`
      <test-element attr-for-selected="href" selected-attribute="selected"
          selectable="[selectable]">
        <a href="/a" selectable>a</a>
        <a href="/b" selectable>b</a>
        <a href="/c" selectable>c</a>
        <a href="/d">d</a>
      </test-element>`);
    element = document.body.querySelector('test-element')!;
    return element.updateComplete;
  });

  test('updates items', async () => {
    assertEquals(3, element.getItemsForTest().length);

    const newItem = document.createElement('a');
    newItem.setAttribute('href', '/e');
    newItem.textContent = 'e';
    newItem.toggleAttribute('selectable', true);
    let itemsUpdated = eventToPromise('iron-items-changed', element);
    element.appendChild(newItem);
    await itemsUpdated;
    assertEquals(4, element.getItemsForTest().length);

    const newUnselectable = document.createElement('a');
    newUnselectable.setAttribute('href', '/f');
    newUnselectable.textContent = 'f';
    itemsUpdated = eventToPromise('iron-items-changed', element);
    element.appendChild(newUnselectable);
    await itemsUpdated;
    assertEquals(4, element.getItemsForTest().length);

    const c = element.querySelectorAll('a')[2]!;
    itemsUpdated = eventToPromise('iron-items-changed', element);
    element.removeChild(c);
    await itemsUpdated;
    assertEquals(3, element.getItemsForTest().length);
  });

  test('fires events', async () => {
    assertEquals(undefined, element.selected);

    let selectEvent = eventToPromise('iron-select', element);
    let activateEvent = eventToPromise('iron-activate', element);
    const elements = element.querySelectorAll('a');
    elements[0]!.click();
    const events = await Promise.all([activateEvent, selectEvent]);

    assertEquals('/a', events[0]!.detail.selected);
    assertEquals(elements[0], events[0]!.detail.item);
    assertEquals(elements[0], events[1]!.detail.item);
    assertEquals('/a', element.selected);

    selectEvent = eventToPromise('iron-select', element);
    activateEvent = eventToPromise('iron-activate', element);
    const deselectEvent = eventToPromise('iron-deselect', element);
    elements[1]!.click();
    const newEvents =
        await Promise.all([activateEvent, deselectEvent, selectEvent]);

    assertEquals('/b', newEvents[0]!.detail.selected);
    assertEquals(elements[1], newEvents[0]!.detail.item);
    assertEquals(elements[0], newEvents[1]!.detail.item);
    assertEquals(elements[1], newEvents[2]!.detail.item);
    assertEquals('/b', element.selected);
  });

  test('sets attribute and class', async () => {
    function assertSelected(selectedIndex: number) {
      const elements = element.querySelectorAll('a[selectable]');
      for (let index = 0; index < elements.length; index++) {
        assertEquals(
            index === selectedIndex, elements[index]!.hasAttribute('selected'));
        assertEquals(
            index === selectedIndex,
            elements[index]!.classList.contains('selected'));
      }
    }

    element.selected = '/c';
    await eventToPromise('iron-select', element);
    assertSelected(2);

    element.selected = '/a';
    await eventToPromise('iron-select', element);
    assertSelected(0);
  });
});

suite('cr-scrollable-mixin overrides', function() {
  const TestOverridesElementBase = CrSelectableMixin(CrLitElement);

  class TestOverridesElement extends TestOverridesElementBase {
    static get is() {
      return 'test-overrides-element';
    }

    override render() {
      return html`
        <a href="/a">a</a>
        <a href="/b">b</a>
        <a href="/c">c</a>
        <a href="/d">d</a>
        <div>e</div>
      `;
    }

    constructor() {
      super();

      /** Property for CrSelectableMixin */
      this.attrForSelected = 'href';
    }

    // Override `observeItems` from CrSelectableMixin.
    override observeItems() {
      // Turn off default observation logic in CrSelectableMixin.
    }

    // Override `queryItems` from CrSelectableMixin.
    override queryItems() {
      return Array.from(this.shadowRoot!.querySelectorAll('a'));
    }

    // Override `queryMatchingItem` from CrSelectableMixin.
    override queryMatchingItem(selector: string) {
      return this.shadowRoot!.querySelector<HTMLElement>(`a${selector}`);
    }

    override connectedCallback() {
      super.connectedCallback();
      this.itemsChanged();
    }
  }

  customElements.define(TestOverridesElement.is, TestOverridesElement);

  let element: TestOverridesElement;

  setup(function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    element = document.createElement('test-overrides-element') as
        TestOverridesElement;
    document.body.appendChild(element);
  });

  test('All', async () => {
    // Assert that selectable items have been detected.
    assertEquals(4, element.getItemsForTest().length);
    assertNull(element.selectedItem);

    // Select the 2nd item.
    element.selected = '/b';
    await element.updateComplete;
    let selectedItem = element.shadowRoot!.querySelector('.selected');
    assertTrue(!!selectedItem);
    assertEquals(selectedItem, element.selectedItem);
    assertEquals('b', selectedItem.textContent);

    // Remove the selected item, and manually call itemsChanged().
    selectedItem.remove();
    element.itemsChanged();
    assertEquals(3, element.getItemsForTest().length);
    assertNull(element.selectedItem);

    // Select the 1st item.
    element.selected = '/a';
    await element.updateComplete;
    selectedItem = element.shadowRoot!.querySelector('.selected');
    assertTrue(!!selectedItem);
    assertEquals(selectedItem, element.selectedItem);
    assertEquals('a', selectedItem.textContent);

    // Select the next item.
    element.selectNext();
    await element.updateComplete;
    assertEquals('/c', element.selected);
    selectedItem = element.shadowRoot!.querySelector('.selected');
    assertTrue(!!selectedItem);
    assertEquals(selectedItem, element.selectedItem);
    assertEquals('c', selectedItem.textContent);
  });
});