chromium/ash/webui/firmware_update_ui/resources/fake_update_controller.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 {FakeObservables} from 'chrome://resources/ash/common/fake_observables.js';
import {PromiseResolver} from 'chrome://resources/ash/common/promise_resolver.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 {fakeDeviceRequest, fakeFirmwareUpdates, fakeInstallationProgress, fakeInstallationProgressFailure, fakeInstallationProgressWithRequest, fakeInstallationProgressWithRequestAndFailure} from './fake_data.js';
import {DeviceRequest, DeviceRequestObserverRemote, FirmwareUpdate, InstallationProgress, UpdateProgressObserverRemote, UpdateProviderInterface, UpdateState} from './firmware_update.mojom-webui.js';
import {FakeInstallControllerInterface, FakeUpdateProviderInterface} from './firmware_update_types.js';
import {getUpdateProvider, setUseFakeProviders} from './mojo_interface_provider.js';

// Method names.
export const ON_PROGRESS_CHANGED = 'UpdateProgressObserver_onStatusChanged';
export const ON_DEVICE_REQUEST = 'DeviceRequestObserver_onDeviceRequest';

/**
 * @fileoverview
 * Implements a fake version of the UpdateController mojo interface.
 */

export class FakeUpdateController implements FakeInstallControllerInterface {
  private observables = new FakeObservables();
  private completedFirmwareUpdates = new Set<string>();
  private startUpdatePromise: Promise<void>|null = null;
  private deviceId = '';
  private isUpdateInProgress = false;
  private updateIntervalInMs = 1000;
  private updateCompletedPromise: PromiseResolver<void>|null = null;

  constructor() {
    setUseFakeProviders(true);
    this.registerObservables();
  }

  /*
   * Implements InstallControllerInterface.addDeviceRequestObserver.
   */
  addDeviceRequestObserver(remote: DeviceRequestObserverRemote): void {
    this.observables.observe(ON_DEVICE_REQUEST, (request: DeviceRequest) => {
      remote.onDeviceRequest(request);
    });
  }

  /*
   * Implements InstallControllerInterface.addUpdateProgressObserver.
   */
  addUpdateProgressObserver(remote: UpdateProgressObserverRemote): void {
    this.isUpdateInProgress = true;
    this.updateCompletedPromise = new PromiseResolver();
    this.startUpdatePromise = this.observeWithArg(
        ON_PROGRESS_CHANGED, this.deviceId,
        (update: InstallationProgress): void => {
          remote.onStatusChanged(update);
          if (update.state === UpdateState.kSuccess ||
              update.state === UpdateState.kFailed) {
            this.isUpdateInProgress = false;
            this.completedFirmwareUpdates.add(this.deviceId);
            this.updateDeviceList();
            this.observables.stopTriggerOnIntervalWithArg(
                ON_PROGRESS_CHANGED, this.deviceId);
            assert(this.updateCompletedPromise);
            this.updateCompletedPromise.resolve();
          }
        });
  }

  beginUpdate(deviceId: string, path: FilePath): void {
    assert(deviceId);
    assert(path);
    assert(this.startUpdatePromise);
    if (deviceId == '4') {
      this.triggerProgressChangedObserverForInstallationProgress(
          fakeInstallationProgressWithRequest);
    } else if (deviceId == '5') {
      this.triggerProgressChangedObserverForInstallationProgress(
          fakeInstallationProgressWithRequestAndFailure);
    } else {
      this.triggerProgressChangedObserver();
    }
  }

  setDeviceIdForUpdateInProgress(deviceId: string): void {
    this.deviceId = deviceId;
  }

  /**
   * Sets the values that will be observed from observePeripheralUpdates.
   */
  setFakeInstallationProgress(
      deviceId: string, installationProgress: InstallationProgress[]): void {
    this.observables.setObservableDataForArg(
        ON_PROGRESS_CHANGED, deviceId, installationProgress);
  }

  /**
   * Returns the promise for the most recent startUpdate observation.
   */
  getStartUpdatePromiseForTesting(): Promise<void>|null {
    return this.startUpdatePromise;
  }

