2023-03-01 15:27:23 -08:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import '@web/third_party/vscode/codicon.css';
|
2023-03-06 12:25:00 -08:00
|
|
|
import { Workbench } from './workbench';
|
2023-03-01 15:27:23 -08:00
|
|
|
import '@web/common.css';
|
|
|
|
import React from 'react';
|
2023-03-08 17:33:27 -08:00
|
|
|
import { TreeView } from '@web/components/treeView';
|
|
|
|
import type { TreeState } from '@web/components/treeView';
|
2023-03-01 15:27:23 -08:00
|
|
|
import { TeleReporterReceiver } from '../../../playwright-test/src/isomorphic/teleReceiver';
|
2023-03-10 12:41:00 -08:00
|
|
|
import type { TeleTestCase } from '../../../playwright-test/src/isomorphic/teleReceiver';
|
2023-03-07 17:20:41 -08:00
|
|
|
import type { FullConfig, Suite, TestCase, TestResult, TestStep, Location } from '../../../playwright-test/types/testReporter';
|
2023-03-01 15:27:23 -08:00
|
|
|
import { SplitView } from '@web/components/splitView';
|
2023-03-06 12:25:00 -08:00
|
|
|
import { MultiTraceModel } from './modelUtil';
|
2023-03-01 15:27:23 -08:00
|
|
|
import './watchMode.css';
|
2023-03-02 13:45:15 -08:00
|
|
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
2023-03-04 15:05:41 -08:00
|
|
|
import { Toolbar } from '@web/components/toolbar';
|
2023-03-05 13:46:21 -08:00
|
|
|
import { toggleTheme } from '@web/theme';
|
2023-03-06 12:25:00 -08:00
|
|
|
import type { ContextEntry } from '../entries';
|
2023-03-06 22:35:57 -08:00
|
|
|
import type * as trace from '@trace/trace';
|
2023-03-07 14:24:50 -08:00
|
|
|
import type { XtermDataSource } from '@web/components/xtermWrapper';
|
|
|
|
import { XtermWrapper } from '@web/components/xtermWrapper';
|
2023-03-10 12:41:00 -08:00
|
|
|
import { Expandable } from '@web/components/expandable';
|
2023-03-01 15:27:23 -08:00
|
|
|
|
2023-03-05 13:46:21 -08:00
|
|
|
let updateRootSuite: (rootSuite: Suite, progress: Progress) => void = () => {};
|
|
|
|
let updateStepsProgress: () => void = () => {};
|
2023-03-04 15:05:41 -08:00
|
|
|
let runWatchedTests = () => {};
|
2023-03-09 20:02:42 -08:00
|
|
|
let xtermSize = { cols: 80, rows: 24 };
|
2023-03-01 15:27:23 -08:00
|
|
|
|
2023-03-07 14:24:50 -08:00
|
|
|
const xtermDataSource: XtermDataSource = {
|
|
|
|
pending: [],
|
|
|
|
clear: () => {},
|
|
|
|
write: data => xtermDataSource.pending.push(data),
|
2023-03-09 20:02:42 -08:00
|
|
|
resize: (cols: number, rows: number) => {
|
|
|
|
xtermSize = { cols, rows };
|
|
|
|
sendMessageNoReply('resizeTerminal', { cols, rows });
|
|
|
|
},
|
2023-03-07 14:24:50 -08:00
|
|
|
};
|
|
|
|
|
2023-03-01 15:27:23 -08:00
|
|
|
export const WatchModeView: React.FC<{}> = ({
|
|
|
|
}) => {
|
2023-03-07 17:20:41 -08:00
|
|
|
const [projects, setProjects] = React.useState<Map<string, boolean>>(new Map());
|
2023-03-04 19:39:55 -08:00
|
|
|
const [rootSuite, setRootSuite] = React.useState<{ value: Suite | undefined }>({ value: undefined });
|
2023-03-07 12:43:16 -08:00
|
|
|
const [isRunningTest, setIsRunningTest] = React.useState<boolean>(false);
|
2023-03-09 21:45:57 -08:00
|
|
|
const [progress, setProgress] = React.useState<Progress>({ total: 0, passed: 0, failed: 0, skipped: 0 });
|
2023-03-08 17:33:27 -08:00
|
|
|
const [selectedTest, setSelectedTest] = React.useState<TestCase | undefined>(undefined);
|
2023-03-07 12:43:16 -08:00
|
|
|
const [settingsVisible, setSettingsVisible] = React.useState<boolean>(false);
|
2023-03-07 15:07:52 -08:00
|
|
|
const [isWatchingFiles, setIsWatchingFiles] = React.useState<boolean>(true);
|
2023-03-09 21:45:57 -08:00
|
|
|
const [visibleTestIds, setVisibleTestIds] = React.useState<string[]>([]);
|
|
|
|
const [filterText, setFilterText] = React.useState<string>('');
|
2023-03-10 12:41:00 -08:00
|
|
|
const [filterExpanded, setFilterExpanded] = React.useState<boolean>(false);
|
2023-03-11 11:43:33 -08:00
|
|
|
const [isShowingOutput, setIsShowingOutput] = React.useState<boolean>(false);
|
2023-03-09 21:45:57 -08:00
|
|
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
inputRef.current?.focus();
|
|
|
|
refreshRootSuite(true);
|
|
|
|
}, []);
|
2023-03-07 12:43:16 -08:00
|
|
|
|
2023-03-09 21:45:57 -08:00
|
|
|
updateRootSuite = (rootSuite: Suite, newProgress: Progress) => {
|
2023-03-07 17:20:41 -08:00
|
|
|
for (const projectName of projects.keys()) {
|
|
|
|
if (!rootSuite.suites.find(s => s.title === projectName))
|
|
|
|
projects.delete(projectName);
|
|
|
|
}
|
|
|
|
for (const projectSuite of rootSuite.suites) {
|
|
|
|
if (!projects.has(projectSuite.title))
|
|
|
|
projects.set(projectSuite.title, false);
|
|
|
|
}
|
|
|
|
if (![...projects.values()].includes(true))
|
|
|
|
projects.set(projects.entries().next().value[0], true);
|
|
|
|
|
|
|
|
setRootSuite({ value: rootSuite });
|
|
|
|
setProjects(new Map(projects));
|
2023-03-09 21:45:57 -08:00
|
|
|
setProgress(newProgress);
|
2023-03-05 13:46:21 -08:00
|
|
|
};
|
2023-03-07 12:43:16 -08:00
|
|
|
|
|
|
|
const runTests = (testIds: string[]) => {
|
2023-03-09 20:02:42 -08:00
|
|
|
// Clear test results.
|
|
|
|
{
|
|
|
|
const testIdSet = new Set(testIds);
|
|
|
|
for (const test of rootSuite.value?.allTests() || []) {
|
|
|
|
if (testIdSet.has(test.id))
|
2023-03-10 12:41:00 -08:00
|
|
|
(test as TeleTestCase)._createTestResult('pending');
|
2023-03-09 20:02:42 -08:00
|
|
|
}
|
|
|
|
setRootSuite({ ...rootSuite });
|
|
|
|
}
|
|
|
|
|
|
|
|
const time = ' [' + new Date().toLocaleTimeString() + ']';
|
|
|
|
xtermDataSource.write('\x1B[2m—'.repeat(Math.max(0, xtermSize.cols - time.length)) + time + '\x1B[22m');
|
2023-03-09 21:45:57 -08:00
|
|
|
setProgress({ total: testIds.length, passed: 0, failed: 0, skipped: 0 });
|
2023-03-07 12:43:16 -08:00
|
|
|
setIsRunningTest(true);
|
|
|
|
sendMessage('run', { testIds }).then(() => {
|
|
|
|
setIsRunningTest(false);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2023-03-10 12:41:00 -08:00
|
|
|
const updateFilter = (name: string, value: string) => {
|
|
|
|
const result: string[] = [];
|
|
|
|
const prefix = name + ':';
|
|
|
|
for (const t of filterText.split(' ')) {
|
|
|
|
if (t.startsWith(prefix)) {
|
|
|
|
if (value) {
|
|
|
|
result.push(prefix + value);
|
|
|
|
value = '';
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
result.push(t);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (value)
|
|
|
|
result.unshift(prefix + value);
|
|
|
|
setFilterText(result.join(' '));
|
|
|
|
};
|
|
|
|
|
2023-03-09 20:02:42 -08:00
|
|
|
const result = selectedTest?.results[0];
|
2023-03-11 11:43:33 -08:00
|
|
|
const isFinished = result && result.duration >= 0;
|
|
|
|
return <div className='vbox watch-mode'>
|
2023-03-07 14:24:50 -08:00
|
|
|
<SplitView sidebarSize={250} orientation='horizontal' sidebarIsFirst={true}>
|
2023-03-11 11:43:33 -08:00
|
|
|
<div className='vbox'>
|
|
|
|
<div className={'vbox' + (isShowingOutput ? '' : ' hidden')}>
|
|
|
|
<Toolbar>
|
|
|
|
<div className='section-title'>Output</div>
|
|
|
|
<ToolbarButton icon='circle-slash' title='Clear output' onClick={() => xtermDataSource.clear()}></ToolbarButton>
|
|
|
|
<div className='spacer'></div>
|
|
|
|
<ToolbarButton icon='close' title='Close' onClick={() => setIsShowingOutput(false)}></ToolbarButton>
|
|
|
|
</Toolbar>
|
|
|
|
<XtermWrapper source={xtermDataSource}></XtermWrapper>;
|
|
|
|
</div>
|
|
|
|
<div className={'vbox' + (isShowingOutput ? ' hidden' : '')}>
|
|
|
|
{isFinished && <FinishedTraceView testResult={result} />}
|
|
|
|
{!isFinished && <InProgressTraceView testResult={result} />}
|
|
|
|
</div>
|
|
|
|
</div>
|
2023-03-07 14:24:50 -08:00
|
|
|
<div className='vbox watch-mode-sidebar'>
|
|
|
|
<Toolbar>
|
|
|
|
<div className='section-title' style={{ cursor: 'pointer' }} onClick={() => setSettingsVisible(false)}>Tests</div>
|
2023-03-09 21:45:57 -08:00
|
|
|
<ToolbarButton icon='play' title='Run' onClick={() => runTests(visibleTestIds)} disabled={isRunningTest}></ToolbarButton>
|
2023-03-07 14:24:50 -08:00
|
|
|
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest}></ToolbarButton>
|
2023-03-07 20:34:57 -08:00
|
|
|
<ToolbarButton icon='refresh' title='Reload' onClick={() => refreshRootSuite(true)} disabled={isRunningTest}></ToolbarButton>
|
2023-03-07 15:07:52 -08:00
|
|
|
<ToolbarButton icon='eye-watch' title='Watch' toggled={isWatchingFiles} onClick={() => setIsWatchingFiles(!isWatchingFiles)}></ToolbarButton>
|
2023-03-07 12:43:16 -08:00
|
|
|
<div className='spacer'></div>
|
2023-03-07 14:24:50 -08:00
|
|
|
<ToolbarButton icon='gear' title='Toggle color mode' toggled={settingsVisible} onClick={() => { setSettingsVisible(!settingsVisible); }}></ToolbarButton>
|
2023-03-11 11:43:33 -08:00
|
|
|
<ToolbarButton icon='terminal' title='Toggle color mode' toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }}></ToolbarButton>
|
2023-03-07 14:24:50 -08:00
|
|
|
</Toolbar>
|
2023-03-10 12:41:00 -08:00
|
|
|
{!settingsVisible && <Expandable
|
|
|
|
title={<input ref={inputRef} type='search' placeholder='Filter (e.g. text, @tag)' spellCheck={false} value={filterText}
|
2023-03-09 21:45:57 -08:00
|
|
|
onChange={e => {
|
|
|
|
setFilterText(e.target.value);
|
|
|
|
}}
|
|
|
|
onKeyDown={e => {
|
|
|
|
if (e.key === 'Enter')
|
|
|
|
runTests(visibleTestIds);
|
2023-03-10 12:41:00 -08:00
|
|
|
}}></input>}
|
|
|
|
style={{ flex: 'none', marginTop: 8 }}
|
|
|
|
expanded={filterExpanded}
|
|
|
|
setExpanded={setFilterExpanded}>
|
|
|
|
<div className='filters'>
|
|
|
|
<span>Status:</span>
|
|
|
|
<div onClick={() => updateFilter('s', '')}>all</div>
|
|
|
|
{['failed', 'passed', 'skipped'].map(s => <div className={filterText.includes('s:' + s) ? 'filters-toggled' : ''} onClick={() => updateFilter('s', s)}>{s}</div>)}
|
|
|
|
</div>
|
|
|
|
{[...projects.values()].filter(v => v).length > 1 && <div className='filters'>
|
|
|
|
<span>Project:</span>
|
|
|
|
<div onClick={() => updateFilter('p', '')}>all</div>
|
|
|
|
{[...projects].filter(([k, v]) => v).map(([k, v]) => k).map(p => <div className={filterText.includes('p:' + p) ? 'filters-toggled' : ''} onClick={() => updateFilter('p', p)}>{p}</div>)}
|
|
|
|
</div>}
|
|
|
|
</Expandable>}
|
2023-03-07 17:20:41 -08:00
|
|
|
<TestList
|
|
|
|
projects={projects}
|
2023-03-09 21:45:57 -08:00
|
|
|
filterText={filterText}
|
2023-03-07 14:24:50 -08:00
|
|
|
rootSuite={rootSuite}
|
|
|
|
isRunningTest={isRunningTest}
|
2023-03-07 15:07:52 -08:00
|
|
|
isWatchingFiles={isWatchingFiles}
|
2023-03-07 14:24:50 -08:00
|
|
|
runTests={runTests}
|
2023-03-08 17:33:27 -08:00
|
|
|
onTestSelected={setSelectedTest}
|
2023-03-09 21:45:57 -08:00
|
|
|
isVisible={!settingsVisible}
|
|
|
|
setVisibleTestIds={setVisibleTestIds} />
|
2023-03-07 17:20:41 -08:00
|
|
|
{settingsVisible && <SettingsView projects={projects} setProjects={setProjects} onClose={() => setSettingsVisible(false)}></SettingsView>}
|
2023-03-07 14:24:50 -08:00
|
|
|
</div>
|
|
|
|
</SplitView>
|
|
|
|
<div className='status-line'>
|
2023-03-09 21:45:57 -08:00
|
|
|
<div>Total: {progress.total}</div>
|
|
|
|
{isRunningTest && <div><span className='codicon codicon-loading'></span>Running {visibleTestIds.length}</div>}
|
|
|
|
{!isRunningTest && <div>Showing: {visibleTestIds.length}</div>}
|
|
|
|
<div>{progress.passed} passed</div>
|
|
|
|
<div>{progress.failed} failed</div>
|
|
|
|
<div>{progress.skipped} skipped</div>
|
2023-03-07 12:43:16 -08:00
|
|
|
</div>
|
2023-03-07 14:24:50 -08:00
|
|
|
</div>;
|
2023-03-07 12:43:16 -08:00
|
|
|
};
|
|
|
|
|
2023-03-08 17:33:27 -08:00
|
|
|
const TreeListView = TreeView<TreeItem>;
|
|
|
|
|
2023-03-07 12:43:16 -08:00
|
|
|
export const TestList: React.FC<{
|
2023-03-07 17:20:41 -08:00
|
|
|
projects: Map<string, boolean>,
|
2023-03-09 21:45:57 -08:00
|
|
|
filterText: string,
|
2023-03-07 12:43:16 -08:00
|
|
|
rootSuite: { value: Suite | undefined },
|
|
|
|
runTests: (testIds: string[]) => void,
|
|
|
|
isRunningTest: boolean,
|
2023-03-07 15:07:52 -08:00
|
|
|
isWatchingFiles: boolean,
|
2023-03-09 21:45:57 -08:00
|
|
|
isVisible: boolean,
|
|
|
|
setVisibleTestIds: (testIds: string[]) => void,
|
2023-03-08 17:33:27 -08:00
|
|
|
onTestSelected: (test: TestCase | undefined) => void,
|
2023-03-09 21:45:57 -08:00
|
|
|
}> = ({ projects, filterText, rootSuite, runTests, isRunningTest, isWatchingFiles, isVisible, onTestSelected, setVisibleTestIds }) => {
|
2023-03-08 17:33:27 -08:00
|
|
|
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
2023-03-07 12:43:16 -08:00
|
|
|
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
|
2023-03-01 15:27:23 -08:00
|
|
|
|
|
|
|
React.useEffect(() => {
|
2023-03-07 20:34:57 -08:00
|
|
|
refreshRootSuite(true);
|
2023-03-01 15:27:23 -08:00
|
|
|
}, []);
|
|
|
|
|
2023-03-09 20:02:42 -08:00
|
|
|
const { rootItem, treeItemMap } = React.useMemo(() => {
|
2023-03-08 17:33:27 -08:00
|
|
|
const rootItem = createTree(rootSuite.value, projects);
|
|
|
|
filterTree(rootItem, filterText);
|
2023-03-09 20:02:42 -08:00
|
|
|
hideOnlyTests(rootItem);
|
2023-03-04 16:28:30 -08:00
|
|
|
const treeItemMap = new Map<string, TreeItem>();
|
|
|
|
const visibleTestIds = new Set<string>();
|
|
|
|
const visit = (treeItem: TreeItem) => {
|
2023-03-09 20:02:42 -08:00
|
|
|
if (treeItem.kind === 'case')
|
|
|
|
treeItem.tests.forEach(t => visibleTestIds.add(t.id));
|
|
|
|
treeItem.children.forEach(visit);
|
2023-03-04 16:28:30 -08:00
|
|
|
treeItemMap.set(treeItem.id, treeItem);
|
|
|
|
};
|
2023-03-08 17:33:27 -08:00
|
|
|
visit(rootItem);
|
2023-03-09 21:45:57 -08:00
|
|
|
setVisibleTestIds([...visibleTestIds]);
|
2023-03-09 20:02:42 -08:00
|
|
|
return { rootItem, treeItemMap };
|
2023-03-09 21:45:57 -08:00
|
|
|
}, [filterText, rootSuite, projects, setVisibleTestIds]);
|
2023-03-04 19:39:55 -08:00
|
|
|
|
2023-03-08 17:33:27 -08:00
|
|
|
const { selectedTreeItem } = React.useMemo(() => {
|
2023-03-06 22:35:57 -08:00
|
|
|
const selectedTreeItem = selectedTreeItemId ? treeItemMap.get(selectedTreeItemId) : undefined;
|
2023-03-08 17:33:27 -08:00
|
|
|
let selectedTest: TestCase | undefined;
|
2023-03-06 22:35:57 -08:00
|
|
|
if (selectedTreeItem?.kind === 'test')
|
2023-03-08 17:33:27 -08:00
|
|
|
selectedTest = selectedTreeItem.test;
|
|
|
|
else if (selectedTreeItem?.kind === 'case' && selectedTreeItem.tests.length === 1)
|
|
|
|
selectedTest = selectedTreeItem.tests[0];
|
|
|
|
onTestSelected(selectedTest);
|
|
|
|
return { selectedTreeItem };
|
|
|
|
}, [onTestSelected, selectedTreeItemId, treeItemMap]);
|
2023-03-02 13:45:15 -08:00
|
|
|
|
2023-03-07 15:07:52 -08:00
|
|
|
React.useEffect(() => {
|
2023-03-07 20:34:57 -08:00
|
|
|
sendMessageNoReply('watch', { fileName: isWatchingFiles ? fileName(selectedTreeItem) : undefined });
|
|
|
|
}, [selectedTreeItem, isWatchingFiles]);
|
2023-03-07 15:07:52 -08:00
|
|
|
|
2023-03-04 16:28:30 -08:00
|
|
|
const runTreeItem = (treeItem: TreeItem) => {
|
|
|
|
setSelectedTreeItemId(treeItem.id);
|
|
|
|
runTests(collectTestIds(treeItem));
|
2023-03-04 15:05:41 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
runWatchedTests = () => {
|
2023-03-04 16:28:30 -08:00
|
|
|
runTests(collectTestIds(selectedTreeItem));
|
2023-03-04 15:05:41 -08:00
|
|
|
};
|
|
|
|
|
2023-03-07 17:20:41 -08:00
|
|
|
if (!isVisible)
|
|
|
|
return <></>;
|
|
|
|
|
2023-03-09 21:45:57 -08:00
|
|
|
return <TreeListView
|
|
|
|
treeState={treeState}
|
|
|
|
setTreeState={setTreeState}
|
|
|
|
rootItem={rootItem}
|
|
|
|
render={treeItem => {
|
|
|
|
return <div className='hbox watch-mode-list-item'>
|
|
|
|
<div className='watch-mode-list-item-title'>{treeItem.title}</div>
|
|
|
|
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={isRunningTest}></ToolbarButton>
|
|
|
|
<ToolbarButton icon='go-to-file' title='Open in VS Code' onClick={() => sendMessageNoReply('open', { location: locationToOpen(treeItem) })}></ToolbarButton>
|
|
|
|
</div>;
|
|
|
|
}}
|
|
|
|
icon={treeItem => {
|
|
|
|
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}
|
|
|
|
onAccepted={runTreeItem}
|
|
|
|
onSelected={treeItem => {
|
|
|
|
setSelectedTreeItemId(treeItem.id);
|
|
|
|
}}
|
|
|
|
noItemsMessage='No tests' />;
|
2023-03-07 14:24:50 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
export const SettingsView: React.FC<{
|
2023-03-07 17:20:41 -08:00
|
|
|
projects: Map<string, boolean>,
|
|
|
|
setProjects: (projectNames: Map<string, boolean>) => void,
|
2023-03-07 14:24:50 -08:00
|
|
|
onClose: () => void,
|
2023-03-07 17:20:41 -08:00
|
|
|
}> = ({ projects, setProjects, onClose }) => {
|
2023-03-07 14:24:50 -08:00
|
|
|
return <div className='vbox'>
|
|
|
|
<div className='hbox' style={{ flex: 'none' }}>
|
|
|
|
<div className='section-title' style={{ marginTop: 10 }}>Projects</div>
|
|
|
|
<div className='spacer'></div>
|
|
|
|
<ToolbarButton icon='close' title='Close settings' toggled={false} onClick={onClose}></ToolbarButton>
|
|
|
|
</div>
|
2023-03-07 17:20:41 -08:00
|
|
|
{[...projects.entries()].map(([projectName, value]) => {
|
2023-03-09 08:04:02 -08:00
|
|
|
return <div style={{ display: 'flex', alignItems: 'center', lineHeight: '24px', marginLeft: 5 }}>
|
2023-03-08 18:24:45 -08:00
|
|
|
<input id={`project-${projectName}`} type='checkbox' checked={value} style={{ cursor: 'pointer' }} onClick={() => {
|
2023-03-07 17:20:41 -08:00
|
|
|
const copy = new Map(projects);
|
|
|
|
copy.set(projectName, !copy.get(projectName));
|
|
|
|
if (![...copy.values()].includes(true))
|
|
|
|
copy.set(projectName, true);
|
|
|
|
setProjects(copy);
|
2023-03-08 18:24:45 -08:00
|
|
|
}}/>
|
|
|
|
<label htmlFor={`project-${projectName}`} style={{ cursor: 'pointer' }}>
|
2023-03-07 14:24:50 -08:00
|
|
|
{projectName}
|
|
|
|
</label>
|
|
|
|
</div>;
|
|
|
|
})}
|
|
|
|
<div className='section-title'>Appearance</div>
|
|
|
|
<div style={{ marginLeft: 3 }}>
|
|
|
|
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}>Toggle color mode</ToolbarButton>
|
|
|
|
</div>
|
2023-03-07 12:43:16 -08:00
|
|
|
</div>;
|
2023-03-01 15:27:23 -08:00
|
|
|
};
|
|
|
|
|
2023-03-09 20:02:42 -08:00
|
|
|
export const InProgressTraceView: React.FC<{
|
|
|
|
testResult: TestResult | undefined,
|
|
|
|
}> = ({ testResult }) => {
|
2023-03-01 15:27:23 -08:00
|
|
|
const [model, setModel] = React.useState<MultiTraceModel | undefined>();
|
2023-03-06 22:35:57 -08:00
|
|
|
const [stepsProgress, setStepsProgress] = React.useState(0);
|
|
|
|
updateStepsProgress = () => setStepsProgress(stepsProgress + 1);
|
2023-03-01 15:27:23 -08:00
|
|
|
|
|
|
|
React.useEffect(() => {
|
2023-03-09 20:02:42 -08:00
|
|
|
setModel(testResult ? stepsToModel(testResult) : undefined);
|
|
|
|
}, [stepsProgress, testResult]);
|
2023-03-06 22:35:57 -08:00
|
|
|
|
2023-03-11 11:43:33 -08:00
|
|
|
return <Workbench model={model} hideTimelineBars={true} hideStackFrames={true} showSourcesFirst={true} />;
|
2023-03-09 20:02:42 -08:00
|
|
|
};
|
2023-03-01 15:27:23 -08:00
|
|
|
|
2023-03-09 20:02:42 -08:00
|
|
|
export const FinishedTraceView: React.FC<{
|
|
|
|
testResult: TestResult,
|
|
|
|
}> = ({ testResult }) => {
|
|
|
|
const [model, setModel] = React.useState<MultiTraceModel | undefined>();
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
// Test finished.
|
|
|
|
const attachment = testResult.attachments.find(a => a.name === 'trace');
|
|
|
|
if (attachment && attachment.path)
|
|
|
|
loadSingleTraceFile(attachment.path).then(setModel);
|
|
|
|
}, [testResult]);
|
|
|
|
|
2023-03-11 11:43:33 -08:00
|
|
|
return <Workbench key='workbench' model={model} hideTimelineBars={true} hideStackFrames={true} showSourcesFirst={true} />;
|
2023-03-01 15:27:23 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
interface Window {
|
|
|
|
binding(data: any): Promise<void>;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-05 13:46:21 -08:00
|
|
|
let receiver: TeleReporterReceiver | undefined;
|
|
|
|
|
2023-03-10 17:01:19 -08:00
|
|
|
let throttleTimer: NodeJS.Timeout | undefined;
|
|
|
|
let throttleData: { rootSuite: Suite, progress: Progress } | undefined;
|
|
|
|
const throttledAction = () => {
|
|
|
|
clearTimeout(throttleTimer);
|
|
|
|
throttleTimer = undefined;
|
|
|
|
updateRootSuite(throttleData!.rootSuite, throttleData!.progress);
|
|
|
|
};
|
|
|
|
|
|
|
|
const throttleUpdateRootSuite = (rootSuite: Suite, progress: Progress, immediate = false) => {
|
|
|
|
throttleData = { rootSuite, progress };
|
|
|
|
if (immediate)
|
|
|
|
throttledAction();
|
|
|
|
else if (!throttleTimer)
|
|
|
|
throttleTimer = setTimeout(throttledAction, 250);
|
|
|
|
};
|
|
|
|
|
2023-03-07 20:34:57 -08:00
|
|
|
const refreshRootSuite = (eraseResults: boolean) => {
|
|
|
|
if (!eraseResults) {
|
|
|
|
sendMessageNoReply('list');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-03-04 19:39:55 -08:00
|
|
|
let rootSuite: Suite;
|
2023-03-05 13:46:21 -08:00
|
|
|
const progress: Progress = {
|
|
|
|
total: 0,
|
|
|
|
passed: 0,
|
|
|
|
failed: 0,
|
2023-03-09 21:45:57 -08:00
|
|
|
skipped: 0,
|
2023-03-05 13:46:21 -08:00
|
|
|
};
|
|
|
|
receiver = new TeleReporterReceiver({
|
2023-03-04 19:39:55 -08:00
|
|
|
onBegin: (config: FullConfig, suite: Suite) => {
|
|
|
|
if (!rootSuite)
|
|
|
|
rootSuite = suite;
|
2023-03-09 21:45:57 -08:00
|
|
|
progress.total = suite.allTests().length;
|
2023-03-05 13:46:21 -08:00
|
|
|
progress.passed = 0;
|
|
|
|
progress.failed = 0;
|
2023-03-09 21:45:57 -08:00
|
|
|
progress.skipped = 0;
|
2023-03-10 17:01:19 -08:00
|
|
|
throttleUpdateRootSuite(rootSuite, progress, true);
|
|
|
|
},
|
|
|
|
|
|
|
|
onEnd: () => {
|
|
|
|
throttleUpdateRootSuite(rootSuite, progress, true);
|
2023-03-04 19:39:55 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
onTestBegin: () => {
|
2023-03-10 17:01:19 -08:00
|
|
|
throttleUpdateRootSuite(rootSuite, progress);
|
2023-03-04 19:39:55 -08:00
|
|
|
},
|
|
|
|
|
2023-03-05 13:46:21 -08:00
|
|
|
onTestEnd: (test: TestCase) => {
|
2023-03-09 21:45:57 -08:00
|
|
|
if (test.outcome() === 'skipped')
|
|
|
|
++progress.skipped;
|
|
|
|
else if (test.outcome() === 'unexpected')
|
2023-03-05 13:46:21 -08:00
|
|
|
++progress.failed;
|
|
|
|
else
|
|
|
|
++progress.passed;
|
2023-03-10 17:01:19 -08:00
|
|
|
throttleUpdateRootSuite(rootSuite, progress);
|
2023-03-08 19:50:32 -08:00
|
|
|
// This will update selected trace viewer.
|
|
|
|
updateStepsProgress();
|
2023-03-04 19:39:55 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
onStepBegin: () => {
|
2023-03-05 13:46:21 -08:00
|
|
|
updateStepsProgress();
|
2023-03-04 19:39:55 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
onStepEnd: () => {
|
2023-03-05 13:46:21 -08:00
|
|
|
updateStepsProgress();
|
2023-03-04 19:39:55 -08:00
|
|
|
},
|
|
|
|
});
|
2023-03-05 13:46:21 -08:00
|
|
|
sendMessageNoReply('list');
|
|
|
|
};
|
2023-03-01 15:27:23 -08:00
|
|
|
|
2023-03-05 13:46:21 -08:00
|
|
|
(window as any).dispatch = (message: any) => {
|
2023-03-07 20:34:57 -08:00
|
|
|
if (message.method === 'listChanged') {
|
|
|
|
refreshRootSuite(false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-03-07 14:24:50 -08:00
|
|
|
if (message.method === 'fileChanged') {
|
2023-03-05 13:46:21 -08:00
|
|
|
runWatchedTests();
|
2023-03-07 20:34:57 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (message.method === 'stdio') {
|
2023-03-07 14:24:50 -08:00
|
|
|
if (message.params.buffer) {
|
|
|
|
const data = atob(message.params.buffer);
|
|
|
|
xtermDataSource.write(data);
|
|
|
|
} else {
|
|
|
|
xtermDataSource.write(message.params.text);
|
|
|
|
}
|
2023-03-07 20:34:57 -08:00
|
|
|
return;
|
2023-03-07 14:24:50 -08:00
|
|
|
}
|
2023-03-07 20:34:57 -08:00
|
|
|
|
|
|
|
receiver?.dispatch(message);
|
2023-03-05 13:46:21 -08:00
|
|
|
};
|
2023-03-04 15:05:41 -08:00
|
|
|
|
|
|
|
const sendMessage = async (method: string, params: any) => {
|
|
|
|
await (window as any).sendMessage({ method, params });
|
2023-03-01 15:27:23 -08:00
|
|
|
};
|
|
|
|
|
2023-03-04 15:05:41 -08:00
|
|
|
const sendMessageNoReply = (method: string, params?: any) => {
|
|
|
|
sendMessage(method, params).catch((e: Error) => {
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
console.error(e);
|
2023-03-01 15:27:23 -08:00
|
|
|
});
|
2023-03-04 15:05:41 -08:00
|
|
|
};
|
|
|
|
|
2023-03-04 16:28:30 -08:00
|
|
|
const fileName = (treeItem?: TreeItem): string | undefined => {
|
2023-03-08 19:50:32 -08:00
|
|
|
return treeItem?.location.file;
|
2023-03-04 16:28:30 -08:00
|
|
|
};
|
|
|
|
|
2023-03-07 17:20:41 -08:00
|
|
|
const locationToOpen = (treeItem?: TreeItem) => {
|
|
|
|
if (!treeItem)
|
|
|
|
return;
|
2023-03-08 19:50:32 -08:00
|
|
|
return treeItem.location.file + ':' + treeItem.location.line;
|
2023-03-07 17:20:41 -08:00
|
|
|
};
|
|
|
|
|
2023-03-04 16:28:30 -08:00
|
|
|
const collectTestIds = (treeItem?: TreeItem): string[] => {
|
|
|
|
if (!treeItem)
|
|
|
|
return [];
|
2023-03-04 15:05:41 -08:00
|
|
|
const testIds: string[] = [];
|
2023-03-04 16:28:30 -08:00
|
|
|
const visit = (treeItem: TreeItem) => {
|
2023-03-08 17:33:27 -08:00
|
|
|
if (treeItem.kind === 'case')
|
|
|
|
testIds.push(...treeItem.tests.map(t => t.id));
|
|
|
|
else if (treeItem.kind === 'test')
|
2023-03-04 16:28:30 -08:00
|
|
|
testIds.push(treeItem.id);
|
2023-03-08 17:33:27 -08:00
|
|
|
else
|
|
|
|
treeItem.children?.forEach(visit);
|
2023-03-04 16:28:30 -08:00
|
|
|
};
|
|
|
|
visit(treeItem);
|
2023-03-04 15:05:41 -08:00
|
|
|
return testIds;
|
|
|
|
};
|
2023-03-04 16:28:30 -08:00
|
|
|
|
2023-03-05 13:46:21 -08:00
|
|
|
type Progress = {
|
|
|
|
total: number;
|
|
|
|
passed: number;
|
|
|
|
failed: number;
|
2023-03-09 21:45:57 -08:00
|
|
|
skipped: number;
|
2023-03-05 13:46:21 -08:00
|
|
|
};
|
|
|
|
|
2023-03-04 16:28:30 -08:00
|
|
|
type TreeItemBase = {
|
2023-03-08 19:50:32 -08:00
|
|
|
kind: 'root' | 'group' | 'case' | 'test',
|
2023-03-04 16:28:30 -08:00
|
|
|
id: string;
|
|
|
|
title: string;
|
2023-03-08 19:50:32 -08:00
|
|
|
location: Location,
|
2023-03-08 17:33:27 -08:00
|
|
|
children: TreeItem[];
|
2023-03-09 20:02:42 -08:00
|
|
|
status: 'none' | 'running' | 'passed' | 'failed' | 'skipped';
|
2023-03-04 16:28:30 -08:00
|
|
|
};
|
|
|
|
|
2023-03-08 19:50:32 -08:00
|
|
|
type GroupItem = TreeItemBase & {
|
|
|
|
kind: 'group',
|
|
|
|
children: (TestCaseItem | GroupItem)[];
|
2023-03-04 16:28:30 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
type TestCaseItem = TreeItemBase & {
|
|
|
|
kind: 'case',
|
2023-03-08 17:33:27 -08:00
|
|
|
tests: TestCase[];
|
2023-03-10 12:41:00 -08:00
|
|
|
children: TestItem[];
|
2023-03-04 16:28:30 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
type TestItem = TreeItemBase & {
|
|
|
|
kind: 'test',
|
|
|
|
test: TestCase;
|
2023-03-10 12:41:00 -08:00
|
|
|
project: string;
|
2023-03-04 16:28:30 -08:00
|
|
|
};
|
|
|
|
|
2023-03-08 19:50:32 -08:00
|
|
|
type TreeItem = GroupItem | TestCaseItem | TestItem;
|
2023-03-04 16:28:30 -08:00
|
|
|
|
2023-03-08 19:50:32 -08:00
|
|
|
function createTree(rootSuite: Suite | undefined, projects: Map<string, boolean>): GroupItem {
|
|
|
|
const rootItem: GroupItem = {
|
|
|
|
kind: 'group',
|
2023-03-08 17:33:27 -08:00
|
|
|
id: 'root',
|
|
|
|
title: '',
|
2023-03-08 19:50:32 -08:00
|
|
|
location: { file: '', line: 0, column: 0 },
|
2023-03-08 17:33:27 -08:00
|
|
|
children: [],
|
2023-03-08 18:24:45 -08:00
|
|
|
status: 'none',
|
2023-03-08 17:33:27 -08:00
|
|
|
};
|
2023-03-08 19:50:32 -08:00
|
|
|
|
|
|
|
const visitSuite = (projectName: string, parentSuite: Suite, parentGroup: GroupItem) => {
|
|
|
|
for (const suite of parentSuite.suites) {
|
|
|
|
const title = suite.title;
|
|
|
|
let group = parentGroup.children.find(item => item.title === title) as GroupItem | undefined;
|
|
|
|
if (!group) {
|
|
|
|
group = {
|
|
|
|
kind: 'group',
|
|
|
|
id: parentGroup.id + '\x1e' + title,
|
|
|
|
title,
|
|
|
|
location: suite.location!,
|
2023-03-04 16:28:30 -08:00
|
|
|
children: [],
|
2023-03-08 18:24:45 -08:00
|
|
|
status: 'none',
|
2023-03-04 16:28:30 -08:00
|
|
|
};
|
2023-03-08 19:50:32 -08:00
|
|
|
parentGroup.children.push(group);
|
2023-03-04 16:28:30 -08:00
|
|
|
}
|
2023-03-08 19:50:32 -08:00
|
|
|
visitSuite(projectName, suite, group);
|
|
|
|
}
|
2023-03-04 16:28:30 -08:00
|
|
|
|
2023-03-08 19:50:32 -08:00
|
|
|
for (const test of parentSuite.tests) {
|
|
|
|
const title = test.title;
|
|
|
|
let testCaseItem = parentGroup.children.find(t => t.title === title) as TestCaseItem;
|
|
|
|
if (!testCaseItem) {
|
|
|
|
testCaseItem = {
|
|
|
|
kind: 'case',
|
|
|
|
id: parentGroup.id + '\x1e' + title,
|
|
|
|
title,
|
2023-03-08 17:33:27 -08:00
|
|
|
children: [],
|
2023-03-08 19:50:32 -08:00
|
|
|
tests: [],
|
|
|
|
location: test.location,
|
|
|
|
status: 'none',
|
|
|
|
};
|
|
|
|
parentGroup.children.push(testCaseItem);
|
2023-03-04 16:28:30 -08:00
|
|
|
}
|
2023-03-08 19:50:32 -08:00
|
|
|
|
2023-03-09 20:02:42 -08:00
|
|
|
let status: 'none' | 'running' | 'passed' | 'failed' | 'skipped' = 'none';
|
2023-03-08 19:50:32 -08:00
|
|
|
if (test.results.some(r => r.duration === -1))
|
|
|
|
status = 'running';
|
2023-03-09 20:02:42 -08:00
|
|
|
else if (test.results.length && test.outcome() === 'skipped')
|
|
|
|
status = 'skipped';
|
2023-03-08 19:50:32 -08:00
|
|
|
else if (test.results.length && test.outcome() !== 'expected')
|
|
|
|
status = 'failed';
|
2023-03-09 20:02:42 -08:00
|
|
|
else if (test.results.length && test.outcome() === 'expected')
|
2023-03-08 19:50:32 -08:00
|
|
|
status = 'passed';
|
|
|
|
|
|
|
|
testCaseItem.tests.push(test);
|
|
|
|
testCaseItem.children.push({
|
|
|
|
kind: 'test',
|
|
|
|
id: test.id,
|
|
|
|
title: projectName,
|
|
|
|
location: test.location!,
|
|
|
|
test,
|
|
|
|
children: [],
|
|
|
|
status,
|
2023-03-10 12:41:00 -08:00
|
|
|
project: projectName
|
2023-03-08 19:50:32 -08:00
|
|
|
});
|
2023-03-04 16:28:30 -08:00
|
|
|
}
|
2023-03-08 19:50:32 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
for (const projectSuite of rootSuite?.suites || []) {
|
|
|
|
if (!projects.get(projectSuite.title))
|
|
|
|
continue;
|
|
|
|
visitSuite(projectSuite.title, projectSuite, rootItem);
|
2023-03-04 16:28:30 -08:00
|
|
|
}
|
2023-03-08 18:24:45 -08:00
|
|
|
|
|
|
|
const propagateStatus = (treeItem: TreeItem) => {
|
|
|
|
for (const child of treeItem.children)
|
|
|
|
propagateStatus(child);
|
|
|
|
|
|
|
|
let allPassed = treeItem.children.length > 0;
|
2023-03-09 20:02:42 -08:00
|
|
|
let allSkipped = treeItem.children.length > 0;
|
2023-03-08 18:24:45 -08:00
|
|
|
let hasFailed = false;
|
|
|
|
let hasRunning = false;
|
|
|
|
|
|
|
|
for (const child of treeItem.children) {
|
2023-03-09 20:02:42 -08:00
|
|
|
allSkipped = allSkipped && child.status === 'skipped';
|
|
|
|
allPassed = allPassed && (child.status === 'passed' || child.status === 'skipped');
|
2023-03-08 18:24:45 -08:00
|
|
|
hasFailed = hasFailed || child.status === 'failed';
|
|
|
|
hasRunning = hasRunning || child.status === 'running';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hasRunning)
|
|
|
|
treeItem.status = 'running';
|
|
|
|
else if (hasFailed)
|
|
|
|
treeItem.status = 'failed';
|
2023-03-09 20:02:42 -08:00
|
|
|
else if (allSkipped)
|
|
|
|
treeItem.status = 'skipped';
|
2023-03-08 18:24:45 -08:00
|
|
|
else if (allPassed)
|
|
|
|
treeItem.status = 'passed';
|
|
|
|
};
|
|
|
|
propagateStatus(rootItem);
|
2023-03-08 17:33:27 -08:00
|
|
|
return rootItem;
|
2023-03-04 16:28:30 -08:00
|
|
|
}
|
|
|
|
|
2023-03-08 19:50:32 -08:00
|
|
|
function filterTree(rootItem: GroupItem, filterText: string) {
|
2023-03-04 16:28:30 -08:00
|
|
|
const trimmedFilterText = filterText.trim();
|
|
|
|
const filterTokens = trimmedFilterText.toLowerCase().split(' ');
|
2023-03-10 12:41:00 -08:00
|
|
|
const textTokens = filterTokens.filter(token => !token.match(/^[sp]:/));
|
|
|
|
const statuses = new Set(filterTokens.filter(t => t.startsWith('s:')).map(t => t.substring(2)));
|
|
|
|
if (statuses.size)
|
|
|
|
statuses.add('running');
|
|
|
|
const projects = new Set(filterTokens.filter(t => t.startsWith('p:')).map(t => t.substring(2)));
|
|
|
|
|
|
|
|
const filter = (testCase: TestCaseItem) => {
|
|
|
|
const title = testCase.tests[0].titlePath().join(' ').toLowerCase();
|
|
|
|
if (!textTokens.every(token => title.includes(token)))
|
|
|
|
return false;
|
|
|
|
testCase.children = (testCase.children as TestItem[]).filter(test => !statuses.size || statuses.has(test.status));
|
|
|
|
testCase.children = (testCase.children as TestItem[]).filter(test => !projects.size || projects.has(test.project));
|
|
|
|
testCase.tests = (testCase.children as TestItem[]).map(c => c.test);
|
|
|
|
return !!testCase.children.length;
|
|
|
|
};
|
2023-03-08 19:50:32 -08:00
|
|
|
|
|
|
|
const visit = (treeItem: GroupItem) => {
|
|
|
|
const newChildren: (GroupItem | TestCaseItem)[] = [];
|
|
|
|
for (const child of treeItem.children) {
|
|
|
|
if (child.kind === 'case') {
|
2023-03-10 12:41:00 -08:00
|
|
|
if (filter(child))
|
2023-03-08 19:50:32 -08:00
|
|
|
newChildren.push(child);
|
|
|
|
} else {
|
|
|
|
visit(child);
|
|
|
|
if (child.children.length)
|
|
|
|
newChildren.push(child);
|
2023-03-04 16:28:30 -08:00
|
|
|
}
|
|
|
|
}
|
2023-03-08 19:50:32 -08:00
|
|
|
treeItem.children = newChildren;
|
|
|
|
};
|
|
|
|
visit(rootItem);
|
2023-03-04 16:28:30 -08:00
|
|
|
}
|
|
|
|
|
2023-03-08 19:50:32 -08:00
|
|
|
function hideOnlyTests(rootItem: GroupItem) {
|
2023-03-08 17:33:27 -08:00
|
|
|
const visit = (treeItem: TreeItem) => {
|
|
|
|
if (treeItem.kind === 'case' && treeItem.children.length === 1)
|
|
|
|
treeItem.children = [];
|
|
|
|
else
|
|
|
|
treeItem.children.forEach(visit);
|
|
|
|
};
|
|
|
|
visit(rootItem);
|
2023-03-04 16:28:30 -08:00
|
|
|
}
|
2023-03-06 12:25:00 -08:00
|
|
|
|
|
|
|
async function loadSingleTraceFile(url: string): Promise<MultiTraceModel> {
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
params.set('trace', url);
|
|
|
|
const response = await fetch(`contexts?${params.toString()}`);
|
|
|
|
const contextEntries = await response.json() as ContextEntry[];
|
|
|
|
return new MultiTraceModel(contextEntries);
|
|
|
|
}
|
2023-03-06 22:35:57 -08:00
|
|
|
|
|
|
|
function stepsToModel(result: TestResult): MultiTraceModel {
|
|
|
|
let startTime = Number.MAX_VALUE;
|
|
|
|
let endTime = Number.MIN_VALUE;
|
|
|
|
const actions: trace.ActionTraceEvent[] = [];
|
|
|
|
|
|
|
|
const flatSteps: TestStep[] = [];
|
|
|
|
const visit = (step: TestStep) => {
|
|
|
|
flatSteps.push(step);
|
|
|
|
step.steps.forEach(visit);
|
|
|
|
};
|
|
|
|
result.steps.forEach(visit);
|
|
|
|
|
|
|
|
for (const step of flatSteps) {
|
|
|
|
let callId: string;
|
|
|
|
if (step.category === 'pw:api')
|
|
|
|
callId = `call@${actions.length}`;
|
|
|
|
else if (step.category === 'expect')
|
|
|
|
callId = `expect@${actions.length}`;
|
|
|
|
else
|
|
|
|
continue;
|
|
|
|
const action: trace.ActionTraceEvent = {
|
|
|
|
type: 'action',
|
|
|
|
callId,
|
|
|
|
startTime: step.startTime.getTime(),
|
|
|
|
endTime: step.startTime.getTime() + step.duration,
|
|
|
|
apiName: step.title,
|
|
|
|
class: '',
|
|
|
|
method: '',
|
|
|
|
params: {},
|
|
|
|
wallTime: step.startTime.getTime(),
|
|
|
|
log: [],
|
|
|
|
snapshots: [],
|
|
|
|
error: step.error ? { name: 'Error', message: step.error.message || step.error.value || '' } : undefined,
|
|
|
|
};
|
|
|
|
if (startTime > action.startTime)
|
|
|
|
startTime = action.startTime;
|
|
|
|
if (endTime < action.endTime)
|
|
|
|
endTime = action.endTime;
|
|
|
|
actions.push(action);
|
|
|
|
}
|
|
|
|
|
|
|
|
const contextEntry: ContextEntry = {
|
|
|
|
traceUrl: '',
|
|
|
|
startTime,
|
|
|
|
endTime,
|
|
|
|
browserName: '',
|
|
|
|
options: {
|
|
|
|
viewport: undefined,
|
|
|
|
deviceScaleFactor: undefined,
|
|
|
|
isMobile: undefined,
|
|
|
|
userAgent: undefined
|
|
|
|
},
|
|
|
|
pages: [],
|
|
|
|
resources: [],
|
|
|
|
actions,
|
|
|
|
events: [],
|
|
|
|
initializers: {},
|
|
|
|
hasSource: false
|
|
|
|
};
|
|
|
|
|
|
|
|
return new MultiTraceModel([contextEntry]);
|
|
|
|
}
|