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 { msToString, settings, useSetting } from '@web/uiUtils';
|
||||||
import type { ActionTraceEvent } from '@trace/trace';
|
import type { ActionTraceEvent } from '@trace/trace';
|
||||||
import { connect } from './wsPort';
|
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 updateRootSuite: (config: FullConfig, rootSuite: Suite, loadErrors: TestError[], progress: Progress | undefined) => void = () => {};
|
||||||
let runWatchedTests = (fileNames: string[]) => {};
|
let runWatchedTests = (fileNames: string[]) => {};
|
||||||
@ -74,7 +76,7 @@ export const UIModeView: React.FC<{}> = ({
|
|||||||
const [projectFilters, setProjectFilters] = React.useState<Map<string, boolean>>(new Map());
|
const [projectFilters, setProjectFilters] = React.useState<Map<string, boolean>>(new Map());
|
||||||
const [testModel, setTestModel] = React.useState<TestModel>({ config: undefined, rootSuite: undefined, loadErrors: [] });
|
const [testModel, setTestModel] = React.useState<TestModel>({ config: undefined, rootSuite: undefined, loadErrors: [] });
|
||||||
const [progress, setProgress] = React.useState<Progress & { total: number } | undefined>();
|
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 [visibleTestIds, setVisibleTestIds] = React.useState<Set<string>>(new Set());
|
||||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||||
const [runningState, setRunningState] = React.useState<{ testIds: Set<string>, itemSelectedByUser?: boolean } | undefined>();
|
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,
|
setWatchedTreeIds: (ids: { value: Set<string> }) => void,
|
||||||
isLoading?: boolean,
|
isLoading?: boolean,
|
||||||
setVisibleTestIds: (testIds: Set<string>) => void,
|
setVisibleTestIds: (testIds: Set<string>) => void,
|
||||||
onItemSelected: (item: { testCase?: TestCase, testFile?: SourceLocation }) => void,
|
onItemSelected: (item: { treeItem?: TreeItem, testCase?: TestCase, testFile?: SourceLocation }) => void,
|
||||||
requestedCollapseAllCount: number,
|
requestedCollapseAllCount: number,
|
||||||
}> = ({ statusFilters, projectFilters, filterText, testModel, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, setVisibleTestIds, requestedCollapseAllCount }) => {
|
}> = ({ statusFilters, projectFilters, filterText, testModel, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, setVisibleTestIds, requestedCollapseAllCount }) => {
|
||||||
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
||||||
@ -444,7 +446,7 @@ const TestList: React.FC<{
|
|||||||
selectedTest = selectedTreeItem.test;
|
selectedTest = selectedTreeItem.test;
|
||||||
else if (selectedTreeItem?.kind === 'case' && selectedTreeItem.tests.length === 1)
|
else if (selectedTreeItem?.kind === 'case' && selectedTreeItem.tests.length === 1)
|
||||||
selectedTest = selectedTreeItem.tests[0];
|
selectedTest = selectedTreeItem.tests[0];
|
||||||
onItemSelected({ testCase: selectedTest, testFile });
|
onItemSelected({ treeItem: selectedTreeItem, testCase: selectedTest, testFile });
|
||||||
return { selectedTreeItem };
|
return { selectedTreeItem };
|
||||||
}, [onItemSelected, selectedTreeItemId, testModel, treeItemMap]);
|
}, [onItemSelected, selectedTreeItemId, testModel, treeItemMap]);
|
||||||
|
|
||||||
@ -517,19 +519,7 @@ const TestList: React.FC<{
|
|||||||
</Toolbar>
|
</Toolbar>
|
||||||
</div>;
|
</div>;
|
||||||
}}
|
}}
|
||||||
icon={treeItem => {
|
icon={treeItem => testStatusIcon(treeItem.status)}
|
||||||
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';
|
|
||||||
}}
|
|
||||||
selectedItem={selectedTreeItem}
|
selectedItem={selectedTreeItem}
|
||||||
onAccepted={runTreeItem}
|
onAccepted={runTreeItem}
|
||||||
onSelected={treeItem => {
|
onSelected={treeItem => {
|
||||||
@ -543,7 +533,7 @@ const TestList: React.FC<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TraceView: React.FC<{
|
const TraceView: React.FC<{
|
||||||
item: { testFile?: SourceLocation, testCase?: TestCase },
|
item: { treeItem?: TreeItem, testFile?: SourceLocation, testCase?: TestCase },
|
||||||
rootDir?: string,
|
rootDir?: string,
|
||||||
}> = ({ item, rootDir }) => {
|
}> = ({ item, rootDir }) => {
|
||||||
const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean } | undefined>();
|
const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean } | undefined>();
|
||||||
@ -610,7 +600,8 @@ const TraceView: React.FC<{
|
|||||||
initialSelection={initialSelection}
|
initialSelection={initialSelection}
|
||||||
onSelectionChanged={onSelectionChanged}
|
onSelectionChanged={onSelectionChanged}
|
||||||
fallbackLocation={item.testFile}
|
fallbackLocation={item.testFile}
|
||||||
isLive={model?.isLive} />;
|
isLive={model?.isLive}
|
||||||
|
status={item.treeItem?.status} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
let receiver: TeleReporterReceiver | undefined;
|
let receiver: TeleReporterReceiver | undefined;
|
||||||
@ -795,7 +786,7 @@ type TreeItemBase = {
|
|||||||
duration: number;
|
duration: number;
|
||||||
parent: TreeItem | undefined;
|
parent: TreeItem | undefined;
|
||||||
children: TreeItem[];
|
children: TreeItem[];
|
||||||
status: 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped';
|
status: UITestStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GroupItem = TreeItemBase & {
|
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 type { Boundaries } from '../geometry';
|
||||||
import { InspectorTab } from './inspectorTab';
|
import { InspectorTab } from './inspectorTab';
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
import { useSetting } from '@web/uiUtils';
|
import { useSetting, msToString } from '@web/uiUtils';
|
||||||
import type { Entry } from '@trace/har';
|
import type { Entry } from '@trace/har';
|
||||||
|
import './workbench.css';
|
||||||
|
import { testStatusIcon, testStatusText } from './testUtils';
|
||||||
|
import type { UITestStatus } from './testUtils';
|
||||||
|
|
||||||
export const Workbench: React.FunctionComponent<{
|
export const Workbench: React.FunctionComponent<{
|
||||||
model?: MultiTraceModel,
|
model?: MultiTraceModel,
|
||||||
@ -46,7 +49,8 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
initialSelection?: ActionTraceEventInContext,
|
initialSelection?: ActionTraceEventInContext,
|
||||||
onSelectionChanged?: (action: ActionTraceEventInContext) => void,
|
onSelectionChanged?: (action: ActionTraceEventInContext) => void,
|
||||||
isLive?: boolean,
|
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 [selectedAction, setSelectedAction] = React.useState<ActionTraceEventInContext | undefined>(undefined);
|
||||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
||||||
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
||||||
@ -185,6 +189,12 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
return { boundaries };
|
return { boundaries };
|
||||||
}, [model]);
|
}, [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'>
|
return <div className='vbox workbench'>
|
||||||
<Timeline
|
<Timeline
|
||||||
model={model}
|
model={model}
|
||||||
@ -211,17 +221,25 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
title: 'Actions',
|
title: 'Actions',
|
||||||
component: <ActionList
|
component: <div className='vbox'>
|
||||||
sdkLanguage={sdkLanguage}
|
{status && <div className='workbench-run-status'>
|
||||||
actions={model?.actions || []}
|
<span className={`codicon ${testStatusIcon(status)}`}></span>
|
||||||
selectedAction={model ? selectedAction : undefined}
|
<div>{testStatusText(status)}</div>
|
||||||
selectedTime={selectedTime}
|
<div className='spacer'></div>
|
||||||
setSelectedTime={setSelectedTime}
|
<div className='workbench-run-duration'>{time ? msToString(time) : ''}</div>
|
||||||
onSelected={onActionSelected}
|
</div>}
|
||||||
onHighlighted={setHighlightedAction}
|
<ActionList
|
||||||
revealConsole={() => selectPropertiesTab('console')}
|
sdkLanguage={sdkLanguage}
|
||||||
isLive={isLive}
|
actions={model?.actions || []}
|
||||||
/>
|
selectedAction={model ? selectedAction : undefined}
|
||||||
|
selectedTime={selectedTime}
|
||||||
|
setSelectedTime={setSelectedTime}
|
||||||
|
onSelected={onActionSelected}
|
||||||
|
onHighlighted={setHighlightedAction}
|
||||||
|
revealConsole={() => selectPropertiesTab('console')}
|
||||||
|
isLive={isLive}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'metadata',
|
id: 'metadata',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user