chromium/chrome/test/data/webui/chromeos/print_preview_cros/destination_dropdown_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://os-print/js/destination_dropdown.js';

import {PDF_DESTINATION} from 'chrome://os-print/js/data/destination_constants.js';
import {DESTINATION_MANAGER_ACTIVE_DESTINATION_CHANGED, DESTINATION_MANAGER_DESTINATIONS_CHANGED, DESTINATION_MANAGER_SESSION_INITIALIZED, DestinationManager} from 'chrome://os-print/js/data/destination_manager.js';
import {DestinationProviderComposite} from 'chrome://os-print/js/data/destination_provider_composite.js';
import {DestinationDropdownElement} from 'chrome://os-print/js/destination_dropdown.js';
import {DESTINATION_DROPDOWN_DROPDOWN_DISABLED_CHANGED, DESTINATION_DROPDOWN_UPDATE_DESTINATIONS, DESTINATION_DROPDOWN_UPDATE_SELECTED_DESTINATION, DestinationDropdownController} from 'chrome://os-print/js/destination_dropdown_controller.js';
import {DestinationRowElement} from 'chrome://os-print/js/destination_row.js';
import {FAKE_PRINT_SESSION_CONTEXT_SUCCESSFUL} from 'chrome://os-print/js/fakes/fake_print_preview_page_handler.js';
import {createCustomEvent} from 'chrome://os-print/js/utils/event_utils.js';
import {getDestinationProvider} from 'chrome://os-print/js/utils/mojo_data_providers.js';
import type {Destination} from 'chrome://os-print/js/utils/print_preview_cros_app_types.js';
import {strictQuery} from 'chrome://resources/ash/common/typescript_utils/strict_query.js';
import {assert} from 'chrome://resources/js/assert.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chromeos/chai_assert.js';
import {MockController} from 'chrome://webui-test/chromeos/mock_controller.m.js';
import {eventToPromise, isVisible} from 'chrome://webui-test/test_util.js';

import {createTestDestination, resetDataManagersAndProviders, waitForInitialDestinationSet, waitForPrintTicketManagerInitialized} from './test_utils.js';

