mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(ui): show test status in trace view (#27785)
This commit is contained in:
parent
7de0ccd36e
commit
0bb9f7cdf7
45
packages/trace-viewer/src/ui/testUtils.ts
Normal file
45
packages/trace-viewer/src/ui/testUtils.ts
Normal file
@ -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';
|
||||
}
|
@ -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<Map<string, boolean>>(new Map());
|
||||
const [testModel, setTestModel] = React.useState<TestModel>({ config: undefined, rootSuite: undefined, loadErrors: [] });
|
||||
const [progress, setProgress] = React.useState<Progress & { total: number } | undefined>();
|
||||
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<Set<string>>(new Set());
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [runningState, setRunningState] = React.useState<{ testIds: Set<string>, itemSelectedByUser?: boolean } | undefined>();
|
||||
@ -361,7 +363,7 @@ const TestList: React.FC<{
|
||||
setWatchedTreeIds: (ids: { value: Set<string> }) => void,
|
||||
isLoading?: boolean,
|
||||
setVisibleTestIds: (testIds: Set<string>) => 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<TreeState>({ 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<{
|
||||
</Toolbar>
|
||||
</div>;
|
||||
}}
|
||||
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 & {
|
||||
|
40
packages/trace-viewer/src/ui/workbench.css
Normal file
40
packages/trace-viewer/src/ui/workbench.css
Normal file
@ -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);
|
||||
}
|
@ -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<ActionTraceEventInContext | undefined>(undefined);
|
||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
||||
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
||||
@ -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 <div className='vbox workbench'>
|
||||
<Timeline
|
||||
model={model}
|
||||
@ -211,17 +221,25 @@ export const Workbench: React.FunctionComponent<{
|
||||
{
|
||||
id: 'actions',
|
||||
title: 'Actions',
|
||||
component: <ActionList
|
||||
sdkLanguage={sdkLanguage}
|
||||
actions={model?.actions || []}
|
||||
selectedAction={model ? selectedAction : undefined}
|
||||
selectedTime={selectedTime}
|
||||
setSelectedTime={setSelectedTime}
|
||||
onSelected={onActionSelected}
|
||||
onHighlighted={setHighlightedAction}
|
||||
revealConsole={() => selectPropertiesTab('console')}
|
||||
isLive={isLive}
|
||||
/>
|
||||
component: <div className='vbox'>
|
||||
{status && <div className='workbench-run-status'>
|
||||
<span className={`codicon ${testStatusIcon(status)}`}></span>
|
||||
<div>{testStatusText(status)}</div>
|
||||
<div className='spacer'></div>
|
||||
<div className='workbench-run-duration'>{time ? msToString(time) : ''}</div>
|
||||
</div>}
|
||||
<ActionList
|
||||
sdkLanguage={sdkLanguage}
|
||||
actions={model?.actions || []}
|
||||
selectedAction={model ? selectedAction : undefined}
|
||||
selectedTime={selectedTime}
|
||||
setSelectedTime={setSelectedTime}
|
||||
onSelected={onActionSelected}
|
||||
onHighlighted={setHighlightedAction}
|
||||
revealConsole={() => selectPropertiesTab('console')}
|
||||
isLive={isLive}
|
||||
/>
|
||||
</div>
|
||||
},
|
||||
{
|
||||
id: 'metadata',
|
||||
|
Loading…
x
Reference in New Issue
Block a user