chromium/chrome/test/data/webui/chromeos/firmware_update/firmware_update_dialog_test.ts

// Copyright 2021 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://accessory-update/firmware_update_dialog.js';

import {fakeFirmwareUpdate} from 'chrome://accessory-update/fake_data.js';
import {DeviceRequest, DeviceRequestId, DeviceRequestKind, UpdateState} from 'chrome://accessory-update/firmware_update.mojom-webui.js';
import {FirmwareUpdateDialogElement} from 'chrome://accessory-update/firmware_update_dialog.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {strictQuery} from 'chrome://resources/ash/common/typescript_utils/strict_query.js';
import {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import {assert} from 'chrome://resources/js/assert.js';
import {mojoString16ToString} from 'chrome://resources/js/mojo_type_util.js';
import {PaperProgressElement} from 'chrome://resources/polymer/v3_0/paper-progress/paper-progress.js';
import {assertEquals, assertFalse, assertThrows, assertTrue} from 'chrome://webui-test/chromeos/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {isVisible} from 'chrome://webui-test/test_util.js';

suite('FirmwareUpdateDialogTest', () => {
  let updateDialogElement: FirmwareUpdateDialogElement|null = null;

  teardown(() => {
    updateDialogElement?.remove();
    updateDialogElement = null;
  });

  function createUpdateDialogElement(): void {
    updateDialogElement = document.createElement('firmware-update-dialog');
    assert(updateDialogElement);
    updateDialogElement.update = fakeFirmwareUpdate;
    updateDialogElement.installationProgress = {
      percentage: 0,
      state: UpdateState.kIdle,
    };
    document.body.appendChild(updateDialogElement);
  }

  function setInstallationProgress(
      percentage: number, state: UpdateState): Promise<void> {
    assert(updateDialogElement);
    updateDialogElement.installationProgress = {percentage, state};
    return flushTasks();
  }

  function clickDoneButton(): Promise<void> {
    assert(updateDialogElement?.shadowRoot);
    const button = strictQuery(
        '#updateDoneButton', updateDialogElement?.shadowRoot, CrButtonElement)!;
    button.click();
    return flushTasks();
  }

  function setIsInitiallyInflight(inflight: boolean) {
    updateDialogElement?.setIsInitiallyInflightForTesting(inflight);
  }

  function getUpdateDialog(): CrDialogElement {
    assert(updateDialogElement?.shadowRoot);
    return strictQuery(
        '#updateDialog', updateDialogElement?.shadowRoot, CrDialogElement);
  }

  function getTextContent(selector: string): string {
    assert(updateDialogElement?.shadowRoot);
    const element =
        strictQuery(selector, updateDialogElement?.shadowRoot, HTMLElement);
    assert(element);
    return element.textContent?.trim() ?? '';
  }

  function createDeviceRequest(
      id: DeviceRequestId,
      kind: DeviceRequestKind = DeviceRequestKind.kImmediate): DeviceRequest {
    return {id, kind};
  }

  test('DialogStateUpdatesCorrectly', async () => {
    createUpdateDialogElement();
    assert(updateDialogElement?.shadowRoot);
    // Start update.
    await setInstallationProgress(1, UpdateState.kUpdating);
    assertTrue(getUpdateDialog().open);

    // |UpdateState.KIdle| handled correctly while an update is still
    // in-progress.
    await setInstallationProgress(20, UpdateState.kIdle);
    assertTrue(getUpdateDialog().open);

    // Dialog remains open while the device is restarting.
    await setInstallationProgress(70, UpdateState.kRestarting);
    assertTrue(getUpdateDialog().open);

    // Dialog remains open when the update is completed.
    await setInstallationProgress(100, UpdateState.kSuccess);
    assertTrue(getUpdateDialog().open);

    // Dialog closes when the "Done" button is clicked.
    await clickDoneButton();
    assertFalse(
        !!updateDialogElement.shadowRoot.querySelector('#updateDialog'));
  });

  test('DeviceRestarting', async () => {
    createUpdateDialogElement();
    assert(updateDialogElement?.shadowRoot);
    // Start update.
    await setInstallationProgress(1, UpdateState.kUpdating);
    assertTrue(getUpdateDialog().open);

    // Dialog remains open while the device is restarting.
    await setInstallationProgress(70, UpdateState.kRestarting);
    assertTrue(getUpdateDialog().open);

    // Correct text is shown.
    assertEquals(
        getTextContent('#updateDialogTitle'),
        loadTimeData.getStringF(
            'restartingTitleText',
            mojoString16ToString(updateDialogElement.update!.deviceName)));
    assertEquals(
        getTextContent('#updateDialogBody'),
        loadTimeData.getString('restartingBodyText'));
    // Body text should not have an aria-live value for non-requests.
    assertEquals(
        strictQuery(
            '#updateDialogBody', updateDialogElement.shadowRoot, HTMLDivElement)
            .ariaLive,
        '');
    assertEquals(
        getTextContent('#progress'),
        loadTimeData.getString('restartingFooterText'));
    // Check that the indeterminate progress is shown.
    assertTrue(isVisible(updateDialogElement.shadowRoot.querySelector(
        '#indeterminateProgressBar')));
    // No percentage progress bar.
    assertFalse(isVisible(
        updateDialogElement.shadowRoot.querySelector('#updateProgressBar')));
  });

  test('UpdateDialogContent', async () => {
    createUpdateDialogElement();
    assert(updateDialogElement?.shadowRoot);
    // Start update.
    await setInstallationProgress(1, UpdateState.kUpdating);
    assertTrue(getUpdateDialog()!.open);

    // Check dialog contents
    assertEquals(
        getTextContent('#updateDialogTitle'),
        loadTimeData.getStringF(
            'updating',
            mojoString16ToString(updateDialogElement.update!.deviceName)));
    assertEquals(
        getTextContent('#updateDialogBody'),
        loadTimeData.getString('updatingInfo'));
    const percentBar = updateDialogElement.shadowRoot.querySelector(
                           '#updateProgressBar') as PaperProgressElement;
    const percentBarStatus = percentBar.value;
    assertEquals(1, percentBarStatus);

    // Dialog remains open while the device is restarting.
    await setInstallationProgress(70, UpdateState.kRestarting);
    assertTrue(getUpdateDialog().open);

    // Correct text is shown.
    assertEquals(
        getTextContent('#updateDialogTitle'),
        loadTimeData.getStringF(
            'restartingTitleText',
            mojoString16ToString(updateDialogElement.update!.deviceName)));
    assertEquals(
        getTextContent('#updateDialogBody'),
        loadTimeData.getString('restartingBodyText'));
    assertEquals(
        getTextContent('#progress'),
        loadTimeData.getString('restartingFooterText'));
    // Check that the indeterminate progress is shown.
    assertTrue(isVisible(updateDialogElement.shadowRoot.querySelector(
        '#indeterminateProgressBar')));
    // No percentage progress bar.
    assertFalse(isVisible(
        updateDialogElement.shadowRoot.querySelector('#updateProgressBar')));
  });

  test('ProgressBarAppears', async () => {
    createUpdateDialogElement();
    assert(updateDialogElement?.shadowRoot);
    // Simulate update inflight, but restarting. Idle state during inflight
    // is equivalent as a restart phase.
    setIsInitiallyInflight(/*inflight=*/ true);
    await flushTasks();
    await setInstallationProgress(0, UpdateState.kIdle);
    assertTrue(getUpdateDialog().open);
    assertTrue(isVisible(strictQuery(
        '#indeterminateProgressBar', updateDialogElement.shadowRoot,
        HTMLElement)));

    // Set inflight to false, expect no progress bar.
    setIsInitiallyInflight(/*inflight=*/ false);
    await flushTasks();
    assertFalse(isVisible(updateDialogElement.shadowRoot.querySelector(
        '#indeterminateProgressBar')));
  });

  test('UpdateDialogContent_WaitingForUser_V2Disabled', async () => {
    loadTimeData.overrideValues({isFirmwareUpdateUIV2Enabled: false});
    createUpdateDialogElement();
    assert(updateDialogElement?.shadowRoot);

    // Start update.
    await setInstallationProgress(1, UpdateState.kUpdating);
    assertTrue(getUpdateDialog().open);

    // Dialog remains open while the device is waiting for user action.
    await setInstallationProgress(70, UpdateState.kWaitingForUser);
    assertTrue(getUpdateDialog().open);

    // If the v2 flag is disabled, the dialog should indicate that it's
    // restarting when the state is kWaitingForUser.
    assertEquals(
        getTextContent('#updateDialogTitle'),
        loadTimeData.getStringF(
            'restartingTitleText',
            mojoString16ToString(updateDialogElement.update!.deviceName)));
    assertEquals(
        getTextContent('#updateDialogBody'),
        loadTimeData.getString('restartingBodyText'));
    // Body text should not have an aria-live value for non-requests.
    assertEquals(
        strictQuery(
            '#updateDialogBody', updateDialogElement.shadowRoot, HTMLDivElement)
            .ariaLive,
        '');
    assertEquals(
        getTextContent('#progress'),
        loadTimeData.getString('restartingFooterText'));
    // Check that the indeterminate progress is shown.
    assertTrue(isVisible(updateDialogElement.shadowRoot.querySelector(
        '#indeterminateProgressBar')));
    // No percentage progress bar.
    assertFalse(isVisible(
        updateDialogElement.shadowRoot.querySelector('#updateProgressBar')));
  });

  test('UpdateDialogContent_DeviceRequest_V2Disabled', async () => {
    loadTimeData.overrideValues({isFirmwareUpdateUIV2Enabled: false});
    createUpdateDialogElement();

    // Start update.
    await setInstallationProgress(1, UpdateState.kUpdating);
    assertTrue(getUpdateDialog().open);

    // Dialog remains open while the device is waiting for user action.
    await setInstallationProgress(70, UpdateState.kWaitingForUser);
    assertTrue(getUpdateDialog().open);

    // Device requests are not expected when the flag is disabled, so
    // throw an error.
    assertThrows(
        () => updateDialogElement?.onDeviceRequest(createDeviceRequest(
            DeviceRequestId.kDoNotPowerOff,
            DeviceRequestKind.kImmediate,
            )));
  });

  test('UpdateDialogContent_WaitingForUser_V2Enabled_ShowUpdate', async () => {
    loadTimeData.overrideValues({isFirmwareUpdateUIV2Enabled: true});
    createUpdateDialogElement();
    assert(updateDialogElement?.shadowRoot);

    // Start update.
    await setInstallationProgress(1, UpdateState.kUpdating);
    assertTrue(getUpdateDialog().open);

    // Dialog remains open while the device is waiting for user action.
    await setInstallationProgress(70, UpdateState.kWaitingForUser);
    assertTrue(getUpdateDialog().open);

    // If the v2 flag is enabled, and the status is kWaitingForUser, but the
    // element hasn't received the onDeviceRequest call yet, it should just
    // show the normal update dialog.
    assertEquals(
        getTextContent('#updateDialogTitle'),
        loadTimeData.getStringF(
            'updating',
            mojoString16ToString(updateDialogElement.update!.deviceName)));
    assertEquals(
        getTextContent('#updateDialogBody'),
        loadTimeData.getString('updatingInfo'));
    const percentBar = updateDialogElement.shadowRoot.querySelector(
                           '#updateProgressBar') as PaperProgressElement;
    const percentBarStatus = percentBar.value;
    assertEquals(70, percentBarStatus);
  });

  test(
      'UpdateDialogContent_DeviceRequest_V2Enabled_IgnoreNonImmediate',
      async () => {
        loadTimeData.overrideValues({isFirmwareUpdateUIV2Enabled: true});
        createUpdateDialogElement();
        assert(updateDialogElement?.shadowRoot);

        // Start update.
        await setInstallationProgress(1, UpdateState.kUpdating);
        assertTrue(getUpdateDialog().open);

        // Dialog remains open while the device is waiting for user action.
        await setInstallationProgress(70, UpdateState.kWaitingForUser);
        assertTrue(getUpdateDialog().open);

        // Non-immediate device requests should continue to show the default
        // update screen as if no request was received.
        updateDialogElement.onDeviceRequest(createDeviceRequest(
            DeviceRequestId.kDoNotPowerOff, DeviceRequestKind.kPost));
        await flushTasks();
        assertEquals(
            getTextContent('#updateDialogTitle'),
            loadTimeData.getStringF(
                'updating',
                mojoString16ToString(updateDialogElement.update!.deviceName)));
        assertEquals(
            getTextContent('#updateDialogBody'),
            loadTimeData.getString('updatingInfo'));
        let percentBar = updateDialogElement.shadowRoot.querySelector(
                             '#updateProgressBar') as PaperProgressElement;
        let percentBarStatus = percentBar.value;
        assertEquals(70, percentBarStatus);

        updateDialogElement.onDeviceRequest(createDeviceRequest(
            DeviceRequestId.kDoNotPowerOff, DeviceRequestKind.kUnknown));
        await flushTasks();

        // Non-immediate device requests should continue to show the default
        // update screen as if no request was received.
        assertEquals(
            getTextContent('#updateDialogTitle'),
            loadTimeData.getStringF(
                'updating',
                mojoString16ToString(updateDialogElement.update!.deviceName)));
        assertEquals(
            getTextContent('#updateDialogBody'),
            loadTimeData.getString('updatingInfo'));
        percentBar = updateDialogElement.shadowRoot.querySelector(
                         '#updateProgressBar') as PaperProgressElement;
        percentBarStatus = percentBar.value;
        assertEquals(70, percentBarStatus);
      });

  test('UpdateDialogContent_DeviceRequest_V2Enabled', async () => {
    loadTimeData.overrideValues({isFirmwareUpdateUIV2Enabled: true});
    createUpdateDialogElement();
    assert(updateDialogElement?.shadowRoot);

    // Start update.
    await setInstallationProgress(1, UpdateState.kUpdating);
    assertTrue(getUpdateDialog().open);

    // Dialog remains open while the device is waiting for user action.
    await setInstallationProgress(70, UpdateState.kWaitingForUser);
    assertTrue(getUpdateDialog().open);

    // If the v2 flag is enabled, the dialog should show the associated string
    // for the given device request.
    const idToExpectedString: Map<DeviceRequestId, string> = new Map([
      [DeviceRequestId.kRemoveReplug, 'requestIdRemoveReplug'],
      [DeviceRequestId.kInsertUSBCable, 'requestIdInsertUsbCable'],
      [DeviceRequestId.kRemoveUSBCable, 'requestIdRemoveUsbCable'],
      [DeviceRequestId.kPressUnlock, 'requestIdPressUnlock'],
      [DeviceRequestId.kDoNotPowerOff, 'requestIdDoNotPowerOff'],
      [DeviceRequestId.kReplugInstall, 'requestIdReplugInstall'],
    ]);

    const deviceName =
        mojoString16ToString(updateDialogElement.update!.deviceName);

    for (const [deviceRequestID, expectedString] of idToExpectedString
             .entries()) {
      updateDialogElement.onDeviceRequest(createDeviceRequest(deviceRequestID));
      await flushTasks();

      // Title of dialog should be generic "Updating [device]"
      assertEquals(
          getTextContent('#updateDialogTitle'),
          loadTimeData.getStringF('updating', deviceName));

      // Body of dialog should correspond to the type of request.
      // In the case of the "DoNotPowerOff" request type, the device name is not
      // part of the body text, so the expected string does not include the
      // device name.
      if (deviceRequestID === DeviceRequestId.kDoNotPowerOff) {
        assertEquals(
            getTextContent('#updateDialogBody'),
            loadTimeData.getString(expectedString));
      } else {
        assertEquals(
            getTextContent('#updateDialogBody'),
            loadTimeData.getStringF(expectedString, deviceName));
      }

      assert(updateDialogElement?.shadowRoot);
      // For user requests, the dialog body should be an assertive aria-live
      // region.
      assertEquals(
          strictQuery(
              '#updateDialogBody', updateDialogElement.shadowRoot,
              HTMLDivElement)
              .ariaLive,
          'assertive');
      assertEquals(
          getTextContent('#progress'),
          loadTimeData.getStringF('waitingFooterText', 70));

      // Percentage progress should be shown when waiting for user action, but
      // the bar should be disabled.
      assertTrue(isVisible(
          updateDialogElement.shadowRoot.querySelector('#updateProgressBar')));
      assertTrue(
          !!updateDialogElement.shadowRoot.querySelector('#updateProgressBar')!
                .hasAttribute('disabled'));
      // Indeterminate progress should not be shown.
      assertFalse(isVisible(updateDialogElement.shadowRoot.querySelector(
          '#indeterminateProgressBar')));
    }
  });
});