suite('DestinationDropdown', () => {
  let element: DestinationDropdownElement;
  let controller: DestinationDropdownController;
  let destinationManager: DestinationManager;
  let mockController: MockController;

  const contentSelector = '#content';
  const selectedDestinationSelector = '#selected';
  const initialDestinations = [PDF_DESTINATION];

  setup(() => {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;

    mockController = new MockController();
    resetDataManagersAndProviders();
    destinationManager = DestinationManager.getInstance();

    // Set PDF as default active destination for testing UI.
    const getActiveDestinationFn = mockController.createFunctionMock(
        destinationManager, 'getActiveDestination');
    getActiveDestinationFn.returnValue = PDF_DESTINATION;

    element = document.createElement(DestinationDropdownElement.is) as
        DestinationDropdownElement;
    assertTrue(!!element);
    document.body.append(element);

    controller = element.getControllerForTesting();
  });

  teardown(() => {
    element.remove();
    resetDataManagersAndProviders();
    mockController.reset();
  });

  function assertExpectedDestinationsDisplayedInContentRows(
      expectedDestinations: Destination[]): void {
    const contentRows =
        getDropdownContent().querySelectorAll(DestinationRowElement.is);
    assertEquals(expectedDestinations.length, contentRows.length);
    contentRows.forEach((element: DestinationRowElement, key: number) => {
      assertDeepEquals(
          expectedDestinations[key], element.destination,
          `Expected DestinationRowElement[${key}] to be ${
              JSON.stringify(expectedDestinations[key])}
              found
              ${JSON.stringify(element.destination)}`);
    });
  }

  function getDropdownContent(): HTMLDivElement {
    return strictQuery<HTMLDivElement>(
        contentSelector, element.shadowRoot, HTMLDivElement);
  }

  function getSelectedDestinationRow(): DestinationRowElement {
    return strictQuery<DestinationRowElement>(
        selectedDestinationSelector, element.shadowRoot, DestinationRowElement);
  }

  function getSelectedDestinationRowLabel(): string {
    const selected = getSelectedDestinationRow();
    const rowLabel =
        strictQuery<HTMLElement>('#label', selected.shadowRoot, HTMLElement);
    return rowLabel.textContent!.trim();
  }

  async function clickDropdownRowFor(destinationId: string): Promise<void> {
    assert(element.shadowRoot);
    const content = strictQuery<HTMLDivElement>(
        '#content', element.shadowRoot, HTMLDivElement);
    assertTrue(isVisible(content), 'Dropdown needs to be open');
    const destinationRow = strictQuery<DestinationRowElement>(
        `#row-${destinationId}`, content, DestinationRowElement);
    assert(destinationRow, 'Only attempt to click existing destination');
    const clickEvent = eventToPromise('click', destinationRow);
    destinationRow.click();
    await clickEvent;
  }

  async function toggleDropdown(): Promise<void> {
    const selected = getSelectedDestinationRow();
    const clickEvent = eventToPromise('click', selected);
    selected.click();
    await clickEvent;
    flush();
  }

  // Verify the dropdown can be added to UI.
  test('element renders', () => {
    assertTrue(
        isVisible(element), `Should display ${DestinationDropdownElement.is}`);
  });

  // Verify dropdown element has a controller configured.
  test('has element controller', async () => {
    assertTrue(
        !!controller,
        `${DestinationDropdownElement.is} should have controller configured`);
  });

  // Verify destination used in UI updates on
  // DESTINATION_DROPDOWN_UPDATE_SELECTED_DESTINATION.
  test(
      `${DESTINATION_DROPDOWN_UPDATE_SELECTED_DESTINATION} updates
    destination display name`,
      async () => {
        const getActiveDestinationFn = mockController.createFunctionMock(
            destinationManager, 'getActiveDestination');
        getActiveDestinationFn.returnValue = null;

        const selectedChangedEvent1 = eventToPromise(
            DESTINATION_DROPDOWN_UPDATE_SELECTED_DESTINATION, controller);
        controller.dispatchEvent(createCustomEvent(
            DESTINATION_DROPDOWN_UPDATE_SELECTED_DESTINATION));
        await selectedChangedEvent1;

        assertEquals(
            '', getSelectedDestinationRowLabel(),
            `Selected destination not display a destination name`);

        // Change result to non-null destination.
        getActiveDestinationFn.returnValue = PDF_DESTINATION;
        const selectedChangedEvent2 = eventToPromise(
            DESTINATION_DROPDOWN_UPDATE_SELECTED_DESTINATION, controller);
        controller.dispatchEvent(createCustomEvent(
            DESTINATION_DROPDOWN_UPDATE_SELECTED_DESTINATION));
        await selectedChangedEvent2;

        assertEquals(
            PDF_DESTINATION.displayName, getSelectedDestinationRowLabel(),
            `Selected destination should match active destination`);
      });

  // Verify clicking dropdown's selected UI toggles content visibility.
  test('clicking dropdown toggles content visibility', async () => {
    await waitForInitialDestinationSet();
    await waitForPrintTicketManagerInitialized();

    assertFalse(element.disabled);
    assertTrue(isVisible(getSelectedDestinationRow()));
    const content = getDropdownContent();
    assertFalse(isVisible(content), 'Content is not initially displayed');

    // Open dropdown.
    await toggleDropdown();
    assertTrue(isVisible(content), 'Content displayed after click');

    // Close dropdown.
    await toggleDropdown();
    assertFalse(isVisible(content), 'Content closed after click');
  });

  // Verify DESTINATION_DROPDOWN_UPDATE_DESTINATIONS triggers handler in UI.
  test(
      `element handles ${DESTINATION_DROPDOWN_UPDATE_DESTINATIONS} event`,
      async () => {
        const updateHandlerFn = mockController.createFunctionMock(
            element, 'onDestinationDropdownUpdateDestinations');
        const updateContentEvent = eventToPromise(
            DESTINATION_DROPDOWN_UPDATE_DESTINATIONS, controller);
        updateHandlerFn.addExpectation();
        controller.dispatchEvent(
            createCustomEvent(DESTINATION_DROPDOWN_UPDATE_DESTINATIONS));
        await updateContentEvent;

        updateHandlerFn.verifyMock();
      });

  // Verify after initializing active destination is selected and dropdown
  // contains destinations added by getLocalDestinations.
  test(
      'dropdown matches destination manager active and destination list',
      async () => {
        // Clear mock from setup and wait until destination manager initialized.
        // Only destination in list should be PDF destination.
        mockController.reset();
        const initializeDestMgrEvent = eventToPromise(
            DESTINATION_MANAGER_SESSION_INITIALIZED, destinationManager);
        const activeDestEvent = eventToPromise(
            DESTINATION_MANAGER_ACTIVE_DESTINATION_CHANGED, destinationManager);
        destinationManager.initializeSession(
            FAKE_PRINT_SESSION_CONTEXT_SUCCESSFUL);
        await initializeDestMgrEvent;
        await activeDestEvent;
        await toggleDropdown();

        const activeDestination = destinationManager.getActiveDestination();
        assert(activeDestination, 'Active destination should be set');
        assertEquals(
            activeDestination.displayName, getSelectedDestinationRowLabel());
        assertExpectedDestinationsDisplayedInContentRows(initialDestinations);
      });

  // Verify dropdown content updates when DestinationManager observes
  // `onDestinationsChanged` and emits a destinations changed event.
  test(
      'dropdown content updates when observer triggers destinations changed',
      async () => {
        // Clear mock from setup and wait until destination manager initialized.
        // Only destination in list should be PDF destination.
        mockController.reset();
        const initializeDestMgrEvent = eventToPromise(
            DESTINATION_MANAGER_SESSION_INITIALIZED, destinationManager);
        destinationManager.initializeSession(
            FAKE_PRINT_SESSION_CONTEXT_SUCCESSFUL);
        await initializeDestMgrEvent;
        await toggleDropdown();

        assertExpectedDestinationsDisplayedInContentRows(initialDestinations);

        // Simulate update from destination observer appends a new destination
        // to the list of destinations in dropdown.
        const destinationProvider =
            (getDestinationProvider() as DestinationProviderComposite)
                .fakeDestinationProvider;
        const addedDestination = createTestDestination();
        destinationProvider.setDestinationsChangesData([addedDestination]);
        const updateContentEvent = eventToPromise(
            DESTINATION_DROPDOWN_UPDATE_DESTINATIONS, controller);
        destinationProvider.triggerOnDestinationsChanged();
        await updateContentEvent;

        assertExpectedDestinationsDisplayedInContentRows([
          PDF_DESTINATION,
          addedDestination,
        ]);
      });

  // Verify clicking a DestinationRowElement in contents calls controller's
  // updateActiveDestination handler with the destination id of the clicked
  // DestinationRowElement, updates selectedDestination, and closes content.
  test(
      'DestinationRowElement immediately closes the dropdown and passes ID ' +
          'of the clicked row to updateActiveDestination',
      async () => {
        mockController.reset();
        const selectedChanged = eventToPromise(
            DESTINATION_DROPDOWN_UPDATE_SELECTED_DESTINATION, controller);
        await waitForInitialDestinationSet();
        await waitForPrintTicketManagerInitialized();
        await selectedChanged;
        const updateFn = mockController.createFunctionMock(
            controller, 'updateActiveDestination');
        updateFn.returnValue = true;
        const testDestination = createTestDestination();
        const destChangedEvent = eventToPromise(
            DESTINATION_DROPDOWN_UPDATE_DESTINATIONS, controller);
        destinationManager.setDestinationForTesting(testDestination);
        destinationManager.dispatchEvent(
            createCustomEvent(DESTINATION_MANAGER_DESTINATIONS_CHANGED));
        await destChangedEvent;

        // Simulate clicking a destination row.
        await toggleDropdown();
        updateFn.addExpectation(testDestination.id);
        await clickDropdownRowFor(testDestination.id);

        assertFalse(isVisible(getDropdownContent()), 'Dropdown closed');
        assertEquals(
            testDestination.displayName, getSelectedDestinationRowLabel());
        updateFn.verifyMock();
      });

  // Verify clicking a DestinationRowElement closes content and does not
  // update selectedDestination if provided ID is active destination.
  test(
      'selectedDestination not updated if updateActiveDestination returns ' +
          'false',
      async () => {
        mockController.reset();
        await waitForInitialDestinationSet();
        await waitForPrintTicketManagerInitialized();
        assertEquals(
            PDF_DESTINATION.displayName, getSelectedDestinationRowLabel());

        // Simulate clicking a destination row.
        await toggleDropdown();
        await clickDropdownRowFor(PDF_DESTINATION.id);

        assertFalse(isVisible(getDropdownContent()), 'Dropdown closed');
        assertEquals(
            PDF_DESTINATION.displayName, getSelectedDestinationRowLabel());
      });

  // Verify click ignored if disabled is true.
  test('cannot toggle open content when disabled', async () => {
    await waitForInitialDestinationSet();
    await waitForPrintTicketManagerInitialized();
    assertFalse(element.disabled);

    // Force disabled to true and emit change event to update UI.
    const updateFn =
        mockController.createFunctionMock(controller, 'shouldDisableDropdown');
    updateFn.returnValue = true;
    controller.dispatchEvent(
        createCustomEvent(DESTINATION_DROPDOWN_DROPDOWN_DISABLED_CHANGED));
    await toggleDropdown();

    assertTrue(element.disabled);
    assertFalse(isVisible(getDropdownContent()), 'Dropdown remains closed');
  });

  // Verify dropdown closed if open when disabled event returns true.
  test('dropdown closed when disabled', async () => {
    await waitForInitialDestinationSet();
    await waitForPrintTicketManagerInitialized();
    // Open dropdown.
    assertFalse(element.disabled);
    await toggleDropdown();
    assertTrue(isVisible(getDropdownContent()), 'Dropdown open');

    // Force disabled to true and emit change event to update UI.
    const updateFn =
        mockController.createFunctionMock(controller, 'shouldDisableDropdown');
    updateFn.returnValue = true;
    controller.dispatchEvent(
        createCustomEvent(DESTINATION_DROPDOWN_DROPDOWN_DISABLED_CHANGED));

    assertTrue(element.disabled);
    assertFalse(isVisible(getDropdownContent()), 'Dropdown closed');
  });
});