chromium/chrome/test/data/webui/chromeos/shimless_rma/shimless_rma_app_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://shimless-rma/shimless_rma.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 {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {PromiseResolver} from 'chrome://resources/ash/common/promise_resolver.js';
import {strictQuery} from 'chrome://resources/ash/common/typescript_utils/strict_query.js';
import {assert} from 'chrome://resources/js/assert.js';
import {FilePath} from 'chrome://resources/mojo/mojo/public/mojom/base/file_path.mojom-webui.js';
import {CLICK_EXIT_BUTTON, DISABLE_NEXT_BUTTON, FATAL_HARDWARE_ERROR, OPEN_LOGS_DIALOG, SET_NEXT_BUTTON_LABEL, TRANSITION_STATE} from 'chrome://shimless-rma/events.js';
import {fakeCalibrationComponentsWithFails, fakeStates} from 'chrome://shimless-rma/fake_data.js';
import {FakeShimlessRmaService} from 'chrome://shimless-rma/fake_shimless_rma_service.js';
import {setShimlessRmaServiceForTesting} from 'chrome://shimless-rma/mojo_interface_provider.js';
import {OnboardingLandingPage} from 'chrome://shimless-rma/onboarding_landing_page.js';
import {OnboardingSelectComponentsPageElement} from 'chrome://shimless-rma/onboarding_select_components_page.js';
import {ButtonState, ShimlessRma} from 'chrome://shimless-rma/shimless_rma.js';
import {RmadErrorCode, State, StateResult} from 'chrome://shimless-rma/shimless_rma.mojom-webui.js';
import {disableAllButtons, enableAllButtons} from 'chrome://shimless-rma/shimless_rma_util.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chromeos/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {eventToPromise, isVisible} from 'chrome://webui-test/test_util.js';

