| // 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(): string { |
| return 'routine-section'; |
| } |
| |
| 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; |
| private announcedText: string; |
| private routineStartTimeMs: number; |
| private executionStatus: ExecutionProgress; |
| private currentTestName: string; |
| 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. |
| */ |
| private 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 { |
| 'routine-section': RoutineSectionElement; |
| } |
| } |
| |
| customElements.define(RoutineSectionElement.is, RoutineSectionElement); |