// 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 'chrome://resources/polymer/v3_0/iron-collapse/iron-collapse.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import 'chrome://resources/polymer/v3_0/paper-tooltip/paper-tooltip.js';
import './diagnostics_card.js';
import './diagnostics_shared.css.js';
import './icons.html.js';
import './routine_result_list.js';
import './text_badge.js';
import './strings.m.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {assert, assertNotReached} from 'chrome://resources/js/assert.js';
import {IronA11yAnnouncer} from 'chrome://resources/polymer/v3_0/iron-a11y-announcer/iron-a11y-announcer.js';
import {IronCollapseElement} from 'chrome://resources/polymer/v3_0/iron-collapse/iron-collapse.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 {getSystemRoutineController} from './mojo_interface_provider.js';
import {RoutineGroup} from './routine_group.js';
import {ExecutionProgress, ResultStatusItem, RoutineListExecutor, TestSuiteStatus} from './routine_list_executor.js';
import {getRoutineType, getSimpleResult} from './routine_result_entry.js';
import {isRoutineGroupArray, isRoutineTypeArray, RoutineResultListElement} from './routine_result_list.js';
import {getTemplate} from './routine_section.html.js';
import {PowerRoutineResult, RoutineType, StandardRoutineResult, SystemRoutineControllerInterface} from './system_routine_controller.mojom-webui.js';
import {BadgeType} from './text_badge.js';
export type Routines = RoutineGroup[]|RoutineType[];
export interface RoutineSectionElement {
$: {
collapse: IronCollapseElement,
};
}
/**
* @fileoverview
* 'routine-section' has a button to run tests and displays their results. The
* parent element eg. a CpuCard binds to the routines property to indicate
* which routines this instance will run.
*/
const RoutineSectionElementBase = I18nMixin(PolymerElement);
export class RoutineSectionElement extends RoutineSectionElementBase {
static get is(): 'routine-section' {
return 'routine-section' as const;
}
static get template(): HTMLTemplateElement {
return getTemplate();
}
static get properties(): PolymerElementProperties {
return {
/**
* Added to support testing of announce behavior.
*/
announcedText: {
type: String,
value: '',
},
routines: {
type: Array,
value: () => [],
},
/**
* Total time in minutes of estimate runtime based on routines array.
*/
routineRuntime: {
type: Number,
value: 0,
},
/**
* Timestamp of when routine test started execution in milliseconds.
*/
routineStartTimeMs: {
type: Number,
value: -1,
},
/**
* Overall ExecutionProgress of the routine.
*/
executionStatus: {
type: Number,
value: ExecutionProgress.NOT_STARTED,
},
/**
* Name of currently running test
*/
currentTestName: {
type: String,
value: '',
},
testSuiteStatus: {
type: Number,
value: TestSuiteStatus.NOT_RUNNING,
notify: true,
},
isPowerRoutine: {
type: Boolean,
value: false,
},
powerRoutineResult: {
type: Object,
value: null,
},
runTestsButtonText: {
type: String,
value: '',
},
additionalMessage: {
type: String,
value: '',
},
learnMoreLinkSection: {
type: String,
value: '',
},
badgeType: {
type: String,
value: BadgeType.RUNNING,
},
badgeText: {
type: String,
value: '',
},
statusText: {
type: String,
value: '',
},
isLoggedIn: {
type: Boolean,
value: loadTimeData.getBoolean('isLoggedIn'),
},
bannerMessage: {
type: Boolean,
value: '',
},
isActive: {
type: Boolean,
},
/**
* Used to reset run button text to its initial state
* when a navigation page change event occurs.
*/
initialButtonText: {
type: String,
value: '',
computed: 'getInitialButtonText(runTestsButtonText)',
},
hideRoutineStatus: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
opened: {
type: Boolean,
value: false,
},
hideVerticalLines: {
type: Boolean,
value: false,
},
usingRoutineGroups: {
type: Boolean,
value: false,
computed: 'getUsingRoutineGroupsVal(routines.*)',
},
};
}
routines: Routines;
routineRuntime: number;
runTestsButtonText: string;
additionalMessage: string;
learnMoreLinkSection: string;
testSuiteStatus: TestSuiteStatus;
isPowerRoutine: boolean;
isActive: boolean;
hideRoutineStatus: boolean;
opened: boolean;
hideVerticalLines: boolean;
usingRoutineGroups: boolean;
ignoreRoutineStatusUpdates: boolean;
announcedText: string;
currentTestName: string;
private routineStartTimeMs: number;
private executionStatus: ExecutionProgress;
private powerRoutineResult: PowerRoutineResult;
private badgeType: BadgeType;
private badgeText: string;
private statusText: string;
private isLoggedIn: boolean;
private bannerMessage: string;
private initialButtonText: string;
private executor: RoutineListExecutor|null = null;
private failedTest: RoutineType|null = null;
private hasTestFailure: boolean = false;
private systemRoutineController: SystemRoutineControllerInterface|null = null;
static get observers(): string[] {
return [
'routineStatusChanged(executionStatus, currentTestName,' +
'additionalMessage)',
'onActivePageChanged(isActive)',
];
}
override connectedCallback(): void {
super.connectedCallback();
IronA11yAnnouncer.requestAvailability();
}
private getInitialButtonText(buttonText: string): string {
return this.initialButtonText || buttonText;
}
private getUsingRoutineGroupsVal(): boolean {
if (this.routines.length === 0) {
return false;
}
return this.routines[0] instanceof RoutineGroup;
}
private getResultListElem(): RoutineResultListElement {
const routineResultList: RoutineResultListElement|null =
this.shadowRoot!.querySelector('routine-result-list');
assert(routineResultList);
return routineResultList;
}
private async getSupportedRoutines(): Promise<RoutineType[]> {
const supported =
await this.systemRoutineController?.getSupportedRoutines();
assert(supported);
assert(isRoutineTypeArray(this.routines));
const filteredRoutineTypes = this.routines.filter(
(routine: RoutineType) => supported.routines.includes(routine));
return filteredRoutineTypes;
}
private async getSupportedRoutineGroups(): Promise<RoutineGroup[]> {
const supported =
await this.systemRoutineController?.getSupportedRoutines();
assert(supported);
const filteredRoutineGroups: RoutineGroup[] = [];
assert(isRoutineGroupArray(this.routines));
for (const routineGroup of this.routines) {
routineGroup.routines = routineGroup.routines.filter(
routine => supported.routines.includes(routine));
if (routineGroup.routines.length > 0) {
filteredRoutineGroups.push(routineGroup.clone());
}
}
return filteredRoutineGroups;
}
async runTests(): Promise<void> {
// Do not attempt to run tests when no routines available to run.
if (this.routines.length === 0) {
return;
}
this.testSuiteStatus = TestSuiteStatus.RUNNING;
this.failedTest = null;
this.systemRoutineController = getSystemRoutineController();
const resultListElem = this.getResultListElem();
const routines = this.usingRoutineGroups ?
await this.getSupportedRoutineGroups() :
await this.getSupportedRoutines();
resultListElem.initializeTestRun(routines);
// Expand result list by default.
if (!this.shouldHideReportList()) {
this.$.collapse.show();
}
if (this.bannerMessage) {
this.showCautionBanner();
}
this.routineStartTimeMs = performance.now();
// Set initial status badge text.
this.setRunningStatusBadgeText();
const remainingTimeUpdaterId =
setInterval(() => this.setRunningStatusBadgeText(), 1000);
assert(this.systemRoutineController);
const executor = new RoutineListExecutor(this.systemRoutineController);
this.executor = executor;
if (!this.usingRoutineGroups) {
assert(isRoutineTypeArray(routines));
const status = await executor.runRoutines(
routines,
(routineStatus) =>
this.handleRunningRoutineStatus(routineStatus, resultListElem));
this.handleRoutinesCompletedStatus(status);
clearInterval(remainingTimeUpdaterId);
return;
}
assert(isRoutineGroupArray(routines));
for (let i = 0; i < routines.length; i++) {
const routineGroup = routines[i];
const status = await executor.runRoutines(
routineGroup.routines,
(routineStatus) =>
this.handleRunningRoutineStatus(routineStatus, resultListElem));
const isLastRoutineGroup = i === routines.length - 1;
if (isLastRoutineGroup) {
this.handleRoutinesCompletedStatus(status);
clearInterval(remainingTimeUpdaterId);
}
}
}
private announceRoutinesComplete(): void {
this.announcedText = loadTimeData.getString('testOnRoutinesCompletedText');
this.dispatchEvent(new CustomEvent('iron-announce', {
bubbles: true,
composed: true,
detail: {
text: this.announcedText,
},
}));
}
private handleRoutinesCompletedStatus(status: ExecutionProgress): void {
this.executionStatus = status;
this.testSuiteStatus = status === ExecutionProgress.CANCELLED ?
TestSuiteStatus.NOT_RUNNING :
TestSuiteStatus.COMPLETED;
this.routineStartTimeMs = -1;
this.runTestsButtonText = loadTimeData.getString('runAgainButtonText');
this.getResultListElem().resetIgnoreStatusUpdatesFlag();
this.cleanUp();
if (status === ExecutionProgress.CANCELLED) {
this.badgeText = loadTimeData.getString('testStoppedBadgeText');
} else {
this.badgeText = this.failedTest ?
loadTimeData.getString('testFailedBadgeText') :
loadTimeData.getString('testSucceededBadgeText');
this.announceRoutinesComplete();
}
}
private handleRunningRoutineStatus(
status: ResultStatusItem,
resultListElem: RoutineResultListElement): void {
if (this.ignoreRoutineStatusUpdates) {
return;
}
if (status.result && status.result.powerResult) {
this.powerRoutineResult = status.result.powerResult;
}
if (status.result &&
getSimpleResult(status.result) === StandardRoutineResult.kTestFailed &&
!this.failedTest) {
this.failedTest = status.routine;
}
// Execution progress is checked here to avoid overwriting
// the test name shown in the status text.
if (status.progress !== ExecutionProgress.CANCELLED) {
this.currentTestName = getRoutineType(status.routine);
}
this.executionStatus = status.progress;
resultListElem.onStatusUpdate.call(resultListElem, status);
}
private cleanUp(): void {
if (this.executor) {
this.executor.close();
this.executor = null;
}
if (this.bannerMessage) {
this.dismissCautionBanner();
}
this.systemRoutineController = null;
}
stopTests(): void {
if (this.executor) {
this.executor.cancel();
}
}
private onToggleReportClicked(): void {
// Toggle report list visibility
this.$.collapse.toggle();
}
protected onLearnMoreClicked(): void {
const baseSupportUrl =
'https://support.google.com/chromebook?p=diagnostics_';
assert(this.learnMoreLinkSection);
window.open(baseSupportUrl + this.learnMoreLinkSection);
}
protected isResultButtonHidden(): boolean {
return this.shouldHideReportList() ||
this.executionStatus === ExecutionProgress.NOT_STARTED;
}
protected isLearnMoreHidden(): boolean {
return !this.shouldHideReportList() || !this.isLoggedIn ||
this.executionStatus !== ExecutionProgress.COMPLETED;
}
protected isStatusHidden(): boolean {
return this.executionStatus === ExecutionProgress.NOT_STARTED;
}
/**
* @param opened Whether the section is expanded or not.
*/
protected getReportToggleButtonText(opened: boolean): string {
return loadTimeData.getString(opened ? 'hideReportText' : 'seeReportText');
}
/**
* Sets status texts for remaining runtime while the routine runs.
*/
setRunningStatusBadgeText(): void {
// Routines that are longer than 5 minutes are considered large
const largeRoutine = this.routineRuntime >= 5;
// Calculate time elapsed since the start of routine in minutes.
const minsElapsed =
(performance.now() - this.routineStartTimeMs) / 1000 / 60;
let timeRemainingInMin = Math.ceil(this.routineRuntime - minsElapsed);
if (largeRoutine && timeRemainingInMin <= 0) {
this.statusText = loadTimeData.getString('routineRemainingMinFinalLarge');
return;
}
// For large routines, round up to 5 minutes increments.
if (largeRoutine && timeRemainingInMin % 5 !== 0) {
timeRemainingInMin += (5 - timeRemainingInMin % 5);
}
this.badgeText = timeRemainingInMin <= 1 ?
loadTimeData.getString('routineRemainingMinFinal') :
loadTimeData.getStringF('routineRemainingMin', timeRemainingInMin);
}
protected routineStatusChanged(): void {
switch (this.executionStatus) {
case ExecutionProgress.NOT_STARTED:
// Do nothing since status is hidden when tests have not been started.
return;
case ExecutionProgress.RUNNING:
this.setBadgeAndStatusText(
BadgeType.RUNNING,
loadTimeData.getStringF(
'routineNameText', this.currentTestName.toLowerCase()));
return;
case ExecutionProgress.CANCELLED:
this.setBadgeAndStatusText(
BadgeType.STOPPED,
loadTimeData.getStringF('testCancelledText', this.currentTestName));
return;
case ExecutionProgress.COMPLETED:
const isPowerRoutine = this.isPowerRoutine || this.powerRoutineResult;
if (this.failedTest) {
this.setBadgeAndStatusText(
BadgeType.ERROR, loadTimeData.getString('testFailure'));
} else {
this.setBadgeAndStatusText(
BadgeType.SUCCESS,
isPowerRoutine ? this.getPowerRoutineString() :
loadTimeData.getString('testSuccess'));
}
return;
}
assertNotReached();
}
private getPowerRoutineString(): string {
assert(!this.usingRoutineGroups);
const stringId =
(this.routines as RoutineType[]).includes(RoutineType.kBatteryCharge) ?
'chargeTestResultText' :
'dischargeTestResultText';
const percentText = loadTimeData.getStringF(
'percentageLabel',
(this.powerRoutineResult?.percentChange || 0).toFixed(2));
return loadTimeData.getStringF(
stringId, percentText,
this.powerRoutineResult?.timeElapsedSeconds || 0);
}
private setBadgeAndStatusText(badgeType: BadgeType, statusText: string):
void {
this.setProperties({
badgeType: badgeType,
statusText: statusText,
});
}
protected isTestRunning(): boolean {
return this.testSuiteStatus === TestSuiteStatus.RUNNING;
}
protected isRunTestsButtonHidden(): boolean {
return this.isTestRunning() &&
this.executionStatus === ExecutionProgress.RUNNING;
}
protected isStopTestsButtonHidden(): boolean {
return this.executionStatus !== ExecutionProgress.RUNNING;
}
protected isRunTestsButtonDisabled(): boolean {
return this.isTestRunning() || this.additionalMessage != '';
}
protected shouldHideReportList(): boolean {
return this.routines.length < 2;
}
protected isAdditionalMessageHidden(): boolean {
return this.additionalMessage == '';
}
private showCautionBanner(): void {
this.dispatchEvent(new CustomEvent('show-caution-banner', {
bubbles: true,
composed: true,
detail: {message: this.bannerMessage},
}));
}
private dismissCautionBanner(): void {
this.dispatchEvent(new CustomEvent(
'dismiss-caution-banner', {bubbles: true, composed: true}));
}
private resetRoutineState(): void {
this.setBadgeAndStatusText(BadgeType.QUEUED, '');
this.badgeText = '';
this.runTestsButtonText = this.initialButtonText;
this.hasTestFailure = false;
this.currentTestName = '';
this.executionStatus = ExecutionProgress.NOT_STARTED;
this.$.collapse.hide();
this.ignoreRoutineStatusUpdates = false;
}
/**
* If the page is active, check if we should run the routines
* automatically, otherwise stop any running tests and reset to
* the initial routine state.
*/
private onActivePageChanged(): void {
if (!this.isActive) {
this.stopTests();
this.resetRoutineState();
return;
}
}
protected isLearnMoreButtonHidden(): boolean {
return !this.isLoggedIn || this.hideRoutineStatus;
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.cleanUp();
}
protected hideRoutineSection(): boolean {
return this.routines.length === 0;
}
}
declare global {
interface HTMLElementTagNameMap {
[RoutineSectionElement.is]: RoutineSectionElement;
}
}
customElements.define(RoutineSectionElement.is, RoutineSectionElement);