chromium/chrome/test/data/webui/print_preview/printer_status_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 {PrintPreviewDestinationDropdownCrosElement, PrintPreviewDestinationSelectCrosElement} from 'chrome://print/print_preview.js';
import {Destination, DestinationOrigin, Error, NativeLayerCrosImpl, NativeLayerImpl, PrinterStatusReason, PrinterStatusSeverity, State} from 'chrome://print/print_preview.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {MockController} from 'chrome://webui-test/mock_controller.js';
import {waitBeforeNextRender} from 'chrome://webui-test/polymer_test_util.js';

import {NativeLayerCrosStub} from './native_layer_cros_stub.js';
import {NativeLayerStub} from './native_layer_stub.js';
import {FakeMediaQueryList, getGoogleDriveDestination, getSaveAsPdfDestination} from './print_preview_test_utils.js';

suite('PrinterStatusTest', function() {
  const PRINTER_ICON_GREEN = 'print-preview:printer-status-green';

  const PRINTER_ICON_GREY = 'print-preview:printer-status-grey';

  const PRINTER_ICON_ORANGE = 'print-preview:printer-status-orange';

  const PRINTER_ICON_RED = 'print-preview:printer-status-red';

  let destinationSelect: PrintPreviewDestinationSelectCrosElement;

  let nativeLayerCros: NativeLayerCrosStub;

  let mockController: MockController;

  let fakePrefersColorSchemeMediaQueryList: FakeMediaQueryList;

  function setNativeLayerPrinterStatusMap() {
    [{
      printerId: 'ID1',
      statusReasons: [],
      timestamp: 0,
    },
     {
       printerId: 'ID2',
       statusReasons: [{
         reason: PrinterStatusReason.LOW_ON_PAPER,
         severity: PrinterStatusSeverity.UNKNOWN_SEVERITY,
       }],
       timestamp: 0,
     },
     {
       printerId: 'ID3',
       statusReasons: [{
         reason: PrinterStatusReason.LOW_ON_PAPER,
         severity: PrinterStatusSeverity.REPORT,
       }],
       timestamp: 0,
     },
     {
       printerId: 'ID4',
       statusReasons: [{
         reason: PrinterStatusReason.LOW_ON_PAPER,
         severity: PrinterStatusSeverity.WARNING,
       }],
       timestamp: 0,
     },
     {
       printerId: 'ID5',
       statusReasons: [{
         reason: PrinterStatusReason.LOW_ON_PAPER,
         severity: PrinterStatusSeverity.ERROR,
       }],
       timestamp: 0,
     },
     {
       printerId: 'ID6',
       statusReasons: [
         {
           reason: PrinterStatusReason.DEVICE_ERROR,
           severity: PrinterStatusSeverity.UNKNOWN_SEVERITY,
         },
         {
           reason: PrinterStatusReason.PRINTER_QUEUE_FULL,
           severity: PrinterStatusSeverity.ERROR,
         },
       ],
       timestamp: 0,
     },
     {
       printerId: 'ID7',
       statusReasons: [
         {
           reason: PrinterStatusReason.DEVICE_ERROR,
           severity: PrinterStatusSeverity.REPORT,
         },
         {
           reason: PrinterStatusReason.PRINTER_QUEUE_FULL,
           severity: PrinterStatusSeverity.UNKNOWN_SEVERITY,
         },
       ],
       timestamp: 0,
     },
     {
       printerId: 'ID8',
       statusReasons: [{
         reason: PrinterStatusReason.UNKNOWN_REASON,
         severity: PrinterStatusSeverity.ERROR,
       }],
       timestamp: 0,
     },
     {
       printerId: 'ID9',
       statusReasons: [{
         reason: PrinterStatusReason.UNKNOWN_REASON,
         severity: PrinterStatusSeverity.UNKNOWN_SEVERITY,
       }],
       timestamp: 0,
     },
     {
       printerId: 'ID10',
       statusReasons: [{
         reason: PrinterStatusReason.PRINTER_UNREACHABLE,
         severity: PrinterStatusSeverity.ERROR,
       }],
       timestamp: 0,
     }]
        .forEach(
            status => nativeLayerCros.addPrinterStatusToMap(
                status.printerId, status));
  }

  function createDestination(
      id: string, displayName: string,
      destinationOrigin: DestinationOrigin): Destination {
    return new Destination(id, destinationOrigin, displayName);
  }

  function escapeForwardSlahes(value: string): string {
    return value.replace(/\//g, '\\/');
  }

  function getIconString(
      dropdown: PrintPreviewDestinationDropdownCrosElement,
      key: string): string {
    return dropdown.shadowRoot!.querySelector(`#${
        escapeForwardSlahes(key)}`)!.querySelector('iron-icon')!.icon!;
  }

  // Mocks calls to window.matchMedia, returning false by default.
  function configureMatchMediaMock() {
    mockController = new MockController();
    const matchMediaMock =
        mockController.createFunctionMock(window, 'matchMedia');
    fakePrefersColorSchemeMediaQueryList =
        new FakeMediaQueryList('(prefers-color-scheme: dark)');
    matchMediaMock.returnValue = fakePrefersColorSchemeMediaQueryList;
    assertFalse(window.matchMedia('(prefers-color-scheme: dark)').matches);
  }

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

    // Stub out native layer.
    NativeLayerImpl.setInstance(new NativeLayerStub());
    nativeLayerCros = new NativeLayerCrosStub();
    NativeLayerCrosImpl.setInstance(nativeLayerCros);
    setNativeLayerPrinterStatusMap();

    destinationSelect =
        document.createElement('print-preview-destination-select-cros');
    document.body.appendChild(destinationSelect);
  });

  teardown(function() {
    mockController.reset();
  });

  /**
   * Verifies icon contains expected color depending on destination's
   * computed printer status reason from `getStatusReasonFromPrinterStatus`.
   */
  test('PrinterStatusUpdatesColor', function() {
    const destination1 =
        createDestination('ID1', 'One', DestinationOrigin.CROS);
    const destination2 =
        createDestination('ID2', 'Two', DestinationOrigin.CROS);
    const destination3 =
        createDestination('ID3', 'Three', DestinationOrigin.CROS);
    const destination4 =
        createDestination('ID4', 'Four', DestinationOrigin.CROS);
    const destination5 =
        createDestination('ID5', 'Five', DestinationOrigin.CROS);
    const destination6 =
        createDestination('ID6', 'Six', DestinationOrigin.CROS);
    const destination7 =
        createDestination('ID7', 'Seven', DestinationOrigin.CROS);
    const destination8 =
        createDestination('ID8', 'Eight', DestinationOrigin.CROS);
    const destination9 =
        createDestination('ID9', 'Nine', DestinationOrigin.CROS);

    return waitBeforeNextRender(destinationSelect)
        .then(() => {
          const whenStatusRequestsDone =
              nativeLayerCros.waitForMultiplePrinterStatusRequests(9);

          destinationSelect.recentDestinationList = [
            destination1,
            destination2,
            destination3,
            destination4,
            destination5,
            destination6,
            destination7,
            destination8,
            destination9,
          ];

          return whenStatusRequestsDone;
        })
        .then(() => {
          return waitBeforeNextRender(destinationSelect);
        })
        .then(() => {
          const dropdown = destinationSelect.$.dropdown;

          // Empty printer status.
          assertEquals(
              PRINTER_ICON_GREEN, getIconString(dropdown, destination1.key));

          // Error printer status with unknown severity.
          assertEquals(
              PRINTER_ICON_GREEN, getIconString(dropdown, destination2.key));

          // Error printer status with report severity.
          assertEquals(
              PRINTER_ICON_GREEN, getIconString(dropdown, destination3.key));

          // Error printer status with warning severity.
          assertEquals(
              PRINTER_ICON_ORANGE, getIconString(dropdown, destination4.key));

          // Error printer status with error severity.
          assertEquals(
              PRINTER_ICON_ORANGE, getIconString(dropdown, destination5.key));

          // Error printer status with unknown severity + error printer
          // status with error severity.
          assertEquals(
              PRINTER_ICON_ORANGE, getIconString(dropdown, destination6.key));

          // Error printer status with unknown severity + error printer
          // status with report severity.
          assertEquals(
              PRINTER_ICON_GREEN, getIconString(dropdown, destination7.key));

          // Unknown reason printer status with error severity.
          assertEquals(
              PRINTER_ICON_GREEN, getIconString(dropdown, destination8.key));

          // Unknown reason printer status with unknown severity.
          assertEquals(
              PRINTER_ICON_GREEN, getIconString(dropdown, destination9.key));
        });
  });

  test('SendStatusRequestOnce', function() {
    return waitBeforeNextRender(destinationSelect).then(() => {
      const destination1 =
          createDestination('ID1', 'One', DestinationOrigin.CROS);
      const destination2 =
          createDestination('ID2', 'Two', DestinationOrigin.CROS);

      destinationSelect.recentDestinationList = [
        destination1,
        destination2,
        createDestination('ID3', 'Three', DestinationOrigin.EXTENSION),
        createDestination('ID4', 'Four', DestinationOrigin.EXTENSION),
      ];
      assertEquals(
          2, nativeLayerCros.getCallCount('requestPrinterStatusUpdate'));

      // Update list with 2 existing destinations and one new destination.
      // Make sure the requestPrinterStatusUpdate only gets called for the
      // new destination.
      destinationSelect.recentDestinationList = [
        destination1,
        destination2,
        createDestination('ID5', 'Five', DestinationOrigin.CROS),
      ];
      assertEquals(
          3, nativeLayerCros.getCallCount('requestPrinterStatusUpdate'));
    });
  });

  test('HiddenStatusText', function() {
    const destinationStatus =
        destinationSelect.shadowRoot!.querySelector<HTMLElement>(
            '.destination-additional-info')!;
    return waitBeforeNextRender(destinationSelect)
        .then(() => {
          const destinationWithoutErrorStatus =
              createDestination('ID1', 'One', DestinationOrigin.CROS);
          // Destination with ID4 will return an error printer status that will
          // trigger the error text being populated.
          const destinationWithErrorStatus =
              createDestination('ID4', 'Four', DestinationOrigin.CROS);
          destinationSelect.recentDestinationList = [
            destinationWithoutErrorStatus,
            destinationWithErrorStatus,
          ];

          const destinationEulaWrapper =
              destinationSelect.$.destinationEulaWrapper;

          destinationSelect.destination = destinationWithoutErrorStatus;
          assertTrue(destinationStatus.hidden);
          assertTrue(destinationEulaWrapper.hidden);

          destinationSelect.set(
              'destination.eulaUrl', 'chrome://os-credits/eula');
          assertFalse(destinationEulaWrapper.hidden);

          destinationSelect.destination = destinationWithErrorStatus;
          return nativeLayerCros.whenCalled('requestPrinterStatusUpdate');
        })
        .then(() => {
          return waitBeforeNextRender(destinationSelect);
        })
        .then(() => {
          assertFalse(destinationStatus.hidden);
        });
  });

  test('ChangeIcon', function() {
    return waitBeforeNextRender(destinationSelect).then(() => {
      const localCrosPrinter =
          createDestination('ID1', 'One', DestinationOrigin.CROS);
      const localNonCrosPrinter =
          createDestination('ID2', 'Two', DestinationOrigin.LOCAL);
      const crosEnterprisePrinter = new Destination(
          'ID5', DestinationOrigin.CROS, 'Five', {isEnterprisePrinter: true});
      const saveToDrive = getGoogleDriveDestination();
      const saveAsPdf = getSaveAsPdfDestination();

      destinationSelect.recentDestinationList = [
        localCrosPrinter,
        saveToDrive,
        saveAsPdf,
      ];
      const dropdown = destinationSelect.$.dropdown;

      destinationSelect.destination = localCrosPrinter;
      destinationSelect.updateDestination();
      assertEquals(PRINTER_ICON_GREY, dropdown.destinationIcon);

      destinationSelect.destination = localNonCrosPrinter;
      destinationSelect.updateDestination();
      assertEquals('print-preview:print', dropdown.destinationIcon);

      destinationSelect.destination = crosEnterprisePrinter;
      destinationSelect.updateDestination();
      assertEquals(
          'print-preview:business-printer-status-grey',
          dropdown.destinationIcon);

      destinationSelect.destination = saveToDrive;
      destinationSelect.updateDestination();
      assertEquals('print-preview:save-to-drive', dropdown.destinationIcon);

      destinationSelect.destination = saveAsPdf;
      destinationSelect.updateDestination();
      assertEquals('cr:insert-drive-file', dropdown.destinationIcon);
    });
  });

  test('SuccessfulPrinterStatusAfterRetry', function() {
    nativeLayerCros.simulateStatusRetrySuccesful();

    const destination =
        createDestination('ID10', 'Ten', DestinationOrigin.CROS);
    destination.setPrinterStatusRetryTimeoutForTesting(100);
    const whenStatusRequestsDonePromise =
        nativeLayerCros.waitForMultiplePrinterStatusRequests(2);
    destinationSelect.recentDestinationList = [
      destination,
    ];

    const dropdown = destinationSelect.$.dropdown;
    return whenStatusRequestsDonePromise
        .then(() => {
          assertEquals(
              PRINTER_ICON_GREY, getIconString(dropdown, destination.key));
          assertEquals(
              0,
              nativeLayerCros.getCallCount(
                  'recordPrinterStatusRetrySuccessHistogram'));
          return waitBeforeNextRender(destinationSelect);
        })
        .then(() => {
          // The printer status is requested twice because of the retry.
          assertEquals(
              2, nativeLayerCros.getCallCount('requestPrinterStatusUpdate'));
          assertEquals(
              PRINTER_ICON_GREEN, getIconString(dropdown, destination.key));
          assertEquals(
              1,
              nativeLayerCros.getCallCount(
                  'recordPrinterStatusRetrySuccessHistogram'));
          assertEquals(
              true,
              nativeLayerCros.getArgs(
                  'recordPrinterStatusRetrySuccessHistogram')[0]);
        });
  });

  test('StatusTextClass', async function() {
    const destinationStatus =
        destinationSelect.shadowRoot!.querySelector<HTMLElement>(
            '.destination-additional-info')!;
    await waitBeforeNextRender(destinationSelect);
    const destinationWithoutErrorStatus =
        createDestination('ID1', 'One', DestinationOrigin.CROS);
    const destinationWithErrorStatus1 =
        createDestination('ID4', 'Four', DestinationOrigin.CROS);
    const destinationWithErrorStatus2 =
        createDestination('ID10', 'Ten', DestinationOrigin.CROS);
    destinationSelect.recentDestinationList = [
      destinationWithoutErrorStatus,
      destinationWithErrorStatus1,
      destinationWithErrorStatus2,
    ];

    // Destination with ID1 will not have an error status shown.
    destinationSelect.destination = destinationWithoutErrorStatus;
    destinationSelect.updateDestination();
    await destinationSelect.destination.requestPrinterStatus();
    await waitBeforeNextRender(destinationSelect);

    const statusText: HTMLElement =
        destinationStatus.querySelector<HTMLElement>('#statusText')!;
    assertEquals('destination-status', statusText.className.trim());

    // Destination with ID4 will return an error printer status that will
    // trigger the error text being populated with status-orange class.
    destinationSelect.destination = destinationWithErrorStatus1;
    await destinationSelect.destination.requestPrinterStatus();
    await waitBeforeNextRender(destinationSelect);

    assertTrue(
        statusText.classList.contains('status-orange'), 'contains orange');

    // Destination with ID10 will return an error printer status that will
    // trigger the error text being populated with status-red class
    destinationSelect.destination = destinationWithErrorStatus2;
    await destinationSelect.destination.requestPrinterStatus();
    await waitBeforeNextRender(destinationSelect);

    assertTrue(
        statusText.classList.contains('status-red'),
        `contains red: ${statusText.className}`);
  });

  test('OverrideStatusForPrinterSetupInfo', async function() {
    await waitBeforeNextRender(destinationSelect);

    // Set to a destination without printer status errors.
    const destinationNoError =
        createDestination('ID1', 'One', DestinationOrigin.CROS);
    destinationSelect.recentDestinationList = [destinationNoError];
    await nativeLayerCros.whenCalled('requestPrinterStatusUpdate');
    destinationSelect.destination = destinationNoError;
    destinationSelect.updateDestination();

    // Confirm there is no error text and the printer status icon is green.
    const destinationStatus =
        destinationSelect.shadowRoot!.querySelector<HTMLElement>(
            '.destination-additional-info')!;
    const statusText: HTMLElement =
        destinationStatus.querySelector<HTMLElement>('#statusText')!;
    assertEquals('', statusText.textContent);
    const dropdown = destinationSelect.$.dropdown;
    assertEquals(PRINTER_ICON_GREEN, dropdown.destinationIcon);

    // Set the preview in an error state that shows the printer setup info UI
    // then verify the status text and icon update accordingly.
    destinationSelect.state = State.ERROR;
    destinationSelect.error = Error.INVALID_PRINTER;
    assertEquals('Not connected to printer', statusText.textContent!.trim());
    assertEquals(PRINTER_ICON_RED, dropdown.destinationIcon);
  });
});