chromium/ash/webui/diagnostics_ui/resources/routine_result_list.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 './diagnostics_card.js';
import './diagnostics_shared.css.js';
import './routine_result_entry.js';

import {assert} from 'chrome://resources/js/assert.js';
import {PolymerElementProperties} from 'chrome://resources/polymer/v3_0/polymer/interfaces.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {RoutineGroup} from './routine_group.js';
import {ExecutionProgress, ResultStatusItem} from './routine_list_executor.js';
import {getTemplate} from './routine_result_list.html.js';
import {RoutineType} from './system_routine_controller.mojom-webui.js';

export const isRoutineGroupArray =
    (arr: RoutineGroup[]|RoutineType[]): arr is RoutineGroup[] =>
        (arr as RoutineGroup[])[0].groupName !== undefined;
export const isRoutineTypeArray =
    (arr: RoutineGroup[]|RoutineType[]): arr is RoutineType[] =>
        Object.values(RoutineType).includes(arr[0] as RoutineType);

type ResultsType = RoutineGroup[]|ResultStatusItem[];

/**
 * @fileoverview
 * 'routine-result-list' shows a list of routine result entries.
 */

export class RoutineResultListElement extends PolymerElement {
  static get is(): 'routine-result-list' {
    return 'routine-result-list' as const;
  }

  static get template(): HTMLTemplateElement {
    return getTemplate();
  }

  static get properties(): PolymerElementProperties {
    return {
      results: {
        type: Array,
        value: () => [],
      },

      hidden: {
        type: Boolean,
        value: false,
      },

      hideVerticalLines: {
        type: Boolean,
        value: false,
      },

      usingRoutineGroups: {
        type: Boolean,
        value: false,
      },

      /**
       * Only used with routine groups.
       */
      ignoreRoutineStatusUpdates: {
        type: Boolean,
        value: false,
      },

    };
  }

  override hidden: boolean;
  hideVerticalLines: boolean;
  usingRoutineGroups: boolean;
  ignoreRoutineStatusUpdates: boolean;
  private results: ResultsType;

  /**
   * Resets the list and creates a new list with all routines in the unstarted
   * state. Called by the parent RoutineResultSection when the user starts
   * a test run.
   */
  initializeTestRun(routines: RoutineGroup[]|RoutineType[]): void {
    this.clearRoutines();
    if (routines.length === 0) {
      return;
    }
    if (this.usingRoutineGroups && isRoutineGroupArray(routines)) {
      this.set('results', routines);
    } else {
      assert(isRoutineTypeArray(routines));
      this.addRoutines(routines);
    }
  }

  /**
   * Removes all the routines from the list.
   */
  clearRoutines(): void {
    this.splice('results', 0, this.results.length);
  }

  /**
   * Creates a list of unstarted routines.
   */
  private addRoutines(routines: RoutineType[]): void {
    for (const routine of routines) {
      this.push('results', new ResultStatusItem(routine));
    }
  }

  /**
   * Updates the routine's status in the results list.
   */
  private updateRoutineStatus(
      index: number, status: RoutineGroup|ResultStatusItem): void {
    assert(index < this.results.length);
    this.splice('results', index, 1, status);
  }

  /**
   * Receives the callback from RoutineListExecutor whenever the status of a
   * routine changed.
   */
  onStatusUpdate(status: ResultStatusItem): void {
    if (this.ignoreRoutineStatusUpdates) {
      return;
    }
    assert(this.results.length > 0);
    this.results.forEach(
        (result: RoutineGroup|ResultStatusItem, idx: number) => {
          if (result instanceof RoutineGroup &&
              result.routines.includes(status.routine)) {
            result.setStatus(status);
            const shouldUpdateRoutineUI = result.hasBlockingFailure();
            this.hideVerticalLines = shouldUpdateRoutineUI;
            this.updateRoutineStatus(idx, result.clone());
            // Whether we should skip the remaining routines (true when a
            // blocking routine fails) or not.
            if (shouldUpdateRoutineUI) {
              this.ignoreRoutineStatusUpdates = true;
              this.updateRoutineUiAfterFailure();
            }
            return;
          }
          if (result instanceof ResultStatusItem) {
            if (status.routine === result.routine) {
              this.updateRoutineStatus(idx, status);
              return;
            }
          }
        });
  }

  protected shouldHideVerticalLines({value}: {
    value: RoutineGroup|ResultStatusItem,
  }): boolean {
    return this.hideVerticalLines ||
        value === this.results[this.results.length - 1];
  }

  /**
   * When a test in a routine group fails, we stop sending status updates to the
   * UI and display 'SKIPPED' for the remaining routine groups.
   */
  updateRoutineUiAfterFailure(): void {
    assert(this.usingRoutineGroups);
    this.results.forEach(
        (routineGroup: RoutineGroup|ResultStatusItem, i: number) => {
          assert(routineGroup instanceof RoutineGroup);
          if (routineGroup.progress === ExecutionProgress.NOT_STARTED) {
            routineGroup.progress = ExecutionProgress.SKIPPED;
            this.updateRoutineStatus(i, routineGroup.clone());
          }
        });
  }

  /**
   * Called from 'routine-section' after all routines have finished running.
   */
  resetIgnoreStatusUpdatesFlag(): void {
    this.ignoreRoutineStatusUpdates = false;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [RoutineResultListElement.is]: RoutineResultListElement;
  }
}

customElements.define(RoutineResultListElement.is, RoutineResultListElement);