chromium/chrome/test/data/webui/cr_elements/cr_toolbar_search_field_test.ts

// Copyright 2016 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 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js';

import type {CrToolbarSearchFieldElement} from 'chrome://resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js';
import {pressAndReleaseKeyOn} from 'chrome://webui-test/keyboard_mock_interactions.js';
import {assertDeepEquals, assertEquals, assertNotEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {eventToPromise} from 'chrome://webui-test/test_util.js';
// clang-format on

/** @fileoverview Suite of tests for cr-toolbar-search-field. */
suite('cr-toolbar-search-field', function() {
  let field: CrToolbarSearchFieldElement;
  let searches: string[]|null = null;

  function simulateSearch(term: string) {
    field.$.searchInput.value = term;
    field.onSearchTermInput();
    field.onSearchTermSearch();
  }

  setup(function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    field = document.createElement('cr-toolbar-search-field');
    searches = [];
    field.addEventListener('search-changed', function(event) {
      searches!.push((event as CustomEvent<string>).detail);
    });
    document.body.appendChild(field);
  });

  // Test that no initial 'search-changed' event is fired during
  // construction and initialization of the cr-toolbar-search-field element.
  test('no initial search-changed event', function() {
    let didFire = false;
    const onSearchChanged = function() {
      didFire = true;
    };

    // Need to attach listener event before the element is created, to catch
    // the unnecessary initial event.
    document.body.addEventListener('search-changed', onSearchChanged);
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    document.body.appendChild(
        document.createElement('cr-toolbar-search-field'));
    // Remove event listener on |body| so that other tests are not affected.
    document.body.removeEventListener('search-changed', onSearchChanged);

    assertFalse(didFire, 'Should not have fired search-changed event');
  });

  test('opens and closes correctly in narrow mode', async function() {
    const searchInput = field.getSearchInput();
    field.narrow = true;
    await field.updateComplete;
    assertTrue(field.hasAttribute('narrow'));
    assertFalse(field.showingSearch);

    field.click();
    await field.updateComplete;
    assertTrue(field.showingSearch);
    assertEquals(searchInput, field.shadowRoot!.activeElement);

    pressAndReleaseKeyOn(searchInput, 27, [], 'Escape');
    assertFalse(field.showingSearch, 'Pressing escape closes field.');
    assertNotEquals(searchInput, field.shadowRoot!.activeElement);
  });

  test('clear search button clears and refocuses input', async function() {
    field.click();
    simulateSearch('query1');
    await field.updateComplete;
    assertTrue(field.hasSearchText);
    const searchInputClearedEventPromise =
        eventToPromise('search-term-cleared', field);
    const clearSearch =
        field.shadowRoot!.querySelector<HTMLElement>('#clearSearch')!;
    clearSearch.focus();
    clearSearch.click();
    assertTrue(field.showingSearch);
    assertEquals('', field.getValue());
    assertEquals(field.$.searchInput, field.shadowRoot!.activeElement);
    assertFalse(field.hasSearchText);
    assertFalse(field.spinnerActive);
    await searchInputClearedEventPromise;
  });

  test('notifies on new searches', async function() {
    field.click();
    simulateSearch('query1');
    await field.updateComplete;
    assertEquals('query1', field.getValue());

    field.shadowRoot!.querySelector<HTMLElement>('#clearSearch')!.click();
    assertTrue(field.showingSearch);
    assertEquals('', field.getValue());

    simulateSearch('query2');
    // Expecting identical query to be ignored.
    simulateSearch('query2');

    assertDeepEquals(['query1', '', 'query2'], searches);
  });

  test('notifies on setValue', function() {
    field.click();
    field.setValue('foo');
    field.setValue('');
    field.setValue('bar');
    // Expecting identical query to be ignored.
    field.setValue('bar');
    field.setValue('baz');
    assertDeepEquals(['foo', '', 'bar', 'baz'], searches);
  });

  test('does not notify on setValue with noEvent=true', function() {
    field.click();
    field.setValue('foo', true);
    field.setValue('bar');
    field.setValue('baz', true);
    assertDeepEquals(['bar'], searches);
  });

  test('treat consecutive whitespace as single space', function() {
    field.click();
    const query = 'foo        bar     baz';
    simulateSearch(query);
    assertEquals(query, field.getValue());

    // Expecting effectively the same query to be ignored.
    const effectivelySameQuery = 'foo   bar    baz';
    simulateSearch(effectivelySameQuery);
    assertEquals(effectivelySameQuery, field.getValue());

    assertDeepEquals(['foo bar baz'], searches);
  });

  test('ignore leading whitespace', () => {
    field.click();
    const query = ' foo';
    simulateSearch(query);
    assertEquals(query, field.getValue());

    // Expecting effectively the same query to be ignored.
    const effectivelySameQuery = '     foo';
    simulateSearch(effectivelySameQuery);
    assertEquals(effectivelySameQuery, field.getValue());

    assertDeepEquals(['foo'], searches);
  });

  test('when there is trailing whitespace, replace with one space', () => {
    field.click();
    const query = 'foo  ';
    simulateSearch(query);
    assertEquals(query, field.getValue());

    // Expecting effectively the same query to be ignored.
    const effectivelySameQuery = 'foo        ';
    simulateSearch(effectivelySameQuery);
    assertEquals(effectivelySameQuery, field.getValue());

    assertDeepEquals(['foo '], searches);
  });

  // Tests that calling setValue() from within a 'search-changed' callback
  // does not result in an infinite loop.
  test('no infinite loop', function() {
    let counter = 0;
    field.addEventListener('search-changed', function(event) {
      counter++;
      // Calling setValue() with the already existing value should not
      // trigger another 'search-changed' event.
      field.setValue((event as CustomEvent<string>).detail);
    });

    field.click();
    field.setValue('bar');
    assertEquals(1, counter);
    assertDeepEquals(['bar'], searches);
  });

  test('blur does not close field when a search is active', function() {
    field.click();
    simulateSearch('test');
    field.$.searchInput.blur();

    assertTrue(field.showingSearch);
  });

  test('opens when value is changed', async function() {
    // Change search value without explicitly opening the field first.
    // Similar to what happens when pasting or dragging into the input
    // field.
    assertFalse(field.hasSearchText);
    simulateSearch('test');
    await field.updateComplete;
    assertTrue(field.hasSearchText);

    const clearSearch =
        field.shadowRoot!.querySelector<HTMLElement>('#clearSearch')!;
    assertFalse(clearSearch.hidden);
    assertTrue(field.showingSearch);
  });

  test('closes when value is cleared while unfocused', function() {
    field.$.searchInput.focus();
    simulateSearch('test');

    // Does not close the field if it is focused when cleared.
    assertTrue(field.showingSearch);
    field.setValue('');
    assertTrue(field.showingSearch);

    // Does close the field if it is blurred before being cleared.
    simulateSearch('test');
    field.$.searchInput.blur();
    field.setValue('');
    assertFalse(field.showingSearch);
  });

  test('autofocus propagated to search input', () => {
    assertFalse(field.autofocus);
    assertFalse(field.getSearchInput().hasAttribute('autofocus'));

    field.remove();
    field = /** @type {!CrToolbarSearchFieldElement} */ (
        document.createElement('cr-toolbar-search-field'));
    field.autofocus = true;

    document.body.appendChild(field);
    assertTrue(field.getSearchInput().hasAttribute('autofocus'));
  });

  test('overrides search icon', async () => {
    assertEquals('cr:search', field.$.icon.ironIcon);
    field.iconOverride = 'cr:more-vert';
    await field.updateComplete;
    assertEquals('cr:more-vert', field.$.icon.ironIcon);
    field.iconOverride = undefined;
    await field.updateComplete;
    assertEquals('cr:search', field.$.icon.ironIcon);
  });

  test('sets aria-description on input', async () => {
    assertEquals('', field.$.searchInput.ariaDescription);
    field.inputAriaDescription = 'hello world';
    await field.updateComplete;
    assertEquals('hello world', field.$.searchInput.ariaDescription);
  });

  test('fires a custom event for native input event', async () => {
    // A series of events that mocks a user typing into the input.
    simulateSearch('a');
    const inputEvent = new InputEvent('input', {data: 'a'});
    const searchTermInputEventPromise =
        eventToPromise('search-term-native-input', field);
    field.$.searchInput.dispatchEvent(inputEvent);
    const searchTermInputEvent = await searchTermInputEventPromise;
    assertEquals(inputEvent, searchTermInputEvent.detail.e);
    assertEquals('a', searchTermInputEvent.detail.inputValue);
  });

  test('fires a custom event for native beforeinput event', async () => {
    const beforeInputEvent = new InputEvent('beforeinput', {data: 'a'});
    const searchTermBeforeInputEventPromise =
        eventToPromise('search-term-native-before-input', field);
    field.$.searchInput.dispatchEvent(beforeInputEvent);
    const searchTermBeforeInputEvent = await searchTermBeforeInputEventPromise;
    assertEquals(beforeInputEvent, searchTermBeforeInputEvent.detail.e);
  });
});