diff --git a/packages/trace-viewer/src/ui/testUtils.ts b/packages/trace-viewer/src/ui/testUtils.ts new file mode 100644 index 0000000000..5fc27b623b --- /dev/null +++ b/packages/trace-viewer/src/ui/testUtils.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type UITestStatus = 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped'; + +export function testStatusIcon(status: UITestStatus): string { + if (status === 'scheduled') + return 'codicon-clock'; + if (status === 'running') + return 'codicon-loading'; + if (status === 'failed') + return 'codicon-error'; + if (status === 'passed') + return 'codicon-check'; + if (status === 'skipped') + return 'codicon-circle-slash'; + return 'codicon-circle-outline'; +} + +export function testStatusText(status: UITestStatus): string { + if (status === 'scheduled') + return 'Pending'; + if (status === 'running') + return 'Running'; + if (status === 'failed') + return 'Failed'; + if (status === 'passed') + return 'Passed'; + if (status === 'skipped') + return 'Skipped'; + return 'Did not run'; +} diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index d1c362a231..498fab951a 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -38,6 +38,8 @@ import { artifactsFolderName } from '@testIsomorphic/folders'; import { msToString, settings, useSetting } from '@web/uiUtils'; import type { ActionTraceEvent } from '@trace/trace'; import { connect } from './wsPort'; +import { testStatusIcon } from './testUtils'; +import type { UITestStatus } from './testUtils'; let updateRootSuite: (config: FullConfig, rootSuite: Suite, loadErrors: TestError[], progress: Progress | undefined) => void = () => {}; let runWatchedTests = (fileNames: string[]) => {}; @@ -74,7 +76,7 @@ export const UIModeView: React.FC<{}> = ({ const [projectFilters, setProjectFilters] = React.useState>(new Map()); const [testModel, setTestModel] = React.useState({ config: undefined, rootSuite: undefined, loadErrors: [] }); const [progress, setProgress] = React.useState(); - const [selectedItem, setSelectedItem] = React.useState<{ testFile?: SourceLocation, testCase?: TestCase }>({}); + const [selectedItem, setSelectedItem] = React.useState<{ treeItem?: TreeItem, testFile?: SourceLocation, testCase?: TestCase }>({}); const [visibleTestIds, setVisibleTestIds] = React.useState>(new Set()); const [isLoading, setIsLoading] = React.useState(false); const [runningState, setRunningState] = React.useState<{ testIds: Set, itemSelectedByUser?: boolean } | undefined>(); @@ -361,7 +363,7 @@ const TestList: React.FC<{ setWatchedTreeIds: (ids: { value: Set }) => void, isLoading?: boolean, setVisibleTestIds: (testIds: Set) => void, - onItemSelected: (item: { testCase?: TestCase, testFile?: SourceLocation }) => void, + onItemSelected: (item: { treeItem?: TreeItem, testCase?: TestCase, testFile?: SourceLocation }) => void, requestedCollapseAllCount: number, }> = ({ statusFilters, projectFilters, filterText, testModel, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, setVisibleTestIds, requestedCollapseAllCount }) => { const [treeState, setTreeState] = React.useState({ expandedItems: new Map() }); @@ -444,7 +446,7 @@ const TestList: React.FC<{ selectedTest = selectedTreeItem.test; else if (selectedTreeItem?.kind === 'case' && selectedTreeItem.tests.length === 1) selectedTest = selectedTreeItem.tests[0]; - onItemSelected({ testCase: selectedTest, testFile }); + onItemSelected({ treeItem: selectedTreeItem, testCase: selectedTest, testFile }); return { selectedTreeItem }; }, [onItemSelected, selectedTreeItemId, testModel, treeItemMap]); @@ -517,19 +519,7 @@ const TestList: React.FC<{ ; }} - icon={treeItem => { - if (treeItem.status === 'scheduled') - return 'codicon-clock'; - if (treeItem.status === 'running') - return 'codicon-loading'; - if (treeItem.status === 'failed') - return 'codicon-error'; - if (treeItem.status === 'passed') - return 'codicon-check'; - if (treeItem.status === 'skipped') - return 'codicon-circle-slash'; - return 'codicon-circle-outline'; - }} + icon={treeItem => testStatusIcon(treeItem.status)} selectedItem={selectedTreeItem} onAccepted={runTreeItem} onSelected={treeItem => { @@ -543,7 +533,7 @@ const TestList: React.FC<{ }; const TraceView: React.FC<{ - item: { testFile?: SourceLocation, testCase?: TestCase }, + item: { treeItem?: TreeItem, testFile?: SourceLocation, testCase?: TestCase }, rootDir?: string, }> = ({ item, rootDir }) => { const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean } | undefined>(); @@ -610,7 +600,8 @@ const TraceView: React.FC<{ initialSelection={initialSelection} onSelectionChanged={onSelectionChanged} fallbackLocation={item.testFile} - isLive={model?.isLive} />; + isLive={model?.isLive} + status={item.treeItem?.status} />; }; let receiver: TeleReporterReceiver | undefined; @@ -795,7 +786,7 @@ type TreeItemBase = { duration: number; parent: TreeItem | undefined; children: TreeItem[]; - status: 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped'; + status: UITestStatus; }; type GroupItem = TreeItemBase & { diff --git a/packages/trace-viewer/src/ui/workbench.css b/packages/trace-viewer/src/ui/workbench.css new file mode 100644 index 0000000000..b3684c2b71 --- /dev/null +++ b/packages/trace-viewer/src/ui/workbench.css @@ -0,0 +1,40 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.workbench-run-status { + height: 30px; + padding: 4px; + flex: none; + display: flex; + flex-direction: row; + align-items: center; + border-bottom: 1px solid var(--vscode-panel-border); +} + +.workbench-run-status.failed { + color: var(--vscode-errorForeground); +} + +.workbench-run-status .codicon { + margin-right: 4px; +} + +.workbench-run-duration { + display: flex; + flex: none; + align-items: center; + color: var(--vscode-editorCodeLens-foreground); +} diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 7366928f95..186a555e33 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -34,8 +34,11 @@ import { AttachmentsTab } from './attachmentsTab'; import type { Boundaries } from '../geometry'; import { InspectorTab } from './inspectorTab'; import { ToolbarButton } from '@web/components/toolbarButton'; -import { useSetting } from '@web/uiUtils'; +import { useSetting, msToString } from '@web/uiUtils'; import type { Entry } from '@trace/har'; +import './workbench.css'; +import { testStatusIcon, testStatusText } from './testUtils'; +import type { UITestStatus } from './testUtils'; export const Workbench: React.FunctionComponent<{ model?: MultiTraceModel, @@ -46,7 +49,8 @@ export const Workbench: React.FunctionComponent<{ initialSelection?: ActionTraceEventInContext, onSelectionChanged?: (action: ActionTraceEventInContext) => void, isLive?: boolean, -}> = ({ model, hideStackFrames, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive }) => { + status?: UITestStatus, +}> = ({ model, hideStackFrames, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status }) => { const [selectedAction, setSelectedAction] = React.useState(undefined); const [highlightedAction, setHighlightedAction] = React.useState(); const [highlightedEntry, setHighlightedEntry] = React.useState(); @@ -185,6 +189,12 @@ export const Workbench: React.FunctionComponent<{ return { boundaries }; }, [model]); + let time: number = 0; + if (model && model.endTime >= 0) + time = model.endTime - model.startTime; + else if (model && model.wallTime) + time = Date.now() - model.wallTime; + return
selectPropertiesTab('console')} - isLive={isLive} - /> + component:
+ {status &&
+ +
{testStatusText(status)}
+
+
{time ? msToString(time) : ''}
+
} + selectPropertiesTab('console')} + isLive={isLive} + /> +
}, { id: 'metadata',