chromium/chrome/test/data/webui/print_preview/searchable_drop_down_cros_test.ts

// Copyright 2018 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://print/print_preview.js';

import {getTrustedHTML} from 'chrome://print/print_preview.js';
import type {CrInputElement, SearchableDropDownCrosElement} from 'chrome://print/print_preview.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {keyDownOn} from 'chrome://webui-test/keyboard_mock_interactions.js';
import {move} from 'chrome://webui-test/mouse_mock_interactions.js';

suite('cr-searchable-drop-down', function() {
  let dropDown: SearchableDropDownCrosElement;
  let outsideElement: HTMLElement;
  let searchInput: CrInputElement;

  /**
   * @param items The list of items to be populated in the drop down.
   */
  function setItems(items: string[]) {
    dropDown.items = items;
    flush();
  }

  function getList(): NodeListOf<HTMLElement> {
    return dropDown.shadowRoot!.querySelectorAll<HTMLElement>('.list-item');
  }

  /**
   * @param searchTerm The string used to filter the list of items in the drop
   *     down.
   */
  async function search(searchTerm: string): Promise<void> {
    const input = /** @type {!CrInputElement} */ (
        dropDown.shadowRoot!.querySelector('cr-input')!);
    input.value = searchTerm;
    await input.updateComplete;
    input.dispatchEvent(
        new CustomEvent('input', {bubbles: true, composed: true}));
    flush();
  }

  async function blur(): Promise<void> {
    const input = /** @type {!CrInputElement} */ (
        dropDown.shadowRoot!.querySelector('cr-input')!);
    input.dispatchEvent(
        new CustomEvent('blur', {bubbles: true, composed: true}));
    await input.updateComplete;
    flush();
  }

  function down() {
    keyDownOn(searchInput, 0, [], 'ArrowDown');
  }

  function up() {
    keyDownOn(searchInput, 0, [], 'ArrowUp');
  }

  function enter() {
    keyDownOn(searchInput, 0, [], 'Enter');
  }

  function tab() {
    keyDownOn(searchInput, 0, [], 'Tab');
  }

  function pointerDown(element: HTMLElement) {
    element.dispatchEvent(new PointerEvent('pointerdown', {
      bubbles: true,
      cancelable: true,
      composed: true,
      buttons: 1,
    }));
  }

  function getSelectedElement() {
    return dropDown.shadowRoot!.querySelector('[selected_]');
  }

  setup(function() {
    document.body.innerHTML = getTrustedHTML`
      <p id="outside">Nothing to see here</p>
      <searchable-drop-down-cros label="test drop down">
      </searchable-drop-down-cros>
    `;
    dropDown = document.querySelector('searchable-drop-down-cros')!;
    outsideElement = document.querySelector<HTMLElement>('#outside')!;
    searchInput = dropDown.$.search;
    flush();
  });

  test('correct list items', function() {
    setItems(['one', 'two', 'three']);

    const itemList = getList();

    assertEquals(3, itemList.length);
    assertEquals('one', itemList[0]!.textContent!.trim());
    assertEquals('two', itemList[1]!.textContent!.trim());
    assertEquals('three', itemList[2]!.textContent!.trim());
  });

  test('filter works correctly', async () => {
    setItems(['cat', 'hat', 'rat', 'rake']);

    await search('c');
    assertEquals(1, getList().length);
    assertEquals('cat', getList()[0]!.textContent!.trim());
    assertTrue(dropDown.invalid);

    await search('at');
    assertEquals(3, getList().length);
    assertEquals('cat', getList()[0]!.textContent!.trim());
    assertEquals('hat', getList()[1]!.textContent!.trim());
    assertEquals('rat', getList()[2]!.textContent!.trim());
    assertTrue(dropDown.invalid);

    await search('ra');
    assertEquals(2, getList().length);
    assertEquals('rat', getList()[0]!.textContent!.trim());
    assertEquals('rake', getList()[1]!.textContent!.trim());
    assertTrue(dropDown.invalid);
  });

  test('value is set on click', async () => {
    setItems(['dog', 'cat', 'mouse']);

    assertNotEquals('dog', dropDown.value);

    getList()[0]!.click();

    assertEquals('dog', dropDown.value);

    // Make sure final value does not change while searching.
    await search('ta');
    assertEquals('dog', dropDown.value);
    assertTrue(dropDown.invalid);
  });

  // If the update-value-on-input flag is passed, final value should be whatever
  // is in the search box.
  test('value is set on click and on search', async () => {
    dropDown.updateValueOnInput = true;
    setItems(['dog', 'cat', 'mouse']);

    assertNotEquals('dog', dropDown.value);

    getList()[0]!.click();

    assertEquals('dog', dropDown.value);

    // Make sure final value does change while searching.
    await search('ta');
    assertEquals('ta', dropDown.value);
    assertFalse(dropDown.invalid);
  });

  test('click closes dropdown', function() {
    setItems(['dog', 'cat', 'mouse']);

    // Dropdown opening is tied to focus.
    dropDown.$.search.focus();
    assertTrue(dropDown.$.dropdown.opened);

    assertNotEquals('dog', dropDown.value);

    getList()[0]!.click();
    assertEquals('dog', dropDown.value);
    assertFalse(dropDown.$.dropdown.opened);
  });

  test('click outside closes dropdown', function() {
    setItems(['dog', 'cat', 'mouse']);

    // Dropdown opening is tied to focus.
    dropDown.$.search.focus();
    assertTrue(dropDown.$.dropdown.opened);
    assertNotEquals('dog', dropDown.value);

    pointerDown(outsideElement);
    assertNotEquals('dog', dropDown.value);
    assertFalse(dropDown.$.dropdown.opened);
  });

  test('tab closes dropdown', function() {
    setItems(['dog', 'cat', 'mouse']);

    // Dropdown opening is tied to focus.
    dropDown.$.search.focus();
    assertTrue(dropDown.$.dropdown.opened);

    tab();
    assertFalse(dropDown.$.dropdown.opened);
  });

  test('selected moves after up/down', function() {
    setItems(['dog', 'cat', 'mouse']);

    dropDown.$.search.focus();
    assertTrue(dropDown.$.dropdown.opened);

    assertEquals(null, getSelectedElement());

    down();
    assertEquals('dog', getSelectedElement()!.textContent!.trim());
    down();
    assertEquals('cat', getSelectedElement()!.textContent!.trim());
    down();
    assertEquals('mouse', getSelectedElement()!.textContent!.trim());
    down();
    assertEquals('dog', getSelectedElement()!.textContent!.trim());

    up();
    assertEquals('mouse', getSelectedElement()!.textContent!.trim());
    up();
    assertEquals('cat', getSelectedElement()!.textContent!.trim());
    up();
    assertEquals('dog', getSelectedElement()!.textContent!.trim());
    up();
    assertEquals('mouse', getSelectedElement()!.textContent!.trim());

    enter();
    assertEquals('mouse', dropDown.value);
    assertFalse(dropDown.$.dropdown.opened);
  });

  test('enter re-opens dropdown after selection', function() {
    setItems(['dog', 'cat', 'mouse']);

    dropDown.$.search.focus();
    assertTrue(dropDown.$.dropdown.opened);

    assertEquals(null, getSelectedElement());

    down();
    assertEquals('dog', getSelectedElement()!.textContent!.trim());

    enter();
    assertEquals('dog', dropDown.value);
    assertFalse(dropDown.$.dropdown.opened);

    enter();
    assertTrue(dropDown.$.dropdown.opened);
    assertEquals(null, getSelectedElement());
  });

  test('focus and up selects last item', function() {
    setItems(['dog', 'cat', 'mouse']);

    dropDown.$.search.focus();
    assertTrue(dropDown.$.dropdown.opened);

    assertEquals(null, getSelectedElement());

    up();
    assertEquals('mouse', getSelectedElement()!.textContent!.trim());
  });

  test('selected follows mouse', function() {
    setItems(['dog', 'cat', 'mouse']);

    dropDown.$.search.focus();
    assertTrue(dropDown.$.dropdown.opened);

    assertEquals(null, getSelectedElement());

    move(getList()[1]!, {x: 0, y: 0}, {x: 0, y: 0}, 1);
    assertEquals('cat', getSelectedElement()!.textContent!.trim());
    move(getList()[2]!, {x: 0, y: 0}, {x: 0, y: 0}, 1);
    assertEquals('mouse', getSelectedElement()!.textContent!.trim());

    // Interacting with the keyboard should update the selected element.
    up();
    assertEquals('cat', getSelectedElement()!.textContent!.trim());

    // When the user moves the mouse again, the selected element should change.
    move(getList()[0]!, {x: 0, y: 0}, {x: 0, y: 0}, 1);
    assertEquals('dog', getSelectedElement()!.textContent!.trim());
  });

  test('input retains focus', function() {
    setItems(['dog', 'cat', 'mouse']);

    searchInput.focus();
    assertTrue(dropDown.$.dropdown.opened);
    assertEquals(searchInput, dropDown.shadowRoot!.activeElement);

    assertEquals(null, getSelectedElement());

    down();
    assertEquals('dog', getSelectedElement()!.textContent!.trim());
    assertEquals(searchInput, dropDown.shadowRoot!.activeElement);
  });

  // If the error-message-allowed flag is passed and the |errorMessage| property
  // is set, then the error message should be displayed. If the |errorMessage|
  // property is not set or |errorMessageAllowed| is false, no error message
  // should be displayed.
  test('error message is displayed if set and allowed', async () => {
    dropDown.errorMessageAllowed = true;
    dropDown.errorMessage = 'error message';

    const input = dropDown.$.search;
    await input.updateComplete;

    assertEquals(dropDown.errorMessage, input.$.error.textContent);
    assertTrue(input.invalid);

    // Set |errorMessageAllowed| to false and verify no error message is shown.
    dropDown.errorMessageAllowed = false;
    await input.updateComplete;

    assertNotEquals(dropDown.errorMessage, input.$.error.textContent);
    assertFalse(input.invalid);

    // Set |errorMessageAllowed| to true and verify it is displayed again.
    dropDown.errorMessageAllowed = true;
    await input.updateComplete;

    assertEquals(dropDown.errorMessage, input.$.error.textContent);
    assertTrue(input.invalid);

    // Clearing |errorMessage| hides the error.
    dropDown.errorMessage = '';
    await input.updateComplete;

    assertEquals(dropDown.errorMessage, input.$.error.textContent);
    assertFalse(input.invalid);
  });

  // The show-loading attribute should determine whether or not the loading
  // spinner and message are shown.
  test('loading spinner is shown and hidden', function() {
    const progress =
        dropDown.shadowRoot!.querySelector<HTMLElement>('#loading-box')!;
    assertTrue(progress.hidden);

    dropDown.showLoading = true;
    assertFalse(progress.hidden);

    dropDown.showLoading = false;
    assertTrue(progress.hidden);
  });


  // The readonly attribute is passed through to the inner cr-input.
  test('readonly attribute', function() {
    const input = dropDown.shadowRoot!.querySelector('cr-input')!;

    dropDown.readonly = true;
    assertTrue(!!input.readonly);

    dropDown.readonly = false;
    assertFalse(!!input.readonly);
  });

  // When a user types in the dropdown but does not choose a valid option, the
  // dropdown should revert to the previously selected option on loss of focus.
  test('value resets on loss of focus', async () => {
    setItems(['dog', 'cat', 'mouse']);

    getList()[0]!.click();
    assertEquals('dog', searchInput.value);
    assertFalse(dropDown.invalid);

    // Make sure the search box value changes back to dog
    await search('ta');
    assertTrue(dropDown.invalid);
    await blur();
    assertEquals('dog', searchInput.value);
    assertFalse(dropDown.invalid);
  });

  // When a user types in the dropdown but does not choose a valid option, the
  // dropdown should keep the same text on loss of focus. (Only when
  // isupdateValueOnInput is set to true).
  test('value remains on loss of focus', async () => {
    dropDown.updateValueOnInput = true;
    setItems(['dog', 'cat', 'mouse']);

    getList()[0]!.click();
    assertEquals('dog', searchInput.value);
    assertFalse(dropDown.invalid);

    // Make sure the search box value keeps the same text
    await search('ta');
    assertFalse(dropDown.invalid);
    await blur();
    assertEquals('ta', searchInput.value);
    assertFalse(dropDown.invalid);
  });

  // In certain cases when a user clicks their desired option from the dropdown,
  // the on-blur event is fired before the on-click event. This test is to
  // guarantee expected behavior given a proceeding blur event.
  test('blur event when option is clicked', async () => {
    setItems(['cat', 'hat', 'rat', 'rake']);

    await search('rat');
    assertEquals(1, getList().length);
    assertEquals('rat', getList()[0]!.textContent!.trim());

    await blur();
    getList()[0]!.click();

    assertEquals('rat', dropDown.value);
  });
});