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-17 09:41:23 -07:00
|
|
|
import { baseFullConfig, TeleReporterReceiver, TeleSuite } from '@testIsomorphic/teleReceiver';
|
2023-03-14 15:58:55 -07:00
|
|
|
import type { TeleTestCase } from '@testIsomorphic/teleReceiver';
|
2023-04-19 16:51:42 -07:00
|
|
|
import type { FullConfig, Suite, TestCase, Location, TestError } from '@playwright/test/types/testReporter';
|
2023-03-01 15:27:23 -08:00
|
|
|
import { SplitView } from '@web/components/splitView';
|
2023-03-31 18:34:51 -07:00
|
|
|
import { idForAction, MultiTraceModel } from './modelUtil';
|
2023-04-19 16:51:42 -07:00
|
|
|
import './uiModeView.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-06 12:25:00 -08:00
|
|
|
import type { ContextEntry } from '../entries';
|
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-13 22:19:31 -07:00
|
|
|
import { toggleTheme } from '@web/theme';
|
2023-03-15 22:33:40 -07:00
|
|
|
import { artifactsFolderName } from '@testIsomorphic/folders';
|
2023-03-20 17:12:02 -07:00
|
|
|
import { msToString, settings, useSetting } from '@web/uiUtils';
|
2023-03-31 18:34:51 -07:00
|
|
|
import type { ActionTraceEvent } from '@trace/trace';
|
2023-03-01 15:27:23 -08:00
|
|
|
|
2023-03-19 12:04:19 -07:00
|
|
|
let updateRootSuite: (config: FullConfig, rootSuite: Suite, progress: Progress | undefined) => void = () => {};
|
2023-03-19 14:50:09 -07:00
|
|
|
let runWatchedTests = (fileNames: string[]) => {};
|
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-17 09:41:23 -07:00
|
|
|
type TestModel = {
|
|
|
|
config: FullConfig | undefined;
|
|
|
|
rootSuite: Suite | undefined;
|
|
|
|
};
|
|
|
|
|
2023-04-19 16:51:42 -07:00
|
|
|
export const UIModeView: React.FC<{}> = ({
|
2023-03-01 15:27:23 -08:00
|
|
|
}) => {
|
2023-03-13 22:19:31 -07:00
|
|
|
const [filterText, setFilterText] = React.useState<string>('');
|
|
|
|
const [isShowingOutput, setIsShowingOutput] = React.useState<boolean>(false);
|
|
|
|
|
|
|
|
const [statusFilters, setStatusFilters] = React.useState<Map<string, boolean>>(new Map([
|
|
|
|
['passed', false],
|
|
|
|
['failed', false],
|
|
|
|
['skipped', false],
|
|
|
|
]));
|
|
|
|
const [projectFilters, setProjectFilters] = React.useState<Map<string, boolean>>(new Map());
|
2023-03-17 09:41:23 -07:00
|
|
|
const [testModel, setTestModel] = React.useState<TestModel>({ config: undefined, rootSuite: undefined });
|
2023-03-19 12:04:19 -07:00
|
|
|
const [progress, setProgress] = React.useState<Progress & { total: number } | undefined>();
|
|
|
|
const [selectedItem, setSelectedItem] = React.useState<{ location?: Location, testCase?: TestCase }>({});
|
2023-03-20 13:45:35 -07:00
|
|
|
const [visibleTestIds, setVisibleTestIds] = React.useState<Set<string>>(new Set());
|
2023-03-13 22:19:31 -07:00
|
|
|
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
2023-03-19 12:04:19 -07:00
|
|
|
const [runningState, setRunningState] = React.useState<{ testIds: Set<string>, itemSelectedByUser?: boolean } | undefined>();
|
2023-03-19 14:50:09 -07:00
|
|
|
const [watchAll, setWatchAll] = useSetting<boolean>('watch-all', false);
|
2023-03-20 17:12:02 -07:00
|
|
|
const [watchedTreeIds, setWatchedTreeIds] = React.useState<{ value: Set<string> }>({ value: new Set() });
|
2023-03-20 13:45:35 -07:00
|
|
|
const runTestPromiseChain = React.useRef(Promise.resolve());
|
2023-03-20 21:25:55 -07:00
|
|
|
const runTestBacklog = React.useRef<Set<string>>(new Set());
|
2023-04-19 18:16:18 -07:00
|
|
|
const [collapseAllCount, setCollapseAllCount] = React.useState(0);
|
2023-03-12 10:50:21 -07:00
|
|
|
|
2023-03-09 21:45:57 -08:00
|
|
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
|
2023-03-13 22:19:31 -07:00
|
|
|
const reloadTests = () => {
|
|
|
|
setIsLoading(true);
|
2023-03-20 17:12:02 -07:00
|
|
|
setWatchedTreeIds({ value: new Set() });
|
2023-03-19 12:04:19 -07:00
|
|
|
updateRootSuite(baseFullConfig, new TeleSuite('', 'root'), undefined);
|
2023-03-13 22:19:31 -07:00
|
|
|
refreshRootSuite(true).then(() => {
|
|
|
|
setIsLoading(false);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2023-03-09 21:45:57 -08:00
|
|
|
React.useEffect(() => {
|
|
|
|
inputRef.current?.focus();
|
2023-03-13 22:19:31 -07:00
|
|
|
reloadTests();
|
2023-03-09 21:45:57 -08:00
|
|
|
}, []);
|
2023-03-07 12:43:16 -08:00
|
|
|
|
2023-03-19 12:04:19 -07:00
|
|
|
updateRootSuite = (config: FullConfig, rootSuite: Suite, newProgress: Progress | undefined) => {
|
2023-03-17 09:41:23 -07:00
|
|
|
const selectedProjects = config.configFile ? settings.getObject<string[] | undefined>(config.configFile + ':projects', undefined) : undefined;
|
2023-03-13 22:19:31 -07:00
|
|
|
for (const projectName of projectFilters.keys()) {
|
2023-03-07 17:20:41 -08:00
|
|
|
if (!rootSuite.suites.find(s => s.title === projectName))
|
2023-03-13 22:19:31 -07:00
|
|
|
projectFilters.delete(projectName);
|
2023-03-07 17:20:41 -08:00
|
|
|
}
|
|
|
|
for (const projectSuite of rootSuite.suites) {
|
2023-03-13 22:19:31 -07:00
|
|
|
if (!projectFilters.has(projectSuite.title))
|
2023-03-17 09:41:23 -07:00
|
|
|
projectFilters.set(projectSuite.title, !!selectedProjects?.includes(projectSuite.title));
|
2023-03-07 17:20:41 -08:00
|
|
|
}
|
2023-03-17 09:41:23 -07:00
|
|
|
if (!selectedProjects && projectFilters.size && ![...projectFilters.values()].includes(true))
|
2023-03-13 22:19:31 -07:00
|
|
|
projectFilters.set(projectFilters.entries().next().value[0], true);
|
2023-03-07 17:20:41 -08:00
|
|
|
|
2023-03-17 09:41:23 -07:00
|
|
|
setTestModel({ config, rootSuite });
|
2023-03-13 22:19:31 -07:00
|
|
|
setProjectFilters(new Map(projectFilters));
|
2023-03-19 12:04:19 -07:00
|
|
|
if (runningState && newProgress)
|
|
|
|
setProgress({ ...newProgress, total: runningState.testIds.size });
|
|
|
|
else if (!newProgress)
|
|
|
|
setProgress(undefined);
|
2023-03-05 13:46:21 -08:00
|
|
|
};
|
2023-03-07 12:43:16 -08:00
|
|
|
|
2023-03-20 13:45:35 -07:00
|
|
|
const runTests = React.useCallback((mode: 'queue-if-busy' | 'bounce-if-busy', testIds: Set<string>) => {
|
|
|
|
if (mode === 'bounce-if-busy' && runningState)
|
|
|
|
return;
|
|
|
|
|
2023-03-20 21:25:55 -07:00
|
|
|
runTestBacklog.current = new Set([...runTestBacklog.current, ...testIds]);
|
2023-03-20 13:45:35 -07:00
|
|
|
runTestPromiseChain.current = runTestPromiseChain.current.then(async () => {
|
2023-03-20 21:25:55 -07:00
|
|
|
const testIds = runTestBacklog.current;
|
|
|
|
runTestBacklog.current = new Set();
|
|
|
|
if (!testIds.size)
|
|
|
|
return;
|
|
|
|
|
2023-03-20 13:45:35 -07:00
|
|
|
// Clear test results.
|
|
|
|
{
|
|
|
|
for (const test of testModel.rootSuite?.allTests() || []) {
|
2023-04-06 08:33:17 -07:00
|
|
|
if (testIds.has(test.id)) {
|
|
|
|
(test as TeleTestCase)._clearResults();
|
2023-03-20 13:45:35 -07:00
|
|
|
(test as TeleTestCase)._createTestResult('pending');
|
2023-04-06 08:33:17 -07:00
|
|
|
}
|
2023-03-20 13:45:35 -07:00
|
|
|
}
|
|
|
|
setTestModel({ ...testModel });
|
2023-03-09 20:02:42 -08:00
|
|
|
}
|
|
|
|
|
2023-03-20 13:45:35 -07:00
|
|
|
const time = ' [' + new Date().toLocaleTimeString() + ']';
|
|
|
|
xtermDataSource.write('\x1B[2m—'.repeat(Math.max(0, xtermSize.cols - time.length)) + time + '\x1B[22m');
|
|
|
|
setProgress({ total: testIds.size, passed: 0, failed: 0, skipped: 0 });
|
|
|
|
setRunningState({ testIds });
|
|
|
|
|
|
|
|
await sendMessage('run', { testIds: [...testIds] });
|
2023-03-17 14:10:25 -07:00
|
|
|
// Clear pending tests in case of interrupt.
|
|
|
|
for (const test of testModel.rootSuite?.allTests() || []) {
|
|
|
|
if (test.results[0]?.duration === -1)
|
|
|
|
(test as TeleTestCase)._clearResults();
|
|
|
|
}
|
|
|
|
setTestModel({ ...testModel });
|
2023-03-12 10:42:02 -07:00
|
|
|
setRunningState(undefined);
|
2023-03-07 12:43:16 -08:00
|
|
|
});
|
2023-03-20 13:45:35 -07:00
|
|
|
}, [runningState, testModel]);
|
2023-03-07 12:43:16 -08:00
|
|
|
|
2023-03-12 10:42:02 -07:00
|
|
|
const isRunningTest = !!runningState;
|
2023-03-15 11:17:03 -07:00
|
|
|
|
2023-04-19 16:51:42 -07:00
|
|
|
return <div className='vbox ui-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>
|
2023-03-19 12:04:19 -07:00
|
|
|
<div className='section-title' style={{ flex: 'none' }}>Output</div>
|
2023-03-11 11:43:33 -08:00
|
|
|
<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>
|
2023-03-19 12:04:19 -07:00
|
|
|
<XtermWrapper source={xtermDataSource}></XtermWrapper>
|
2023-03-11 11:43:33 -08:00
|
|
|
</div>
|
|
|
|
<div className={'vbox' + (isShowingOutput ? ' hidden' : '')}>
|
2023-03-19 12:04:19 -07:00
|
|
|
<TraceView item={selectedItem} rootDir={testModel.config?.rootDir} />
|
2023-03-11 11:43:33 -08:00
|
|
|
</div>
|
|
|
|
</div>
|
2023-04-19 16:51:42 -07:00
|
|
|
<div className='vbox ui-mode-sidebar'>
|
2023-03-20 20:45:32 -07:00
|
|
|
<Toolbar noShadow={true} noMinHeight={true}>
|
2023-03-13 22:19:31 -07:00
|
|
|
<img src='icon-32x32.png' />
|
|
|
|
<div className='section-title'>Playwright</div>
|
2023-03-19 14:50:09 -07:00
|
|
|
<ToolbarButton icon='color-mode' title='Toggle color mode' onClick={() => toggleTheme()} />
|
2023-03-13 22:19:31 -07:00
|
|
|
<ToolbarButton icon='refresh' title='Reload' onClick={() => reloadTests()} disabled={isRunningTest || isLoading}></ToolbarButton>
|
|
|
|
<ToolbarButton icon='terminal' title='Toggle output' toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }} />
|
|
|
|
</Toolbar>
|
|
|
|
<FiltersView
|
|
|
|
filterText={filterText}
|
|
|
|
setFilterText={setFilterText}
|
|
|
|
statusFilters={statusFilters}
|
|
|
|
setStatusFilters={setStatusFilters}
|
|
|
|
projectFilters={projectFilters}
|
|
|
|
setProjectFilters={setProjectFilters}
|
2023-03-17 09:41:23 -07:00
|
|
|
testModel={testModel}
|
2023-03-20 13:45:35 -07:00
|
|
|
runTests={() => runTests('bounce-if-busy', visibleTestIds)} />
|
2023-03-19 12:04:19 -07:00
|
|
|
<Toolbar noMinHeight={true}>
|
|
|
|
{!isRunningTest && !progress && <div className='section-title'>Tests</div>}
|
|
|
|
{!isRunningTest && progress && <div data-testid='status-line' className='status-line'>
|
|
|
|
<div>{progress.passed}/{progress.total} passed ({(progress.passed / progress.total) * 100 | 0}%)</div>
|
|
|
|
</div>}
|
|
|
|
{isRunningTest && progress && <div data-testid='status-line' className='status-line'>
|
|
|
|
<div>Running {progress.passed}/{runningState.testIds.size} passed ({(progress.passed / runningState.testIds.size) * 100 | 0}%)</div>
|
|
|
|
</div>}
|
2023-03-20 13:45:35 -07:00
|
|
|
<ToolbarButton icon='play' title='Run all' onClick={() => runTests('bounce-if-busy', visibleTestIds)} disabled={isRunningTest || isLoading}></ToolbarButton>
|
2023-03-13 22:19:31 -07:00
|
|
|
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest || isLoading}></ToolbarButton>
|
2023-03-20 17:12:02 -07:00
|
|
|
<ToolbarButton icon='eye' title='Watch all' toggled={watchAll} onClick={() => setWatchAll(!watchAll)}></ToolbarButton>
|
2023-04-19 18:16:18 -07:00
|
|
|
<ToolbarButton icon='collapse-all' title='Collapse all' onClick={() => {
|
|
|
|
setCollapseAllCount(collapseAllCount + 1);
|
|
|
|
}} />
|
2023-03-07 14:24:50 -08:00
|
|
|
</Toolbar>
|
2023-03-07 17:20:41 -08:00
|
|
|
<TestList
|
2023-03-13 22:19:31 -07:00
|
|
|
statusFilters={statusFilters}
|
|
|
|
projectFilters={projectFilters}
|
2023-03-09 21:45:57 -08:00
|
|
|
filterText={filterText}
|
2023-03-17 09:41:23 -07:00
|
|
|
testModel={testModel}
|
2023-03-12 10:42:02 -07:00
|
|
|
runningState={runningState}
|
2023-03-07 14:24:50 -08:00
|
|
|
runTests={runTests}
|
2023-03-19 12:04:19 -07:00
|
|
|
onItemSelected={setSelectedItem}
|
2023-03-19 14:50:09 -07:00
|
|
|
setVisibleTestIds={setVisibleTestIds}
|
2023-03-19 22:52:48 -07:00
|
|
|
watchAll={watchAll}
|
2023-03-20 17:12:02 -07:00
|
|
|
watchedTreeIds={watchedTreeIds}
|
|
|
|
setWatchedTreeIds={setWatchedTreeIds}
|
2023-04-19 18:16:18 -07:00
|
|
|
isLoading={isLoading}
|
|
|
|
requestedCollapseAllCount={collapseAllCount} />
|
2023-03-07 14:24:50 -08:00
|
|
|
</div>
|
|
|
|
</SplitView>
|
|
|
|
</div>;
|
2023-03-07 12:43:16 -08:00
|
|
|
};
|
|
|
|
|
2023-03-13 22:19:31 -07:00
|
|
|
const FiltersView: React.FC<{
|
|
|
|
filterText: string;
|
|
|
|
setFilterText: (text: string) => void;
|
|
|
|
statusFilters: Map<string, boolean>;
|
|
|
|
setStatusFilters: (filters: Map<string, boolean>) => void;
|
|
|
|
projectFilters: Map<string, boolean>;
|
|
|
|
setProjectFilters: (filters: Map<string, boolean>) => void;
|
2023-03-17 09:41:23 -07:00
|
|
|
testModel: TestModel | undefined,
|
2023-03-13 22:19:31 -07:00
|
|
|
runTests: () => void;
|
2023-03-17 09:41:23 -07:00
|
|
|
}> = ({ filterText, setFilterText, statusFilters, setStatusFilters, projectFilters, setProjectFilters, testModel, runTests }) => {
|
2023-03-13 22:19:31 -07:00
|
|
|
const [expanded, setExpanded] = React.useState(false);
|
|
|
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
React.useEffect(() => {
|
|
|
|
inputRef.current?.focus();
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const statusLine = [...statusFilters.entries()].filter(([_, v]) => v).map(([s]) => s).join(' ') || 'all';
|
|
|
|
const projectsLine = [...projectFilters.entries()].filter(([_, v]) => v).map(([p]) => p).join(' ') || 'all';
|
|
|
|
return <div className='filters'>
|
|
|
|
<Expandable
|
|
|
|
expanded={expanded}
|
|
|
|
setExpanded={setExpanded}
|
|
|
|
title={<input ref={inputRef} type='search' placeholder='Filter (e.g. text, @tag)' spellCheck={false} value={filterText}
|
|
|
|
onChange={e => {
|
|
|
|
setFilterText(e.target.value);
|
|
|
|
}}
|
|
|
|
onKeyDown={e => {
|
|
|
|
if (e.key === 'Enter')
|
|
|
|
runTests();
|
|
|
|
}} />}>
|
|
|
|
</Expandable>
|
2023-03-20 20:45:32 -07:00
|
|
|
<div className='filter-summary' title={'Status: ' + statusLine + '\nProjects: ' + projectsLine} onClick={() => setExpanded(!expanded)}>
|
2023-03-13 22:19:31 -07:00
|
|
|
<span className='filter-label'>Status:</span> {statusLine}
|
|
|
|
<span className='filter-label'>Projects:</span> {projectsLine}
|
2023-03-20 20:45:32 -07:00
|
|
|
</div>
|
|
|
|
{expanded && <div className='hbox' style={{ marginLeft: 14 }}>
|
|
|
|
<div className='filter-list'>
|
|
|
|
{[...statusFilters.entries()].map(([status, value]) => {
|
|
|
|
return <div className='filter-entry'>
|
|
|
|
<label>
|
|
|
|
<input type='checkbox' checked={value} onClick={() => {
|
|
|
|
const copy = new Map(statusFilters);
|
|
|
|
copy.set(status, !copy.get(status));
|
|
|
|
setStatusFilters(copy);
|
|
|
|
}}/>
|
|
|
|
<div>{status}</div>
|
|
|
|
</label>
|
|
|
|
</div>;
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
<div className='filter-list'>
|
|
|
|
{[...projectFilters.entries()].map(([projectName, value]) => {
|
|
|
|
return <div className='filter-entry'>
|
|
|
|
<label>
|
|
|
|
<input type='checkbox' checked={value} onClick={() => {
|
|
|
|
const copy = new Map(projectFilters);
|
|
|
|
copy.set(projectName, !copy.get(projectName));
|
|
|
|
setProjectFilters(copy);
|
|
|
|
const configFile = testModel?.config?.configFile;
|
|
|
|
if (configFile)
|
|
|
|
settings.setObject(configFile + ':projects', [...copy.entries()].filter(([_, v]) => v).map(([k]) => k));
|
|
|
|
}}/>
|
|
|
|
<div>{projectName}</div>
|
|
|
|
</label>
|
|
|
|
</div>;
|
|
|
|
})}
|
|
|
|
</div>
|
2023-03-13 22:19:31 -07:00
|
|
|
</div>}
|
|
|
|
</div>;
|
|
|
|
};
|
|
|
|
|
2023-03-13 12:14:51 -07:00
|
|
|
const TestTreeView = TreeView<TreeItem>;
|
2023-03-08 17:33:27 -08:00
|
|
|
|
2023-03-13 22:19:31 -07:00
|
|
|
const TestList: React.FC<{
|
|
|
|
statusFilters: Map<string, boolean>,
|
|
|
|
projectFilters: Map<string, boolean>,
|
2023-03-09 21:45:57 -08:00
|
|
|
filterText: string,
|
2023-03-17 09:41:23 -07:00
|
|
|
testModel: { rootSuite: Suite | undefined, config: FullConfig | undefined },
|
2023-03-20 13:45:35 -07:00
|
|
|
runTests: (mode: 'bounce-if-busy' | 'queue-if-busy', testIds: Set<string>) => void,
|
2023-03-12 10:42:02 -07:00
|
|
|
runningState?: { testIds: Set<string>, itemSelectedByUser?: boolean },
|
2023-03-20 17:12:02 -07:00
|
|
|
watchAll: boolean,
|
|
|
|
watchedTreeIds: { value: Set<string> },
|
|
|
|
setWatchedTreeIds: (ids: { value: Set<string> }) => void,
|
2023-03-19 22:52:48 -07:00
|
|
|
isLoading?: boolean,
|
2023-03-20 13:45:35 -07:00
|
|
|
setVisibleTestIds: (testIds: Set<string>) => void,
|
2023-03-19 12:04:19 -07:00
|
|
|
onItemSelected: (item: { testCase?: TestCase, location?: Location }) => void,
|
2023-04-19 18:16:18 -07:00
|
|
|
requestedCollapseAllCount: number,
|
|
|
|
}> = ({ statusFilters, projectFilters, filterText, testModel, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, setVisibleTestIds, requestedCollapseAllCount }) => {
|
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-04-19 18:16:18 -07:00
|
|
|
const [collapseAllCount, setCollapseAllCount] = React.useState(requestedCollapseAllCount);
|
2023-03-01 15:27:23 -08:00
|
|
|
|
2023-03-19 14:50:09 -07:00
|
|
|
// Build the test tree.
|
|
|
|
const { rootItem, treeItemMap, fileNames } = React.useMemo(() => {
|
2023-03-23 13:29:52 -07:00
|
|
|
let rootItem = createTree(testModel.rootSuite, projectFilters);
|
2023-03-20 17:12:02 -07:00
|
|
|
filterTree(rootItem, filterText, statusFilters, runningState?.testIds);
|
2023-03-23 13:29:52 -07:00
|
|
|
sortAndPropagateStatus(rootItem);
|
|
|
|
rootItem = shortenRoot(rootItem);
|
|
|
|
|
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>();
|
2023-03-19 14:50:09 -07:00
|
|
|
const fileNames = new Set<string>();
|
2023-03-04 16:28:30 -08:00
|
|
|
const visit = (treeItem: TreeItem) => {
|
2023-03-19 14:50:09 -07:00
|
|
|
if (treeItem.kind === 'group' && treeItem.location.file)
|
|
|
|
fileNames.add(treeItem.location.file);
|
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-20 13:45:35 -07:00
|
|
|
setVisibleTestIds(visibleTestIds);
|
2023-03-19 14:50:09 -07:00
|
|
|
return { rootItem, treeItemMap, fileNames };
|
2023-03-20 17:12:02 -07:00
|
|
|
}, [filterText, testModel, statusFilters, projectFilters, setVisibleTestIds, runningState]);
|
2023-03-04 19:39:55 -08:00
|
|
|
|
2023-03-19 14:50:09 -07:00
|
|
|
// Look for a first failure within the run batch to select it.
|
2023-03-12 10:42:02 -07:00
|
|
|
React.useEffect(() => {
|
2023-04-19 18:16:18 -07:00
|
|
|
// If collapse was requested, clear the expanded items and return w/o selected item.
|
|
|
|
if (collapseAllCount !== requestedCollapseAllCount) {
|
|
|
|
treeState.expandedItems.clear();
|
|
|
|
for (const item of treeItemMap.keys())
|
|
|
|
treeState.expandedItems.set(item, false);
|
|
|
|
setCollapseAllCount(requestedCollapseAllCount);
|
|
|
|
setSelectedTreeItemId(undefined);
|
|
|
|
setTreeState({ ...treeState });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-03-12 10:42:02 -07:00
|
|
|
if (!runningState || runningState.itemSelectedByUser)
|
|
|
|
return;
|
|
|
|
let selectedTreeItem: TreeItem | undefined;
|
|
|
|
const visit = (treeItem: TreeItem) => {
|
2023-03-13 22:19:31 -07:00
|
|
|
treeItem.children.forEach(visit);
|
2023-03-12 10:42:02 -07:00
|
|
|
if (selectedTreeItem)
|
|
|
|
return;
|
|
|
|
if (treeItem.status === 'failed') {
|
|
|
|
if (treeItem.kind === 'test' && runningState.testIds.has(treeItem.test.id))
|
|
|
|
selectedTreeItem = treeItem;
|
|
|
|
else if (treeItem.kind === 'case' && runningState.testIds.has(treeItem.tests[0]?.id))
|
|
|
|
selectedTreeItem = treeItem;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
visit(rootItem);
|
|
|
|
|
|
|
|
if (selectedTreeItem)
|
|
|
|
setSelectedTreeItemId(selectedTreeItem.id);
|
2023-04-19 18:16:18 -07:00
|
|
|
}, [runningState, setSelectedTreeItemId, rootItem, collapseAllCount, setCollapseAllCount, requestedCollapseAllCount, treeState, setTreeState, treeItemMap]);
|
2023-03-12 10:42:02 -07:00
|
|
|
|
2023-03-19 14:50:09 -07:00
|
|
|
// Compute selected item.
|
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-19 12:04:19 -07:00
|
|
|
const location = selectedTreeItem?.location;
|
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];
|
2023-03-19 12:04:19 -07:00
|
|
|
onItemSelected({ testCase: selectedTest, location });
|
2023-03-08 17:33:27 -08:00
|
|
|
return { selectedTreeItem };
|
2023-03-19 12:04:19 -07:00
|
|
|
}, [onItemSelected, selectedTreeItemId, treeItemMap]);
|
2023-03-02 13:45:15 -08:00
|
|
|
|
2023-03-19 14:50:09 -07:00
|
|
|
// Update watch all.
|
|
|
|
React.useEffect(() => {
|
|
|
|
if (watchAll) {
|
|
|
|
sendMessageNoReply('watch', { fileNames: [...fileNames] });
|
|
|
|
} else {
|
|
|
|
const fileNames = new Set<string>();
|
|
|
|
for (const itemId of watchedTreeIds.value) {
|
2023-03-21 12:03:26 -07:00
|
|
|
const treeItem = treeItemMap.get(itemId);
|
|
|
|
const fileName = treeItem?.location.file;
|
2023-03-19 14:50:09 -07:00
|
|
|
if (fileName)
|
|
|
|
fileNames.add(fileName);
|
|
|
|
}
|
|
|
|
sendMessageNoReply('watch', { fileNames: [...fileNames] });
|
2023-03-12 10:50:21 -07:00
|
|
|
}
|
2023-03-19 14:50:09 -07:00
|
|
|
}, [rootItem, fileNames, watchAll, watchedTreeIds, treeItemMap]);
|
2023-03-07 15:07:52 -08:00
|
|
|
|
2023-03-04 16:28:30 -08:00
|
|
|
const runTreeItem = (treeItem: TreeItem) => {
|
|
|
|
setSelectedTreeItemId(treeItem.id);
|
2023-03-20 13:45:35 -07:00
|
|
|
runTests('bounce-if-busy', collectTestIds(treeItem));
|
2023-03-04 15:05:41 -08:00
|
|
|
};
|
|
|
|
|
2023-03-20 13:45:35 -07:00
|
|
|
runWatchedTests = (changedTestFiles: string[]) => {
|
2023-03-12 10:50:21 -07:00
|
|
|
const testIds: string[] = [];
|
2023-03-20 13:45:35 -07:00
|
|
|
const set = new Set(changedTestFiles);
|
2023-03-19 14:50:09 -07:00
|
|
|
if (watchAll) {
|
|
|
|
const visit = (treeItem: TreeItem) => {
|
2023-03-20 13:45:35 -07:00
|
|
|
const fileName = treeItem.location.file;
|
2023-03-19 14:50:09 -07:00
|
|
|
if (fileName && set.has(fileName))
|
|
|
|
testIds.push(...collectTestIds(treeItem));
|
2023-03-20 13:45:35 -07:00
|
|
|
if (treeItem.kind === 'group' && treeItem.subKind === 'folder')
|
|
|
|
treeItem.children.forEach(visit);
|
2023-03-19 14:50:09 -07:00
|
|
|
};
|
|
|
|
visit(rootItem);
|
|
|
|
} else {
|
|
|
|
for (const treeId of watchedTreeIds.value) {
|
2023-03-21 12:03:26 -07:00
|
|
|
const treeItem = treeItemMap.get(treeId);
|
|
|
|
const fileName = treeItem?.location.file;
|
2023-03-19 14:50:09 -07:00
|
|
|
if (fileName && set.has(fileName))
|
|
|
|
testIds.push(...collectTestIds(treeItem));
|
|
|
|
}
|
2023-03-12 10:50:21 -07:00
|
|
|
}
|
2023-03-20 13:45:35 -07:00
|
|
|
runTests('queue-if-busy', new Set(testIds));
|
2023-03-04 15:05:41 -08:00
|
|
|
};
|
|
|
|
|
2023-03-13 12:14:51 -07:00
|
|
|
return <TestTreeView
|
2023-03-09 21:45:57 -08:00
|
|
|
treeState={treeState}
|
|
|
|
setTreeState={setTreeState}
|
|
|
|
rootItem={rootItem}
|
2023-03-12 15:18:47 -07:00
|
|
|
dataTestId='test-tree'
|
2023-03-09 21:45:57 -08:00
|
|
|
render={treeItem => {
|
2023-04-19 16:51:42 -07:00
|
|
|
return <div className='hbox ui-mode-list-item'>
|
|
|
|
<div className='ui-mode-list-item-title'>{treeItem.title}</div>
|
|
|
|
{!!treeItem.duration && treeItem.status !== 'skipped' && <div className='ui-mode-list-item-time'>{msToString(treeItem.duration)}</div>}
|
2023-03-20 20:45:32 -07:00
|
|
|
<Toolbar noMinHeight={true} noShadow={true}>
|
|
|
|
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={!!runningState}></ToolbarButton>
|
|
|
|
<ToolbarButton icon='go-to-file' title='Open in VS Code' onClick={() => sendMessageNoReply('open', { location: locationToOpen(treeItem) })}></ToolbarButton>
|
|
|
|
{!watchAll && <ToolbarButton icon='eye' title='Watch' onClick={() => {
|
|
|
|
if (watchedTreeIds.value.has(treeItem.id))
|
|
|
|
watchedTreeIds.value.delete(treeItem.id);
|
|
|
|
else
|
|
|
|
watchedTreeIds.value.add(treeItem.id);
|
|
|
|
setWatchedTreeIds({ ...watchedTreeIds });
|
|
|
|
}} toggled={watchedTreeIds.value.has(treeItem.id)}></ToolbarButton>}
|
|
|
|
</Toolbar>
|
2023-03-09 21:45:57 -08:00
|
|
|
</div>;
|
|
|
|
}}
|
|
|
|
icon={treeItem => {
|
2023-03-20 17:12:02 -07:00
|
|
|
if (treeItem.status === 'scheduled')
|
|
|
|
return 'codicon-clock';
|
2023-03-09 21:45:57 -08:00
|
|
|
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 => {
|
2023-03-12 10:42:02 -07:00
|
|
|
if (runningState)
|
|
|
|
runningState.itemSelectedByUser = true;
|
2023-03-09 21:45:57 -08:00
|
|
|
setSelectedTreeItemId(treeItem.id);
|
|
|
|
}}
|
2023-03-13 12:14:51 -07:00
|
|
|
autoExpandDeep={!!filterText}
|
2023-03-19 22:52:48 -07:00
|
|
|
noItemsMessage={isLoading ? 'Loading\u2026' : 'No tests'} />;
|
2023-03-07 14:24:50 -08:00
|
|
|
};
|
|
|
|
|
2023-03-15 11:17:03 -07:00
|
|
|
const TraceView: React.FC<{
|
2023-03-19 12:04:19 -07:00
|
|
|
item: { location?: Location, testCase?: TestCase },
|
|
|
|
rootDir?: string,
|
|
|
|
}> = ({ item, rootDir }) => {
|
2023-03-01 15:27:23 -08:00
|
|
|
const [model, setModel] = React.useState<MultiTraceModel | undefined>();
|
2023-03-15 22:33:40 -07:00
|
|
|
const [counter, setCounter] = React.useState(0);
|
2023-03-15 11:17:03 -07:00
|
|
|
const pollTimer = React.useRef<NodeJS.Timeout | null>(null);
|
2023-03-01 15:27:23 -08:00
|
|
|
|
2023-03-19 12:04:19 -07:00
|
|
|
const { outputDir, result } = React.useMemo(() => {
|
|
|
|
const outputDir = item.testCase ? outputDirForTestCase(item.testCase) : undefined;
|
|
|
|
const result = item.testCase?.results[0];
|
|
|
|
return { outputDir, result };
|
|
|
|
}, [item]);
|
|
|
|
|
2023-03-31 18:34:51 -07:00
|
|
|
// Preserve user selection upon live-reloading trace model by persisting the action id.
|
|
|
|
// This avoids auto-selection of the last action every time we reload the model.
|
|
|
|
const [selectedActionId, setSelectedActionId] = React.useState<string | undefined>();
|
|
|
|
const onSelectionChanged = React.useCallback((action: ActionTraceEvent) => setSelectedActionId(idForAction(action)), [setSelectedActionId]);
|
|
|
|
const initialSelection = selectedActionId ? model?.actions.find(a => idForAction(a) === selectedActionId) : undefined;
|
|
|
|
|
2023-03-01 15:27:23 -08:00
|
|
|
React.useEffect(() => {
|
2023-03-15 11:17:03 -07:00
|
|
|
if (pollTimer.current)
|
|
|
|
clearTimeout(pollTimer.current);
|
2023-03-09 20:02:42 -08:00
|
|
|
|
2023-03-15 22:33:40 -07:00
|
|
|
if (!result) {
|
|
|
|
setModel(undefined);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-03-09 20:02:42 -08:00
|
|
|
// Test finished.
|
2023-03-15 22:33:40 -07:00
|
|
|
const attachment = result && result.duration >= 0 && result.attachments.find(a => a.name === 'trace');
|
|
|
|
if (attachment && attachment.path) {
|
|
|
|
loadSingleTraceFile(attachment.path).then(model => setModel(model));
|
2023-03-15 11:17:03 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-03-16 20:09:09 -07:00
|
|
|
if (!outputDir) {
|
|
|
|
setModel(undefined);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-03-19 12:04:19 -07:00
|
|
|
const traceLocation = `${outputDir}/${artifactsFolderName(result!.workerIndex)}/traces/${item.testCase?.id}.json`;
|
2023-03-15 11:17:03 -07:00
|
|
|
// Start polling running test.
|
2023-03-15 22:33:40 -07:00
|
|
|
pollTimer.current = setTimeout(async () => {
|
|
|
|
try {
|
|
|
|
const model = await loadSingleTraceFile(traceLocation);
|
|
|
|
setModel(model);
|
|
|
|
} catch {
|
|
|
|
setModel(undefined);
|
|
|
|
} finally {
|
|
|
|
setCounter(counter + 1);
|
|
|
|
}
|
2023-04-19 07:29:28 -07:00
|
|
|
}, 500);
|
2023-03-15 22:33:40 -07:00
|
|
|
return () => {
|
|
|
|
if (pollTimer.current)
|
|
|
|
clearTimeout(pollTimer.current);
|
|
|
|
};
|
2023-03-19 12:04:19 -07:00
|
|
|
}, [result, outputDir, item, setModel, counter, setCounter]);
|
|
|
|
|
|
|
|
return <Workbench
|
|
|
|
key='workbench'
|
|
|
|
model={model}
|
|
|
|
hideTimelineBars={true}
|
|
|
|
hideStackFrames={true}
|
|
|
|
showSourcesFirst={true}
|
|
|
|
rootDir={rootDir}
|
2023-03-31 18:34:51 -07:00
|
|
|
initialSelection={initialSelection}
|
|
|
|
onSelectionChanged={onSelectionChanged}
|
2023-03-19 12:04:19 -07:00
|
|
|
defaultSourceLocation={item.location} />;
|
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;
|
2023-03-17 09:41:23 -07:00
|
|
|
let throttleData: { config: FullConfig, rootSuite: Suite, progress: Progress } | undefined;
|
2023-03-10 17:01:19 -08:00
|
|
|
const throttledAction = () => {
|
|
|
|
clearTimeout(throttleTimer);
|
|
|
|
throttleTimer = undefined;
|
2023-03-17 09:41:23 -07:00
|
|
|
updateRootSuite(throttleData!.config, throttleData!.rootSuite, throttleData!.progress);
|
2023-03-10 17:01:19 -08:00
|
|
|
};
|
|
|
|
|
2023-03-17 09:41:23 -07:00
|
|
|
const throttleUpdateRootSuite = (config: FullConfig, rootSuite: Suite, progress: Progress, immediate = false) => {
|
|
|
|
throttleData = { config, rootSuite, progress };
|
2023-03-10 17:01:19 -08:00
|
|
|
if (immediate)
|
|
|
|
throttledAction();
|
|
|
|
else if (!throttleTimer)
|
|
|
|
throttleTimer = setTimeout(throttledAction, 250);
|
|
|
|
};
|
|
|
|
|
2023-03-13 22:19:31 -07:00
|
|
|
const refreshRootSuite = (eraseResults: boolean): Promise<void> => {
|
|
|
|
if (!eraseResults)
|
|
|
|
return sendMessage('list', {});
|
2023-03-07 20:34:57 -08:00
|
|
|
|
2023-03-04 19:39:55 -08:00
|
|
|
let rootSuite: Suite;
|
2023-03-05 13:46:21 -08:00
|
|
|
const progress: Progress = {
|
|
|
|
passed: 0,
|
|
|
|
failed: 0,
|
2023-03-09 21:45:57 -08:00
|
|
|
skipped: 0,
|
2023-03-05 13:46:21 -08:00
|
|
|
};
|
2023-03-17 09:41:23 -07:00
|
|
|
let config: FullConfig;
|
2023-03-22 13:59:57 -07:00
|
|
|
receiver = new TeleReporterReceiver(pathSeparator, {
|
2023-03-17 09:41:23 -07:00
|
|
|
onBegin: (c: FullConfig, suite: Suite) => {
|
2023-03-04 19:39:55 -08:00
|
|
|
if (!rootSuite)
|
|
|
|
rootSuite = suite;
|
2023-03-17 09:41:23 -07:00
|
|
|
config = c;
|
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-17 09:41:23 -07:00
|
|
|
throttleUpdateRootSuite(config, rootSuite, progress, true);
|
2023-03-10 17:01:19 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
onEnd: () => {
|
2023-03-17 09:41:23 -07:00
|
|
|
throttleUpdateRootSuite(config, rootSuite, progress, true);
|
2023-03-04 19:39:55 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
onTestBegin: () => {
|
2023-03-17 09:41:23 -07:00
|
|
|
throttleUpdateRootSuite(config, 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-17 09:41:23 -07:00
|
|
|
throttleUpdateRootSuite(config, rootSuite, progress);
|
2023-03-04 19:39:55 -08:00
|
|
|
},
|
2023-03-21 12:03:26 -07:00
|
|
|
|
|
|
|
onError: (error: TestError) => {
|
|
|
|
xtermDataSource.write((error.stack || error.value || '') + '\n');
|
|
|
|
},
|
2023-03-04 19:39:55 -08:00
|
|
|
});
|
2023-04-06 08:33:17 -07:00
|
|
|
receiver._setClearPreviousResultsWhenTestBegins();
|
2023-03-13 22:19:31 -07:00
|
|
|
return sendMessage('list', {});
|
2023-03-05 13:46:21 -08:00
|
|
|
};
|
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') {
|
2023-03-13 22:19:31 -07:00
|
|
|
refreshRootSuite(false).catch(() => {});
|
2023-03-07 20:34:57 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-03-23 11:30:28 -07:00
|
|
|
if (message.method === 'testFilesChanged') {
|
|
|
|
runWatchedTests(message.params.testFileNames);
|
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
|
|
|
|
2023-04-05 14:23:06 -07:00
|
|
|
receiver?.dispatch(message)?.catch(() => {});
|
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) => {
|
2023-03-21 12:13:20 -07:00
|
|
|
if ((window as any)._overrideProtocolForTest) {
|
|
|
|
(window as any)._overrideProtocolForTest({ method, params }).catch(() => {});
|
|
|
|
return;
|
|
|
|
}
|
2023-03-04 15:05:41 -08:00
|
|
|
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-16 20:09:09 -07:00
|
|
|
const outputDirForTestCase = (testCase: TestCase): string | undefined => {
|
|
|
|
for (let suite: Suite | undefined = testCase.parent; suite; suite = suite.parent) {
|
|
|
|
if (suite.project())
|
|
|
|
return suite.project()?.outputDir;
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
};
|
|
|
|
|
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-20 13:45:35 -07:00
|
|
|
const collectTestIds = (treeItem?: TreeItem): Set<string> => {
|
|
|
|
const testIds = new Set<string>();
|
2023-03-04 16:28:30 -08:00
|
|
|
if (!treeItem)
|
2023-03-20 13:45:35 -07:00
|
|
|
return testIds;
|
|
|
|
|
2023-03-04 16:28:30 -08:00
|
|
|
const visit = (treeItem: TreeItem) => {
|
2023-03-08 17:33:27 -08:00
|
|
|
if (treeItem.kind === 'case')
|
2023-03-20 13:45:35 -07:00
|
|
|
treeItem.tests.map(t => t.id).forEach(id => testIds.add(id));
|
2023-03-08 17:33:27 -08:00
|
|
|
else if (treeItem.kind === 'test')
|
2023-03-20 13:45:35 -07:00
|
|
|
testIds.add(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 = {
|
|
|
|
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-20 17:12:02 -07:00
|
|
|
duration: number;
|
2023-03-12 10:42:02 -07:00
|
|
|
parent: TreeItem | undefined;
|
2023-03-08 17:33:27 -08:00
|
|
|
children: TreeItem[];
|
2023-03-20 17:12:02 -07:00
|
|
|
status: 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped';
|
2023-03-04 16:28:30 -08:00
|
|
|
};
|
|
|
|
|
2023-03-08 19:50:32 -08:00
|
|
|
type GroupItem = TreeItemBase & {
|
2023-03-19 18:51:09 -07:00
|
|
|
kind: 'group';
|
|
|
|
subKind: 'folder' | 'file' | 'describe';
|
2023-03-08 19:50:32 -08:00
|
|
|
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-19 18:51:09 -07:00
|
|
|
function getFileItem(rootItem: GroupItem, filePath: string[], isFile: boolean, fileItems: Map<string, GroupItem>): GroupItem {
|
|
|
|
if (filePath.length === 0)
|
|
|
|
return rootItem;
|
2023-03-19 22:52:48 -07:00
|
|
|
const fileName = filePath.join(pathSeparator);
|
2023-03-19 18:51:09 -07:00
|
|
|
const existingFileItem = fileItems.get(fileName);
|
|
|
|
if (existingFileItem)
|
|
|
|
return existingFileItem;
|
|
|
|
const parentFileItem = getFileItem(rootItem, filePath.slice(0, filePath.length - 1), false, fileItems);
|
|
|
|
const fileItem: GroupItem = {
|
|
|
|
kind: 'group',
|
|
|
|
subKind: isFile ? 'file' : 'folder',
|
|
|
|
id: fileName,
|
|
|
|
title: filePath[filePath.length - 1],
|
|
|
|
location: { file: fileName, line: 0, column: 0 },
|
2023-03-20 17:12:02 -07:00
|
|
|
duration: 0,
|
2023-03-19 18:51:09 -07:00
|
|
|
parent: parentFileItem,
|
|
|
|
children: [],
|
|
|
|
status: 'none',
|
|
|
|
};
|
|
|
|
parentFileItem.children.push(fileItem);
|
|
|
|
fileItems.set(fileName, fileItem);
|
|
|
|
return fileItem;
|
|
|
|
}
|
|
|
|
|
2023-03-13 22:19:31 -07:00
|
|
|
function createTree(rootSuite: Suite | undefined, projectFilters: Map<string, boolean>): GroupItem {
|
|
|
|
const filterProjects = [...projectFilters.values()].some(Boolean);
|
2023-03-08 19:50:32 -08:00
|
|
|
const rootItem: GroupItem = {
|
|
|
|
kind: 'group',
|
2023-03-19 18:51:09 -07:00
|
|
|
subKind: 'folder',
|
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-20 17:12:02 -07:00
|
|
|
duration: 0,
|
2023-03-12 10:42:02 -07:00
|
|
|
parent: undefined,
|
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) {
|
2023-03-24 17:09:11 -07:00
|
|
|
const title = suite.title || '<anonymous>';
|
2023-03-08 19:50:32 -08:00
|
|
|
let group = parentGroup.children.find(item => item.title === title) as GroupItem | undefined;
|
|
|
|
if (!group) {
|
|
|
|
group = {
|
|
|
|
kind: 'group',
|
2023-03-19 18:51:09 -07:00
|
|
|
subKind: 'describe',
|
2023-03-08 19:50:32 -08:00
|
|
|
id: parentGroup.id + '\x1e' + title,
|
|
|
|
title,
|
|
|
|
location: suite.location!,
|
2023-03-20 17:12:02 -07:00
|
|
|
duration: 0,
|
2023-03-12 10:42:02 -07:00
|
|
|
parent: parentGroup,
|
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-12 10:42:02 -07:00
|
|
|
parent: parentGroup,
|
2023-03-08 17:33:27 -08:00
|
|
|
children: [],
|
2023-03-08 19:50:32 -08:00
|
|
|
tests: [],
|
|
|
|
location: test.location,
|
2023-03-20 17:12:02 -07:00
|
|
|
duration: 0,
|
2023-03-08 19:50:32 -08:00
|
|
|
status: 'none',
|
|
|
|
};
|
|
|
|
parentGroup.children.push(testCaseItem);
|
2023-03-04 16:28:30 -08:00
|
|
|
}
|
2023-03-08 19:50:32 -08:00
|
|
|
|
2023-03-23 13:29:52 -07:00
|
|
|
const result = (test as TeleTestCase).results[0];
|
2023-03-20 17:12:02 -07:00
|
|
|
let status: 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped' = 'none';
|
2023-03-23 13:29:52 -07:00
|
|
|
if (result?.statusEx === 'scheduled')
|
2023-03-20 17:12:02 -07:00
|
|
|
status = 'scheduled';
|
2023-03-23 13:29:52 -07:00
|
|
|
else if (result?.statusEx === 'running')
|
2023-03-08 19:50:32 -08:00
|
|
|
status = 'running';
|
2023-03-23 13:29:52 -07:00
|
|
|
else if (result?.status === 'skipped')
|
2023-03-09 20:02:42 -08:00
|
|
|
status = 'skipped';
|
2023-03-23 13:29:52 -07:00
|
|
|
else if (result?.status === 'interrupted')
|
2023-03-17 14:10:25 -07:00
|
|
|
status = 'none';
|
2023-03-23 13:29:52 -07:00
|
|
|
else if (result && test.outcome() !== 'expected')
|
2023-03-08 19:50:32 -08:00
|
|
|
status = 'failed';
|
2023-03-23 13:29:52 -07:00
|
|
|
else if (result && 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,
|
2023-03-12 10:42:02 -07:00
|
|
|
parent: testCaseItem,
|
2023-03-08 19:50:32 -08:00
|
|
|
children: [],
|
|
|
|
status,
|
2023-03-20 17:12:02 -07:00
|
|
|
duration: test.results.length ? Math.max(0, test.results[0].duration) : 0,
|
|
|
|
project: projectName,
|
2023-03-08 19:50:32 -08:00
|
|
|
});
|
2023-03-20 17:12:02 -07:00
|
|
|
testCaseItem.duration = (testCaseItem.children as TestItem[]).reduce((a, b) => a + b.duration, 0);
|
2023-03-04 16:28:30 -08:00
|
|
|
}
|
2023-03-08 19:50:32 -08:00
|
|
|
};
|
|
|
|
|
2023-03-19 18:51:09 -07:00
|
|
|
const fileMap = new Map<string, GroupItem>();
|
2023-03-08 19:50:32 -08:00
|
|
|
for (const projectSuite of rootSuite?.suites || []) {
|
2023-03-13 22:19:31 -07:00
|
|
|
if (filterProjects && !projectFilters.get(projectSuite.title))
|
2023-03-08 19:50:32 -08:00
|
|
|
continue;
|
2023-03-19 18:51:09 -07:00
|
|
|
for (const fileSuite of projectSuite.suites) {
|
2023-03-19 22:52:48 -07:00
|
|
|
const fileItem = getFileItem(rootItem, fileSuite.location!.file.split(pathSeparator), true, fileMap);
|
2023-03-19 18:51:09 -07:00
|
|
|
visitSuite(projectSuite.title, fileSuite, fileItem);
|
|
|
|
}
|
2023-03-04 16:28:30 -08:00
|
|
|
}
|
2023-03-23 13:29:52 -07:00
|
|
|
return rootItem;
|
2023-03-04 16:28:30 -08:00
|
|
|
}
|
|
|
|
|
2023-03-20 17:12:02 -07:00
|
|
|
function filterTree(rootItem: GroupItem, filterText: string, statusFilters: Map<string, boolean>, runningTestIds: Set<string> | undefined) {
|
2023-03-13 22:19:31 -07:00
|
|
|
const tokens = filterText.trim().toLowerCase().split(' ');
|
|
|
|
const filtersStatuses = [...statusFilters.values()].some(Boolean);
|
2023-03-10 12:41:00 -08:00
|
|
|
|
|
|
|
const filter = (testCase: TestCaseItem) => {
|
|
|
|
const title = testCase.tests[0].titlePath().join(' ').toLowerCase();
|
2023-03-20 17:12:02 -07:00
|
|
|
if (!tokens.every(token => title.includes(token)) && !testCase.tests.some(t => runningTestIds?.has(t.id)))
|
2023-03-10 12:41:00 -08:00
|
|
|
return false;
|
2023-03-20 17:12:02 -07:00
|
|
|
testCase.children = (testCase.children as TestItem[]).filter(test => {
|
|
|
|
return !filtersStatuses || runningTestIds?.has(test.id) || statusFilters.get(test.status);
|
|
|
|
});
|
2023-03-10 12:41:00 -08:00
|
|
|
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-23 13:29:52 -07:00
|
|
|
function sortAndPropagateStatus(treeItem: TreeItem) {
|
|
|
|
for (const child of treeItem.children)
|
|
|
|
sortAndPropagateStatus(child);
|
|
|
|
|
|
|
|
if (treeItem.kind === 'group') {
|
|
|
|
treeItem.children.sort((a, b) => {
|
|
|
|
const fc = a.location.file.localeCompare(b.location.file);
|
|
|
|
return fc || a.location.line - b.location.line;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let allPassed = treeItem.children.length > 0;
|
|
|
|
let allSkipped = treeItem.children.length > 0;
|
|
|
|
let hasFailed = false;
|
|
|
|
let hasRunning = false;
|
|
|
|
let hasScheduled = false;
|
|
|
|
|
|
|
|
for (const child of treeItem.children) {
|
|
|
|
allSkipped = allSkipped && child.status === 'skipped';
|
|
|
|
allPassed = allPassed && (child.status === 'passed' || child.status === 'skipped');
|
|
|
|
hasFailed = hasFailed || child.status === 'failed';
|
|
|
|
hasRunning = hasRunning || child.status === 'running';
|
|
|
|
hasScheduled = hasScheduled || child.status === 'scheduled';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hasRunning)
|
|
|
|
treeItem.status = 'running';
|
|
|
|
else if (hasScheduled)
|
|
|
|
treeItem.status = 'scheduled';
|
|
|
|
else if (hasFailed)
|
|
|
|
treeItem.status = 'failed';
|
|
|
|
else if (allSkipped)
|
|
|
|
treeItem.status = 'skipped';
|
|
|
|
else if (allPassed)
|
|
|
|
treeItem.status = 'passed';
|
|
|
|
}
|
|
|
|
|
|
|
|
function shortenRoot(rootItem: GroupItem): GroupItem {
|
|
|
|
let shortRoot = rootItem;
|
|
|
|
while (shortRoot.children.length === 1 && shortRoot.children[0].kind === 'group' && shortRoot.children[0].subKind === 'folder')
|
|
|
|
shortRoot = shortRoot.children[0];
|
|
|
|
shortRoot.location = rootItem.location;
|
|
|
|
return shortRoot;
|
|
|
|
}
|
|
|
|
|
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-19 22:52:48 -07:00
|
|
|
|
|
|
|
const pathSeparator = navigator.userAgent.toLowerCase().includes('windows') ? '\\' : '/';
|