  /**
   * Causes the progress changed observer to fire with device requests.
   */
  async triggerProgressChangedObserverForInstallationProgress(
      allProgress: InstallationProgress[]): Promise<void> {
    for (const progress of allProgress) {
      this.observables.triggerWithArg(ON_PROGRESS_CHANGED, this.deviceId);
      if (progress.state === UpdateState.kWaitingForUser) {
        this.observables.trigger(ON_DEVICE_REQUEST);
        // Wait a little longer than usual to give developers/testers time to
        // view the request screen.
        await this.wait(this.updateIntervalInMs * 2);
      }
      await this.wait(this.updateIntervalInMs);
    }
  }

  /**
   * Returns a promise that waits for the specified amount of time before
   * resolving.
   */
  private async wait(timeoutMs: number): Promise<void> {
    return new Promise(resolve => setTimeout(() => resolve(), timeoutMs));
  }

  /**
   * Causes the progress changed observer to fire.
   */
  triggerProgressChangedObserver(): void {
    this.observables.startTriggerOnIntervalWithArg(
        ON_PROGRESS_CHANGED, this.deviceId, this.updateIntervalInMs);
  }

  registerObservables(): void {
    this.observables.registerObservableWithArg(ON_PROGRESS_CHANGED);
    this.observables.register(ON_DEVICE_REQUEST);
    // Set up fake installation progress data for each fake firmware update.
    fakeFirmwareUpdates.flat().forEach(({deviceId}) => {
      // Use the third fake firmware update to mock a failed installation.
      if (deviceId === '3') {
        this.setFakeInstallationProgress(
            deviceId, fakeInstallationProgressFailure);
        // Use the fourth fake firmware update to mock a successful installation
        // that includes a device request.
      } else if (deviceId === '4') {
        this.observables.setObservableData(
            ON_DEVICE_REQUEST, [fakeDeviceRequest]);
        this.setFakeInstallationProgress(
            deviceId, fakeInstallationProgressWithRequest);
        // Use the fifth fake firmware update to mock a failed installation
        // that includes a device request.
      } else if (deviceId === '5') {
        this.observables.setObservableData(
            ON_DEVICE_REQUEST, [fakeDeviceRequest]);
        this.setFakeInstallationProgress(
            deviceId, fakeInstallationProgressWithRequestAndFailure);
      } else {
        this.setFakeInstallationProgress(deviceId, fakeInstallationProgress);
      }
    });
  }

  /**
   * Disables all observers and resets controller to its initial state.
   */
  reset(): void {
    this.stopTriggerIntervals();
    this.observables = new FakeObservables();
    this.completedFirmwareUpdates.clear();
    this.deviceId = '';
    this.isUpdateInProgress = false;
    this.registerObservables();
  }

  /**
   * Stop automatically triggering observables.
   */
  stopTriggerIntervals(): void {
    this.observables.stopAllTriggerIntervals();
  }

  /*
   * Sets up an observer for a methodName that takes an additional arg.
   */
  private observeWithArg<T>(
      methodName: string, arg: string,
      callback: (arg: T) => void): Promise<void> {
    return new Promise((resolve) => {
      this.observables.observeWithArg(methodName, arg, callback);
      resolve();
    });
  }

  /**
   * Returns true when the promise stored in |startUpdatePromise| has not
   * resolved.
   */
  getIsUpdateInProgressForTesting(): boolean {
    return this.isUpdateInProgress;
  }

  setUpdateIntervalInMs(intervalMs: number): void {
    this.updateIntervalInMs = intervalMs;
  }

  /**
   * Remove the completed firmware update and trigger the list observer.
   */
  private updateDeviceList(): void {
    const updatedFakeFirmwareUpdates = fakeFirmwareUpdates.flat().filter(
        u => !this.completedFirmwareUpdates.has(u.deviceId));

    const provider = getUpdateProvider() as UpdateProviderInterface &
        FakeUpdateProviderInterface;
    provider.setFakeFirmwareUpdates(
        [updatedFakeFirmwareUpdates] as FirmwareUpdate[][]);
    provider.triggerDeviceAddedObserver();
  }

  /**
   * Returns the pending run routine promise.
   */
  getUpdateCompletedPromiseForTesting(): Promise<void> {
    assert(this.updateCompletedPromise != null);
    return this.updateCompletedPromise.promise;
  }

  /**
   * Returns a list of |deviceId|s representing completed firmware updates.
   */
  getCompletedFirmwareUpdatesForTesting(): Set<string> {
    return this.completedFirmwareUpdates;
  }
}