suite('shimlessRMAAppTest', function() {
  let component: ShimlessRma|null = null;

  const service: FakeShimlessRmaService = new FakeShimlessRmaService();

  const nextButtonSelector = '#next';
  const backButtonSelector = '#back';
  const exitButtonSelector = '#exit';

  const exitDialogSelector = '#exitDialog';
  const confirmExitButtonSelector = '#confirmExitDialogButton';
  const cancelExitButtonSelector = '#cancelExitDialogButton';

  const saveLogButtonSelector = '#saveLogDialogButton';
  const logSaveDoneButtonSelector = '#logSaveDoneDialogButton';
  const logRetryButtonSelector = '#logRetryDialogButton';
  const logSavedStatusSelector = '#logSavedStatusText';
  const closeLogsDialogButtonSelector = '#closeLogDialogButton';

  const logsDialogSelector = '#logsDialog';
  const logConnectUsbMessageSelector = '#logConnectUsbMessageContainer';
  const saveLogContainerSelector = '#saveLogButtonContainer';

  const hardwareErrorPageSelector = 'hardware-error-page';
  const landingPageSelector = 'onboarding-landing-page';
  const networkPageSelector = 'onboarding-network-page';
  const updatePageSelector = 'onboarding-update-page';
  const rebootPageSelector = 'reboot-page';
  const selectComponentsPageSelector = 'onboarding-select-components-page';

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

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

  function initializeShimlessRMAApp(states: StateResult[] = fakeStates):
      Promise<void> {
    // Initialize the fake data.
    service.setStates(states);
    service.setGetCurrentOsVersionResult(/* version= */ '');
    service.setCheckForOsUpdatesResult(/* version= */ '');

    assert(!component);
    component = document.createElement(ShimlessRma.is);
    assert(component);
    document.body.appendChild(component);

    return flushTasks();
  }

  // Utility function to click next button
  function clickNext(): Promise<void> {
    assert(component);
    // Make sure the Next button is enabled.
    component.dispatchEvent(new CustomEvent(
        DISABLE_NEXT_BUTTON,
        {bubbles: true, composed: true, detail: false},
        ));
    strictQuery(nextButtonSelector, component.shadowRoot, CrButtonElement)
        .click();
    return flushTasks();
  }

  // Utility function to click back button
  function clickBack(): Promise<void> {
    assert(component);
    strictQuery(backButtonSelector, component.shadowRoot, CrButtonElement)
        .click();
    return flushTasks();
  }

  // Utility function to click exit button
  function clickExit(): Promise<void> {
    assert(component);
    strictQuery(exitButtonSelector, component.shadowRoot, CrButtonElement)
        .click();
    return flushTasks();
  }

  function clickButton(buttonNameSelector: string): Promise<void> {
    assert(component);
    strictQuery(buttonNameSelector, component.shadowRoot, CrButtonElement)
        .click();
    return flushTasks();
  }

  function openLogsDialog(): Promise<void> {
    assert(component);
    component.dispatchEvent(new CustomEvent(
        OPEN_LOGS_DIALOG,
        {bubbles: true, composed: true},
        ));
    return flushTasks();
  }

  // Verify the correct components are loaded
  test('ShimlessRMALoaded', async () => {
    await initializeShimlessRMAApp();

    assert(component);
    assert(
        strictQuery(nextButtonSelector, component.shadowRoot, CrButtonElement));
    assert(
        strictQuery(exitButtonSelector, component.shadowRoot, CrButtonElement));
    assert(
        strictQuery(backButtonSelector, component.shadowRoot, CrButtonElement));

    // The Hardware Error page should be hidden by default.
    assert(!(component.shadowRoot!.querySelector(hardwareErrorPageSelector)));
    // 3P Diagnostics should be loaded.
    strictQuery('#shimless3pDiagnostics', component.shadowRoot, HTMLElement);
  });

  // Verify clicking the next button goes to the next page and the back button
  // goes to the previous page.
  test('ShimlessRMABasicNavigation', async () => {
    await initializeShimlessRMAApp();

    assert(component);
    const initialPage =
        strictQuery(landingPageSelector, component.shadowRoot, HTMLElement);
    assertFalse(initialPage.hidden);

    // This enables the next button on the landing page.
    assert(service);
    service.triggerHardwareVerificationStatusObserver(
        /* isCompliant= */ true, /* errorMessage= */ '', /* delayMs= */ 0);
    await flushTasks();
    await clickNext();

    const selectNetworkPage =
        strictQuery(networkPageSelector, component.shadowRoot, HTMLElement);
    assertFalse(selectNetworkPage.hidden);

    // Click the back button and expect the networking page to be hidden but not
    // destroyed.
    await clickBack();
    assert(selectNetworkPage);
    assertTrue(selectNetworkPage.hidden);
    assertFalse(initialPage.hidden);
  });

  // Verify clicking the exit button attempts to abort Shimless RMA.
  test('ShimlessRMAExit', async () => {
    await initializeShimlessRMAApp();
    let abortRmaCount = 0;
    service.abortRma = () => {
      ++abortRmaCount;
      return Promise.resolve({error: RmadErrorCode.kOk});
    };

    await clickExit();

    assert(component);
    const exitDialog =
        strictQuery(exitDialogSelector, component.shadowRoot, CrDialogElement);
    assertTrue(exitDialog.open);
    strictQuery(
        confirmExitButtonSelector, component.shadowRoot, CrButtonElement)
        .click();
    await flushTasks();

    assertFalse(exitDialog.open);
    assertEquals(1, abortRmaCount);
  });

  // Verify clicking the cancel exit button doesn't attempt to abort Shimless
  // RMA.
  test('CancelExitDialog', async () => {
    await initializeShimlessRMAApp();
    let abortRmaCount = 0;
    service.abortRma = () => {
      ++abortRmaCount;
      return Promise.resolve({error: RmadErrorCode.kOk});
    };

    await clickExit();

    assert(component);
    const exitDialog =
        strictQuery(exitDialogSelector, component.shadowRoot, CrDialogElement);
    assertTrue(exitDialog.open);
    strictQuery(cancelExitButtonSelector, component.shadowRoot, CrButtonElement)
        .click();
    await flushTasks();

    assertFalse(exitDialog.open);
    assertEquals(0, abortRmaCount);
  });

  // Verify the page change doesn't change when the next button click is
  // rejected.
  test('NextButtonClickedOnNotReady', async () => {
    await initializeShimlessRMAApp();

    // Confirm the initial page is visible.
    assert(component);
    const initialPage = strictQuery(
        landingPageSelector, component.shadowRoot, OnboardingLandingPage);
    assertFalse(initialPage.hidden);

    // Click the next button expecting to go to the next page.
    const resolver = new PromiseResolver<{stateResult: StateResult}>();
    initialPage.onNextButtonClick = () => resolver.promise;
    await clickNext();

    // Reject the next button click and confirm the initial page is still
    // visible.
    resolver.reject();
    await flushTasks();
    assertFalse(initialPage.hidden);
  });

  // Verify the back button becomes visible when protected.
  test('UpdateBackButtonVisibility', async () => {
    await initializeShimlessRMAApp();

    assert(component);
    const backButton =
        strictQuery(backButtonSelector, component.shadowRoot, CrButtonElement);
    assertTrue(backButton.hidden);

    component.updateButtonState(
        /* buttonName= */ 'buttonBack', ButtonState.VISIBLE);
    await flushTasks();

    assertFalse(backButton.hidden);
  });

  // Verify the next button label updates to "Skip" when requested.
  test('UpdateNextButtonLabel', async () => {
    await initializeShimlessRMAApp([{
      state: State.kSelectComponents,
      canExit: true,
      canGoBack: true,
      error: RmadErrorCode.kOk,
    }]);

    assert(component);
    const nextButtonLabel =
        strictQuery('#nextButtonLabel', component.shadowRoot, HTMLElement);
    assertEquals(
        loadTimeData.getString('nextButtonLabel'),
        nextButtonLabel.textContent!.trim());

    // Trigger the next button to update its label.
    component.dispatchEvent(new CustomEvent(
        SET_NEXT_BUTTON_LABEL,
        {bubbles: true, composed: true, detail: 'skipButtonLabel'},
        ));
    assertEquals(
        loadTimeData.getString('skipButtonLabel'),
        nextButtonLabel.textContent!.trim());
  });

  // Verify the correct button spinners are showing based on the current state.
  test('ButtonSpinnerStates', async () => {
    await initializeShimlessRMAApp([{
      state: State.kSelectComponents,
      canExit: true,
      canGoBack: true,
      error: RmadErrorCode.kOk,
    }]);

    assert(component);
    const nextButtonSpinner =
        strictQuery('#nextButtonSpinner', component.shadowRoot, HTMLElement);
    const backButtonSpinner =
        strictQuery('#backButtonSpinner', component.shadowRoot, HTMLElement);
    const exitButtonSpinner =
        strictQuery('#exitButtonSpinner', component.shadowRoot, HTMLElement);
    assertTrue(nextButtonSpinner.hidden);
    assertTrue(backButtonSpinner.hidden);
    assertTrue(exitButtonSpinner.hidden);

    // Next spinner
    const nextResolver = new PromiseResolver<{stateResult: StateResult}>();
    strictQuery(
        selectComponentsPageSelector, component.shadowRoot,
        OnboardingSelectComponentsPageElement)
        .onNextButtonClick = () => nextResolver.promise;

    await clickNext();
    assertFalse(nextButtonSpinner.hidden);
    assertTrue(backButtonSpinner.hidden);
    assertTrue(exitButtonSpinner.hidden);

    nextResolver.resolve({
      stateResult: {
        state: State.kUpdateOs,
        error: RmadErrorCode.kOk,
        canExit: false,
        canGoBack: false,
      },
    });
    await flushTasks();

    assertTrue(nextButtonSpinner.hidden);
    assertTrue(backButtonSpinner.hidden);
    assertTrue(exitButtonSpinner.hidden);

    // Back spinner
    const backResolver = new PromiseResolver<{stateResult: StateResult}>();
    assert(service);
    service.transitionPreviousState = () => {
      return backResolver.promise;
    };

    await clickBack();
    assertTrue(nextButtonSpinner.hidden);
    assertFalse(backButtonSpinner.hidden);
    assertTrue(exitButtonSpinner.hidden);

    backResolver.resolve({
      stateResult: {
        state: State.kUpdateOs,
        error: RmadErrorCode.kOk,
        canExit: false,
        canGoBack: false,
      },
    });
    await flushTasks();
    assertTrue(nextButtonSpinner.hidden);
    assertTrue(backButtonSpinner.hidden);
    assertTrue(exitButtonSpinner.hidden);

    // Exit spinner
    const exitResolver = new PromiseResolver<{error: RmadErrorCode}>();
    service.abortRma = () => {
      return exitResolver.promise;
    };
    await clickExit();
    assertTrue(nextButtonSpinner.hidden);
    assertTrue(backButtonSpinner.hidden);
    assertTrue(exitButtonSpinner.hidden);

    assert(component);
    await clickButton(confirmExitButtonSelector);
    assertTrue(nextButtonSpinner.hidden);
    assertTrue(backButtonSpinner.hidden);
    assertFalse(exitButtonSpinner.hidden);

    exitResolver.resolve({error: RmadErrorCode.kOk});
    await flushTasks();
    assertTrue(nextButtonSpinner.hidden);
    assertTrue(backButtonSpinner.hidden);
    assertTrue(exitButtonSpinner.hidden);
  });

  // Verify when requested, all buttons are disabled with busy state overlay
  // visible.
  test('AllButtonsDisabled', async () => {
    await initializeShimlessRMAApp();

    assert(component);
    const nextButton =
        strictQuery(nextButtonSelector, component.shadowRoot, CrButtonElement);
    const backButton =
        strictQuery(nextButtonSelector, component.shadowRoot, CrButtonElement);
    const exitButton =
        strictQuery(exitButtonSelector, component.shadowRoot, CrButtonElement);
    const busyStateOverlay =
        strictQuery('#busyStateOverlay', component.shadowRoot, HTMLElement);

    assertFalse(nextButton.disabled);
    assertFalse(backButton.disabled);
    assertFalse(exitButton.disabled);

    disableAllButtons(component, /* showBusyStateOverlay= */ false);
    assertTrue(nextButton.disabled);
    assertTrue(backButton.disabled);
    assertTrue(exitButton.disabled);
    assertFalse(isVisible(busyStateOverlay));

    disableAllButtons(component, /* showBusyStateOverlay= */ true);
    assertTrue(isVisible(busyStateOverlay));

    enableAllButtons(component);
    assertFalse(nextButton.disabled);
    assertFalse(backButton.disabled);
    assertFalse(exitButton.disabled);
    assertFalse(isVisible(busyStateOverlay));
  });

  // Verify the exit button event opens the exit dialog.
  test('ExitButtonClickEventIsHandled', async () => {
    await initializeShimlessRMAApp();

    let callCounter = 0;
    const resolver = new PromiseResolver<{error: RmadErrorCode}>();
    assert(service);
    service.abortRma = () => {
      ++callCounter;
      return resolver.promise;
    };

    assert(component);
    component.dispatchEvent(new CustomEvent(
        CLICK_EXIT_BUTTON,
        {bubbles: true, composed: true},
        ));

    await flushTasks();
    assertTrue(
        strictQuery(exitDialogSelector, component.shadowRoot, CrDialogElement)
            .open);
    assertEquals(0, callCounter);
  });

  // Verify the "transition state" event is transitions to the requested page.
  test('TransitionStateListener', async () => {
    await initializeShimlessRMAApp();

    // Attempt to transition OS Update page.
    assert(component);
    component.dispatchEvent(new CustomEvent(
        TRANSITION_STATE,
        {
          bubbles: true,
          composed: true,
          detail: () => Promise.resolve({
            stateResult: {state: State.kUpdateOs, error: RmadErrorCode.kOk},
            canExit: false,
            canGoBack: false,
          }),
        },
        ));
    await flushTasks();

    // Confirm transition to the OS Update page.
    assert(strictQuery(updatePageSelector, component.shadowRoot, HTMLElement));

    // Both buttons should be hidden due to the `stateResult` response.
    assertTrue(
        strictQuery(backButtonSelector, component.shadowRoot, CrButtonElement)
            .hidden);
    assertTrue(
        strictQuery(exitButtonSelector, component.shadowRoot, CrButtonElement)
            .hidden);
  });

  // Verify the back button can't be hidden on the failed calibartion page.
  test('FailedCalibrationHiddenExitButton', async () => {
    await initializeShimlessRMAApp();

    // Initialize the fake data.
    assert(service);
    service.setGetCalibrationComponentListResult(
        fakeCalibrationComponentsWithFails);

    // Transition to Calibration Failed page but attempt to hide the back button
    // by setting `canGoBack` to false.
    assert(component);
    component.dispatchEvent(new CustomEvent(
        'transition-state',
        {
          bubbles: true,
          composed: true,
          detail: () => Promise.resolve({
            stateResult: {
              state: State.kCheckCalibration,
              error: RmadErrorCode.kOk,
              canExit: false,
              canGoBack: false,
            },
          }),
        },
        ));
    await flushTasks();

    // The back button will be hidden but the exit button should never be hidden
    // for the Calibration failed page.
    assertTrue(
        strictQuery(backButtonSelector, component.shadowRoot, CrButtonElement)
            .hidden);
    assertFalse(
        strictQuery(exitButtonSelector, component.shadowRoot, CrButtonElement)
            .hidden);
  });

  // Verify the "fatal hardware error" event launches the hardware error page.
  test('HardwareErrorEventIsHandled', async () => {
    await initializeShimlessRMAApp();

    assert(component);
    component.dispatchEvent(new CustomEvent(
        FATAL_HARDWARE_ERROR,
        {
          bubbles: true,
          composed: true,
          detail: {
            rmadErrorCode: RmadErrorCode.kProvisioningFailed,
            fatalErrorCode: 1001,
          },
        },
        ));

    await flushTasks();

    // Confirm transition to the Hardware Error page.
    assert(strictQuery(
        hardwareErrorPageSelector, component.shadowRoot, HTMLElement));
  });

  // Verify the getting the `kExpectReboot` code transitions to the reboot page.
  test('RebootErrorCodeResultsInShowingRebootPage', async () => {
    await initializeShimlessRMAApp();

    // Emulate platform sending a reboot error code.
    assert(component);
    component.dispatchEvent(new CustomEvent(
        TRANSITION_STATE,
        {
          bubbles: true,
          composed: true,
          detail: () => Promise.resolve({
            stateResult: {
              state: State.kWPDisableComplete,
              error: RmadErrorCode.kExpectReboot,
            },
          }),
        },
        ));
    await flushTasks();

    // Confirm transition to the reboot page.
    assert(strictQuery(rebootPageSelector, component.shadowRoot, HTMLElement));
  });

  // Verify the getting the `kExpectShutdown` code transitions to the reboot
  // page.
  test('ShutdownErrorCodeResultsInRebootPage', async () => {
    await initializeShimlessRMAApp();

    // Emulate platform sending a shut down error code.
    assert(component);
    component.dispatchEvent(new CustomEvent(
        TRANSITION_STATE,
        {
          bubbles: true,
          composed: true,
          detail: () => Promise.resolve({
            stateResult: {
              state: State.kWPDisableComplete,
              error: RmadErrorCode.kExpectShutdown,
            },
          }),
        },
        ));
    await flushTasks();

    // Confirm transition to the reboot page.
    assert(strictQuery(rebootPageSelector, component.shadowRoot, HTMLElement));
  });

  // Verify logs can be saved via the save logs dialog.
  test('SaveLogsToUsb', async () => {
    await initializeShimlessRMAApp();

    assert(service);
    service.triggerExternalDiskObserver(/* detected= */ true, /* delayMs= */ 0);
    await flushTasks();

    let saveLogCallCount = 0;
    const resolver =
        new PromiseResolver<{savePath: FilePath, error: RmadErrorCode}>();
    service.saveLog = () => {
      ++saveLogCallCount;
      return resolver.promise;
    };

    await openLogsDialog();

    // Attempt to save the logs.
    await clickButton(saveLogButtonSelector);
    const savePath = 'save/path';
    resolver.resolve({savePath: {path: savePath}, error: RmadErrorCode.kOk});
    await flushTasks();

    assertEquals(1, saveLogCallCount);

    // The save log button should be replaced by the done button.
    assert(component);
    assertFalse(isVisible(strictQuery(
        saveLogButtonSelector, component.shadowRoot, CrButtonElement)));
    assertTrue(isVisible(strictQuery(
        logSaveDoneButtonSelector, component.shadowRoot, CrButtonElement)));
    assertEquals(
        loadTimeData.getStringF('rmaLogsSaveSuccessText', savePath),
        strictQuery(logSavedStatusSelector, component.shadowRoot, HTMLElement)
            .textContent!.trim());

    // Close the logs dialog.
    await clickButton(logSaveDoneButtonSelector);
    await flushTasks();

    // Open the logs dialog and verify we are at the original state with the
    // Save Log button displayed.
    await openLogsDialog();
    assertTrue(isVisible(strictQuery(
        saveLogButtonSelector, component.shadowRoot, CrButtonElement)));
  });

  // Verify the save logs dialog shows error messasges for a failed save and
  // allows for a retry.
  test('SaveLogFails', async () => {
    await initializeShimlessRMAApp();

    assert(service);
    service.triggerExternalDiskObserver(/* detected= */ true, /* delayMs= */ 0);
    await flushTasks();

    let saveLogCallCount = 0;
    const resolver =
        new PromiseResolver<{savePath: FilePath, error: RmadErrorCode}>();
    service.saveLog = () => {
      ++saveLogCallCount;
      return resolver.promise;
    };

    // Attempt to save the logs but it fails.
    await openLogsDialog();
    await clickButton(saveLogButtonSelector);
    resolver.resolve(
        {savePath: {'path': 'save/path'}, error: RmadErrorCode.kCannotSaveLog});
    await flushTasks();

    assertEquals(1, saveLogCallCount);

    // The save log button should be replaced by the done button and the retry
    // button.
    assert(component);
    assertFalse(isVisible(strictQuery(
        saveLogButtonSelector, component.shadowRoot, CrButtonElement)));
    assertTrue(isVisible(strictQuery(
        logRetryButtonSelector, component.shadowRoot, CrButtonElement)));
    assertEquals(
        loadTimeData.getString('rmaLogsSaveFailText'),
        strictQuery(logSavedStatusSelector, component.shadowRoot, HTMLElement)
            .textContent!.trim());

    // Click the retry button and verify that it retries saving the logs.
    await clickButton(logRetryButtonSelector);
    resolver.resolve(
        {savePath: {'path': 'save/path'}, error: RmadErrorCode.kCannotSaveLog});
    await flushTasks();

    assertEquals(2, saveLogCallCount);
  });

  // Verify the correct message is shown for USB not found when saving logs.
  test('SaveLogFailsUsbNotFound', async () => {
    await initializeShimlessRMAApp();

    assert(service);
    service.triggerExternalDiskObserver(/* detected= */ true, /* delayMs= */ 0);
    await flushTasks();

    const resolver =
        new PromiseResolver<{savePath: FilePath, error: RmadErrorCode}>();
    service.saveLog = () => {
      return resolver.promise;
    };

    // Attempt to save the logs but it fails because the USB is not detected.
    await openLogsDialog();
    await clickButton(saveLogButtonSelector);
    resolver.resolve(
        {savePath: {path: 'save/path'}, error: RmadErrorCode.kUsbNotFound});
    await flushTasks();

    // The save log button should be replaced by the done button and the retry
    // button.
    assert(component);
    assertFalse(isVisible(strictQuery(
        saveLogButtonSelector, component.shadowRoot, CrButtonElement)));
    // await waitAfterNextRender(strictQuery(logRetryButtonSelector,
    // component.shadowRoot, CrButtonElement)); await flushTasks();
    assertTrue(isVisible(strictQuery(
        logRetryButtonSelector, component.shadowRoot, CrButtonElement)));
    assertEquals(
        loadTimeData.getString('rmaLogsSaveUsbNotFound'),
        strictQuery(logSavedStatusSelector, component.shadowRoot, HTMLElement)
            .textContent!.trim());
  });

  // Verify the correct message is shown for USB connected or disconnected.
  test('ExternalDiskConnectedShowsUsbActionButtons', async () => {
    await initializeShimlessRMAApp();

    assert(component);
    const logConnectUsbMessageContainer = strictQuery(
        logConnectUsbMessageSelector, component.shadowRoot, HTMLElement);
    const saveLogButtonContainer = strictQuery(
        saveLogContainerSelector, component.shadowRoot, HTMLElement);

    // When an external disk is connected, verify the "save log" message is
    // displayed.
    assert(service);
    service.triggerExternalDiskObserver(/* detected= */ true, /* delayMs= */ 0);
    await flushTasks();
    assertTrue(logConnectUsbMessageContainer.hidden);
    assertFalse(saveLogButtonContainer.hidden);

    // When an eexternal disk isn't connected, verify the "connect a USB"
    // message is displayed.
    service.triggerExternalDiskObserver(
        /* detected= */ false, /* delayMs= */ 0);
    await flushTasks();
    assertFalse(logConnectUsbMessageContainer.hidden);
    assertTrue(saveLogButtonContainer.hidden);
  });

  // Verify the save logs dialog can be closed even after the USB is unplugged.
  test('LogsDialogCloses', async () => {
    await initializeShimlessRMAApp();

    await openLogsDialog();
    assert(component);
    const logsDialog =
        strictQuery(logsDialogSelector, component.shadowRoot, CrDialogElement);
    assertTrue(logsDialog.open);

    await clickButton(closeLogsDialogButtonSelector);
    assertFalse(logsDialog.open);

    // Verify logs dialog can be closed when the USB is unplugged.
    service.triggerExternalDiskObserver(false, 0);
    await openLogsDialog();
    assertTrue(logsDialog.open);

    await clickButton(closeLogsDialogButtonSelector);
    assertFalse(logsDialog.open);
  });

  // Verify the Alt+Shift+L keyboard shortcut opens the logs dialog.
  test('KeyboardShortcutOpensLogsDialog', async () => {
    await initializeShimlessRMAApp();

    // Confirm logs dialog starts closed.
    assert(component);
    const logsDialog =
        strictQuery(logsDialogSelector, component.shadowRoot, CrDialogElement);
    assertFalse(logsDialog.open);

    // Invoke the Shimless logs keyboard shortcut.
    const keydownEventPromise = eventToPromise('keydown', component);
    component.dispatchEvent(new KeyboardEvent(
        'keydown',
        {
          bubbles: true,
          composed: true,
          key: 'L',
          altKey: true,
          shiftKey: true,
        },
        ));

    await keydownEventPromise;
    assertTrue(logsDialog.open);
  });
});