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

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import type {Destination, DestinationStore, LocalDestinationInfo, PrintPreviewDestinationDialogCrosElement, RecentDestination} from 'chrome://print/print_preview.js';
import {DESTINATION_DIALOG_CROS_LOADING_TIMER_IN_MS, makeRecentDestination, NativeLayerImpl, PrinterSetupInfoMessageType, PrintPreviewPrinterSetupInfoCrosElement} from 'chrome://print/print_preview.js';
import {webUIListenerCallback} from 'chrome://resources/js/cr.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {keyEventOn} from 'chrome://webui-test/keyboard_mock_interactions.js';
import {MockTimer} from 'chrome://webui-test/mock_timer.js';
import {eventToPromise, isChildVisible, isVisible} from 'chrome://webui-test/test_util.js';

import type {NativeLayerCrosStub} from './native_layer_cros_stub.js';
import {setNativeLayerCrosInstance} from './native_layer_cros_stub.js';
import {NativeLayerStub} from './native_layer_stub.js';
import {createDestinationStore, getDestinations, setupTestListenerElement} from './print_preview_test_utils.js';

suite('DestinationDialogCrosTest', function() {
  let dialog: PrintPreviewDestinationDialogCrosElement;

  let destinationStore: DestinationStore;

  let nativeLayer: NativeLayerStub;

  let nativeLayerCros: NativeLayerCrosStub;

  let destinations: Destination[] = [];

  const localDestinations: LocalDestinationInfo[] = [];

  let recentDestinations: RecentDestination[] = [];

  let mockTimer: MockTimer;

  suiteSetup(function() {
    setupTestListenerElement();
  });

  setup(function() {
    // Create data classes
    nativeLayer = new NativeLayerStub();
    NativeLayerImpl.setInstance(nativeLayer);
    nativeLayerCros = setNativeLayerCrosInstance();
    destinationStore = createDestinationStore();
    destinations = getDestinations(localDestinations);
    recentDestinations = [makeRecentDestination(destinations[4]!)];
    nativeLayer.setLocalDestinations(localDestinations);
    destinationStore.init(
        false /* pdfPrinterDisabled */, false /* saveToDriveDisabled */,
        'FooDevice' /* printerName */,
        '' /* serializedDefaultDestinationSelectionRulesStr */,
        recentDestinations /* recentDestinations */);

    // Setup fake timer.
    mockTimer = new MockTimer();
    mockTimer.install();

    // Set up dialog
    dialog = document.createElement('print-preview-destination-dialog-cros');
    dialog.destinationStore = destinationStore;
  });

  teardown(function() {
    mockTimer.uninstall();
  });

  function finishSetup() {
    document.body.appendChild(dialog);
    return nativeLayer.whenCalled('getPrinterCapabilities')
        .then(function() {
          destinationStore.startLoadAllDestinations();
          dialog.show();
          mockTimer.tick(DESTINATION_DIALOG_CROS_LOADING_TIMER_IN_MS);
          return nativeLayer.whenCalled('getPrinters');
        })
        .then(function() {
          return nativeLayerCros.whenCalled('getShowManagePrinters');
        })
        .then(function() {
          flush();
        });
  }

  /**
   * Remove and recreate destination-dialog-cros then return `finishSetup`. If
   * `removeDestinations` is set, also overrides destination-store to be empty.
   */
  function recreateElementAndFinishSetup(removeDestinations: boolean):
      Promise<void> {
    // Remove existing dialog.
    dialog.remove();
    flush();

    if (removeDestinations) {
      // Re-create data classes with no destinations.
      destinationStore = createDestinationStore();
      nativeLayer.setLocalDestinations([]);
      destinationStore.init(
          false /* pdfPrinterDisabled */, false /* saveToDriveDisabled */,
          'FooDevice' /* printerName */,
          '' /* serializedDefaultDestinationSelectionRulesStr */,
          [] /* recentDestinations */);
    }

    // Set up dialog.
    dialog = document.createElement('print-preview-destination-dialog-cros');
    dialog.destinationStore = destinationStore;
    return finishSetup();
  }

  /**
   * Checks that `recordInHistogram` is called with expected bucket.
   */
  function verifyRecordInHistogramCall(
      callIndex: number, expectedBucket: number): void {
    const calls = nativeLayer.getArgs('recordInHistogram');
    assertTrue(!!calls && calls.length > 0);
    assertTrue(callIndex < calls.length);
    const [histogramName, bucket] = calls[callIndex];
    assertEquals('PrintPreview.PrinterSettingsLaunchSource', histogramName);
    assertEquals(expectedBucket, bucket);
  }

  // Test that clicking a provisional destination shows the provisional
  // destinations dialog, and that the escape key closes only the provisional
  // dialog when it is open, not the destinations dialog.
  test(
      'ShowProvisionalDialog', async () => {
        const provisionalDestination = {
          extensionId: 'ABC123',
          extensionName: 'ABC Printing',
          id: 'XYZDevice',
          name: 'XYZ',
          provisional: true,
        };

        // Set the extension destinations and force the destination store to
        // reload printers.
        nativeLayer.setExtensionDestinations([[provisionalDestination]]);
        await finishSetup();
        flush();
        const provisionalDialog = dialog.shadowRoot!.querySelector(
            'print-preview-provisional-destination-resolver')!;
        assertFalse(provisionalDialog.$.dialog.open);
        const list =
            dialog.shadowRoot!.querySelector('print-preview-destination-list')!;
        const printerItems = list.shadowRoot!.querySelectorAll(
            'print-preview-destination-list-item');

        // Should have 5 local destinations, Save as PDF + extension
        // destination.
        assertEquals(7, printerItems.length);
        const provisionalItem = Array.from(printerItems).find(printerItem => {
          return printerItem.destination.id === provisionalDestination.id;
        });

        // Click the provisional destination to select it.
        provisionalItem!.click();
        flush();
        assertTrue(provisionalDialog.$.dialog.open);

        // Send escape key on provisionalDialog. Destinations dialog should
        // not close.
        const whenClosed = eventToPromise('close', provisionalDialog);
        keyEventOn(provisionalDialog, 'keydown', 19, [], 'Escape');
        flush();
        await whenClosed;

        assertFalse(provisionalDialog.$.dialog.open);
        assertTrue(dialog.$.dialog.open);
      });

  // Test that checks that print server searchable input and its selections are
  // updated according to the PRINT_SERVERS_CHANGED event.
  test(
      'PrintServersChanged', async () => {
        await finishSetup();

        const printServers = [
          {id: 'print-server-1', name: 'Print Server 1'},
          {id: 'print-server-2', name: 'Print Server 2'},
        ];
        const isSingleServerFetchingMode = true;
        webUIListenerCallback('print-servers-config-changed', {
          printServers: printServers,
          isSingleServerFetchingMode: isSingleServerFetchingMode,
        });
        flush();

        assertFalse(dialog.shadowRoot!
                        .querySelector<HTMLElement>(
                            '.server-search-box-input')!.hidden);
        const serverSelector =
            dialog.shadowRoot!.querySelector('.server-search-box-input')!;
        const serverSelections =
            serverSelector.shadowRoot!.querySelectorAll('.list-item');
        assertEquals(
            'Print Server 1', serverSelections[0]!.textContent!.trim());
        assertEquals(
            'Print Server 2', serverSelections[1]!.textContent!.trim());
      });

  // Tests that choosePrintServers is called when the print server searchable
  // input value is changed.
  test(
      'PrintServerSelected', async () => {
        await finishSetup();
        const printServers = [
          {id: 'user-print-server-1', name: 'Print Server 1'},
          {id: 'user-print-server-2', name: 'Print Server 2'},
          {id: 'device-print-server-1', name: 'Print Server 1'},
          {id: 'device-print-server-2', name: 'Print Server 2'},
        ];
        const isSingleServerFetchingMode = true;
        webUIListenerCallback('print-servers-config-changed', {
          printServers: printServers,
          isSingleServerFetchingMode: isSingleServerFetchingMode,
        });
        flush();
        nativeLayerCros.reset();

        const pendingPrintServerId =
            nativeLayerCros.whenCalled('choosePrintServers');
        dialog.shadowRoot!.querySelector('searchable-drop-down-cros')!.value =
            'Print Server 2';
        flush();

        assertEquals(1, nativeLayerCros.getCallCount('choosePrintServers'));
        assertDeepEquals(
            ['user-print-server-2', 'device-print-server-2'],
            await pendingPrintServerId);
      });

  // Test that the correct elements are displayed when the destination store has
  // destinations.
  test(
      'PrinterSetupAssistanceHasDestinations', async () => {
        await recreateElementAndFinishSetup(/*removeDestinations=*/ false);

        // Manage printers button hidden when there are valid destinations.
        const managePrintersButton =
            dialog.shadowRoot!.querySelector<HTMLElement>(
                'cr-button:not(.cancel-button)');
        assertTrue(isVisible(managePrintersButton));

        // Printer setup element should not be displayed when there are
        // valid destinations.
        const printerSetupInfo = dialog.shadowRoot!.querySelector<HTMLElement>(
            'print-preview-printer-setup-info-cros')!;
        assertTrue(printerSetupInfo.hidden);

        // Destination list should display if there are valid destinations.
        const destinationList =
            dialog.shadowRoot!.querySelector<HTMLElement>('#printList');
        assertTrue(isVisible(destinationList));

        // Destination search box should be shown if there are valid
        // destinations.
        const searchBox = dialog.shadowRoot!.querySelector<HTMLElement>(
            'print-preview-search-box');
        assertTrue(isVisible(searchBox));
      });

  // Test that the correct elements are displayed when the printer setup
  // assistance flag is on and destination store has found destinations but
  // is still searching for more.
  test('PrinterSetupAssistanceHasDestinationsSearching', async () => {
    nativeLayer.setSimulateNoResponseForGetPrinters(true);

    document.body.appendChild(dialog);
    await nativeLayer.whenCalled('getPrinterCapabilities');
    destinationStore.startLoadAllDestinations();
    dialog.show();
    flush();

    // Throbber should show while DestinationStore is still searching.
    const throbber =
        dialog.shadowRoot!.querySelector<HTMLElement>('.throbber-container');
    assertTrue(!!throbber);
    assertFalse(
        throbber.hidden,
        'Loading UI should display while DestinationStore is searching');

    // Manage printers button hidden when there are valid destinations.
    const managePrintersButton = dialog.shadowRoot!.querySelector<HTMLElement>(
        'cr-button:not(.cancel-button)');
    assertTrue(isVisible(managePrintersButton));

    // Printer setup element should not be displayed when there are
    // valid destinations.
    const printerSetupInfo = dialog.shadowRoot!.querySelector<HTMLElement>(
        'print-preview-printer-setup-info-cros')!;
    assertTrue(printerSetupInfo.hidden);

    // Destination list should display if there are valid destinations.
    const destinationList =
        dialog.shadowRoot!.querySelector<HTMLElement>('#printList');
    assertTrue(isVisible(destinationList));

    // Destination search box should be shown if there are valid
    // destinations.
    const searchBox = dialog.shadowRoot!.querySelector<HTMLElement>(
        'print-preview-search-box');
    assertTrue(isVisible(searchBox));
  });

  // Test that the correct elements are displayed when the printer setup
  // assistance flag is on and destination store has no destinations.
  test(
      'PrinterSetupAssistanceHasNoDestinations', async () => {
        await recreateElementAndFinishSetup(/*removeDestinations=*/ true);

        // Manage printers button hidden when there are no destinations.
        const managePrintersButton =
            dialog.shadowRoot!.querySelector<HTMLElement>(
                'cr-button:not(.cancel-button)');
        assertFalse(isVisible(managePrintersButton));

        // Printer setup element should be displayed when there are no valid
        // destinations.
        const printerSetupInfo =
            dialog.shadowRoot!
                .querySelector<PrintPreviewPrinterSetupInfoCrosElement>(
                    PrintPreviewPrinterSetupInfoCrosElement.is)!;
        assertFalse(printerSetupInfo.hidden);
        assertEquals(
            PrinterSetupInfoMessageType.NO_PRINTERS,
            printerSetupInfo!.messageType);

        // Destination list should be hidden if there are no valid destinations.
        const destinationList =
            dialog.shadowRoot!.querySelector<HTMLElement>('#printList');
        assertFalse(isVisible(destinationList));

        // Destination search box should be hidden if there are no valid
        // destinations.
        const searchBox = dialog.shadowRoot!.querySelector<HTMLElement>(
            'print-preview-search-box');
        assertFalse(isVisible(searchBox));
      });

  // Test that `PrintPreview.PrinterSettingsLaunchSource` metric is recorded
  // with bucket `DESTINATION_DIALOG_CROS_HAS_PRINTERS` when flag is on and
  // destination store has destinations.
  test(
      'ManagePrintersMetrics_HasDestinations', async () => {
        await recreateElementAndFinishSetup(/*removeDestinations=*/ false);

        assertEquals(0, nativeLayer.getCallCount('recordInHistogram'));

        // Manage printers button hidden when there are valid destinations.
        const managePrintersButton =
            dialog.shadowRoot!.querySelector<HTMLElement>(
                'cr-button:not(.cancel-button)')!;
        managePrintersButton.click();

        // Call should use bucket `DESTINATION_DIALOG_CROS_HAS_PRINTERS`.
        verifyRecordInHistogramCall(/*callIndex=*/ 0, /*bucket=*/ 2);
      });

  // Test that `PrintPreview.PrinterSettingsLaunchSource` metric is recorded
  // with bucket `DESTINATION_DIALOG_CROS_NO_PRINTERS` when flag is on and
  // destination store has no destinations.
  test(
      'ManagePrintersMetrics_HasNoDestinations', async () => {
        await recreateElementAndFinishSetup(/*removeDestinations=*/ true);

        assertEquals(0, nativeLayer.getCallCount('recordInHistogram'));

        // Manage printers button hidden when there are no destinations.
        const printerSetupInfo =
            dialog.shadowRoot!
                .querySelector<PrintPreviewPrinterSetupInfoCrosElement>(
                    PrintPreviewPrinterSetupInfoCrosElement.is)!;
        const managePrintersButton =
            printerSetupInfo.shadowRoot!.querySelector<HTMLElement>(
                'cr-button')!;
        managePrintersButton.click();

        // Call should use bucket `DESTINATION_DIALOG_CROS_NO_PRINTERS`.
        verifyRecordInHistogramCall(/*callIndex=*/ 0, /*bucket=*/ 1);
      });

  // Test that the correct elements are displayed when the printer setup
  // assistance flag is on, destination store has destinations, and
  // getShowManagePrinters return false. Simulates opening print preview from
  // a UI which cannot launch settings (ex. OS Settings app).
  test(
      'PrinterSetupAssistanceHasDestinations_ShowManagedPrintersFalse',
      async () => {
        nativeLayerCros.setShowManagePrinters(false);
        await recreateElementAndFinishSetup(/*removeDestinations=*/ false);

        // Manage printers button hidden when show manage printers returns
        // false.
        const managePrintersButton =
            dialog.shadowRoot!.querySelector<HTMLElement>('#managePrinters');
        assertFalse(isVisible(managePrintersButton));

        // Cancel button shown when there are valid destinations.
        const cancelButton = dialog.shadowRoot!.querySelector<HTMLElement>(
            'cr-button.cancel-button');
        assertTrue(isVisible(cancelButton));

        // Printer setup element should not be displayed when there are
        // valid destinations.
        const printerSetupInfo = dialog.shadowRoot!.querySelector<HTMLElement>(
            'print-preview-printer-setup-info-cros')!;
        assertTrue(printerSetupInfo.hidden);

        // Destination list should display if there are valid destinations.
        const destinationList =
            dialog.shadowRoot!.querySelector<HTMLElement>('#printList');
        assertTrue(isVisible(destinationList));

        // Destination search box should be shown if there are valid
        // destinations.
        const searchBox = dialog.shadowRoot!.querySelector<HTMLElement>(
            'print-preview-search-box');
        assertTrue(isVisible(searchBox));
      });

  // Test loading icon rendered while destinations are loading and for a minimum
  // of 2 seconds before destination list and search box are visible.
  test(
      'CorrectlyDisplaysAndHidesLoadingUI', async () => {
        document.body.appendChild(dialog);
        mockTimer.install();
        await nativeLayer.whenCalled('getPrinterCapabilities');
        destinationStore.startLoadAllDestinations();
        dialog.show();
        flush();

        // Dialog should be visible with loading UI displayed.
        const throbber = dialog.shadowRoot!.querySelector<HTMLElement>(
            '.throbber-container');
        assertTrue(!!throbber);
        assertFalse(
            throbber.hidden,
            'Loading UI should display while timer is running and ' +
                'destinations have not loaded');

        // Move timer forward to clear delay.
        mockTimer.tick(DESTINATION_DIALOG_CROS_LOADING_TIMER_IN_MS);

        // Dialog should be visible with loading UI displayed.
        assertFalse(
            throbber.hidden,
            'Loading UI should display while destinations have not loaded');

        // Get destinations.
        await nativeLayer.whenCalled('getPrinters');
        flush();

        // Loading UI should be hidden. Destination list and search box should
        // be visible.
        assertTrue(
            throbber.hidden,
            'Loading UI should be hidden after timer is cleared and ' +
                'destinations have loaded');
        assertTrue(
            isChildVisible(dialog, '#printList'),
            'Destination list should display');
        assertTrue(
            isChildVisible(dialog, 'print-preview-search-box'),
            'Search-box should display');
      });

  // Tests that the destination list starts hidden then will resize to display
  // destinations once they are loaded.
  test('NewDestinationsShowsAndResizesList', async () => {
    await recreateElementAndFinishSetup(/*removeDestinations=*/ true);

    // The list should start hidden and empty since there aren't destinations
    // available.
    const list =
        dialog.shadowRoot!.querySelector('print-preview-destination-list')!;
    let printerItems = list.shadowRoot!.querySelectorAll(
        'print-preview-destination-list-item');
    assertFalse(isVisible(list));
    assertEquals(0, printerItems.length);

    // Add destinations then trigger them to load.
    nativeLayer.setLocalDestinations(localDestinations);
    await destinationStore.reloadLocalPrinters();
    flush();

    // Now expect all the local destinations to be drawn.
    printerItems = list.shadowRoot!.querySelectorAll(
        'print-preview-destination-list-item');
    assertTrue(isVisible(list));
    assertEquals(6, printerItems.length);
  });
});