mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
Pulled out from https://github.com/microsoft/playwright/pull/31900 I stumbled over `React.Children`, because it's the first time I saw that used. https://react.dev/reference/react/Children lists `React.Children` it as "Legacy" and mentions it's uncommon. Also, the fact that SplitView only displays its first two children, and all others are silently discarded, can be a surprise to some. By separating things out into `sidebar` and `main`, not only do we give the two elements names (otherwise one needs to remember that sidebar is always the first child), but we also prevent any "third children" from being dropped.
542 lines
23 KiB
TypeScript
542 lines
23 KiB
TypeScript
/**
|
|
* 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';
|
|
import '@web/common.css';
|
|
import React from 'react';
|
|
import { TeleSuite } from '@testIsomorphic/teleReceiver';
|
|
import { TeleSuiteUpdater } from './teleSuiteUpdater';
|
|
import type { Progress } from './uiModeModel';
|
|
import type { TeleTestCase } from '@testIsomorphic/teleReceiver';
|
|
import type * as reporterTypes from 'playwright/types/testReporter';
|
|
import { SplitView } from '@web/components/splitView';
|
|
import type { SourceLocation } from './modelUtil';
|
|
import './uiModeView.css';
|
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
|
import { Toolbar } from '@web/components/toolbar';
|
|
import type { XtermDataSource } from '@web/components/xtermWrapper';
|
|
import { XtermWrapper } from '@web/components/xtermWrapper';
|
|
import { useDarkModeSetting } from '@web/theme';
|
|
import { clsx, settings, useSetting } from '@web/uiUtils';
|
|
import { statusEx, TestTree } from '@testIsomorphic/testTree';
|
|
import type { TreeItem } from '@testIsomorphic/testTree';
|
|
import { TestServerConnection } from '@testIsomorphic/testServerConnection';
|
|
import { pathSeparator } from './uiModeModel';
|
|
import type { TestModel } from './uiModeModel';
|
|
import { FiltersView } from './uiModeFiltersView';
|
|
import { TestListView } from './uiModeTestListView';
|
|
import { TraceView } from './uiModeTraceView';
|
|
import { SettingsView } from './settingsView';
|
|
|
|
let xtermSize = { cols: 80, rows: 24 };
|
|
const xtermDataSource: XtermDataSource = {
|
|
pending: [],
|
|
clear: () => {},
|
|
write: data => xtermDataSource.pending.push(data),
|
|
resize: () => {},
|
|
};
|
|
|
|
const searchParams = new URLSearchParams(window.location.search);
|
|
const guid = searchParams.get('ws');
|
|
const wsURL = new URL(`../${guid}`, window.location.toString());
|
|
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
|
|
const queryParams = {
|
|
args: searchParams.getAll('arg'),
|
|
grep: searchParams.get('grep') || undefined,
|
|
grepInvert: searchParams.get('grepInvert') || undefined,
|
|
projects: searchParams.getAll('project'),
|
|
workers: searchParams.get('workers') || undefined,
|
|
timeout: searchParams.has('timeout') ? +searchParams.get('timeout')! : undefined,
|
|
headed: searchParams.has('headed'),
|
|
outputDir: searchParams.get('outputDir') || undefined,
|
|
updateSnapshots: (searchParams.get('updateSnapshots') as 'all' | 'none' | 'missing' | undefined) || undefined,
|
|
reporters: searchParams.has('reporter') ? searchParams.getAll('reporter') : undefined,
|
|
};
|
|
if (queryParams.updateSnapshots && !['all', 'none', 'missing'].includes(queryParams.updateSnapshots))
|
|
queryParams.updateSnapshots = undefined;
|
|
|
|
const isMac = navigator.platform === 'MacIntel';
|
|
|
|
export const UIModeView: React.FC<{}> = ({
|
|
}) => {
|
|
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());
|
|
const [testModel, setTestModel] = React.useState<TestModel>();
|
|
const [progress, setProgress] = React.useState<Progress & { total: number } | undefined>();
|
|
const [selectedItem, setSelectedItem] = React.useState<{ treeItem?: TreeItem, testFile?: SourceLocation, testCase?: reporterTypes.TestCase }>({});
|
|
const [visibleTestIds, setVisibleTestIds] = React.useState<Set<string>>(new Set());
|
|
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
|
const [runningState, setRunningState] = React.useState<{ testIds: Set<string>, itemSelectedByUser?: boolean } | undefined>();
|
|
const [watchAll, setWatchAll] = useSetting<boolean>('watch-all', false);
|
|
const [watchedTreeIds, setWatchedTreeIds] = React.useState<{ value: Set<string> }>({ value: new Set() });
|
|
const commandQueue = React.useRef(Promise.resolve());
|
|
const runTestBacklog = React.useRef<Set<string>>(new Set());
|
|
const [collapseAllCount, setCollapseAllCount] = React.useState(0);
|
|
const [isDisconnected, setIsDisconnected] = React.useState(false);
|
|
const [hasBrowsers, setHasBrowsers] = React.useState(true);
|
|
const [testServerConnection, setTestServerConnection] = React.useState<TestServerConnection>();
|
|
const [teleSuiteUpdater, setTeleSuiteUpdater] = React.useState<TeleSuiteUpdater>();
|
|
const [settingsVisible, setSettingsVisible] = React.useState(false);
|
|
const [testingOptionsVisible, setTestingOptionsVisible] = React.useState(false);
|
|
const [revealSource, setRevealSource] = React.useState(false);
|
|
const onRevealSource = React.useCallback(() => setRevealSource(true), [setRevealSource]);
|
|
|
|
const [runWorkers, setRunWorkers] = React.useState(queryParams.workers);
|
|
const singleWorkerSetting = React.useMemo(() => {
|
|
return [
|
|
runWorkers === '1',
|
|
(value: boolean) => {
|
|
// When started with `--workers=1`, the setting allows to undo that.
|
|
// Otherwise, fallback to the cli `--workers=X` argument.
|
|
setRunWorkers(value ? '1' : (queryParams.workers === '1' ? undefined : queryParams.workers));
|
|
},
|
|
'Single worker',
|
|
] as const;
|
|
}, [runWorkers, setRunWorkers]);
|
|
|
|
const [runHeaded, setRunHeaded] = React.useState(queryParams.headed);
|
|
const showBrowserSetting = React.useMemo(() => [runHeaded, setRunHeaded, 'Show browser'] as const, [runHeaded, setRunHeaded]);
|
|
|
|
const [runUpdateSnapshots, setRunUpdateSnapshots] = React.useState(queryParams.updateSnapshots);
|
|
const updateSnapshotsSetting = React.useMemo(() => {
|
|
return [
|
|
runUpdateSnapshots === 'all',
|
|
(value: boolean) => setRunUpdateSnapshots(value ? 'all' : 'missing'),
|
|
'Update snapshots',
|
|
] as const;
|
|
}, [runUpdateSnapshots, setRunUpdateSnapshots]);
|
|
|
|
const [, , showRouteActionsSetting] = useSetting('show-route-actions', true, 'Show route actions');
|
|
|
|
const darkModeSetting = useDarkModeSetting();
|
|
|
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
const reloadTests = React.useCallback(() => {
|
|
setTestServerConnection(new TestServerConnection(wsURL.toString()));
|
|
}, []);
|
|
|
|
// Load tests on startup.
|
|
React.useEffect(() => {
|
|
inputRef.current?.focus();
|
|
setIsLoading(true);
|
|
reloadTests();
|
|
}, [reloadTests]);
|
|
|
|
// Wire server connection to the auxiliary UI features.
|
|
React.useEffect(() => {
|
|
if (!testServerConnection)
|
|
return;
|
|
const disposables = [
|
|
testServerConnection.onStdio(params => {
|
|
if (params.buffer) {
|
|
const data = atob(params.buffer);
|
|
xtermDataSource.write(data);
|
|
} else {
|
|
xtermDataSource.write(params.text!);
|
|
}
|
|
}),
|
|
testServerConnection.onClose(() => setIsDisconnected(true))
|
|
];
|
|
xtermDataSource.resize = (cols, rows) => {
|
|
xtermSize = { cols, rows };
|
|
testServerConnection.resizeTerminalNoReply({ cols, rows });
|
|
};
|
|
return () => {
|
|
for (const disposable of disposables)
|
|
disposable.dispose();
|
|
};
|
|
}, [testServerConnection]);
|
|
|
|
// This is the main routine, every time connection updates it starts the
|
|
// whole workflow.
|
|
React.useEffect(() => {
|
|
if (!testServerConnection)
|
|
return;
|
|
|
|
let throttleTimer: NodeJS.Timeout | undefined;
|
|
const teleSuiteUpdater = new TeleSuiteUpdater({
|
|
onUpdate: immediate => {
|
|
clearTimeout(throttleTimer);
|
|
throttleTimer = undefined;
|
|
if (immediate) {
|
|
setTestModel(teleSuiteUpdater.asModel());
|
|
} else if (!throttleTimer) {
|
|
throttleTimer = setTimeout(() => {
|
|
setTestModel(teleSuiteUpdater.asModel());
|
|
}, 250);
|
|
}
|
|
},
|
|
onError: error => {
|
|
xtermDataSource.write((error.stack || error.value || '') + '\n');
|
|
},
|
|
pathSeparator,
|
|
});
|
|
|
|
setTeleSuiteUpdater(teleSuiteUpdater);
|
|
|
|
setTestModel(undefined);
|
|
setIsLoading(true);
|
|
setWatchedTreeIds({ value: new Set() });
|
|
(async () => {
|
|
try {
|
|
await testServerConnection.initialize({
|
|
interceptStdio: true,
|
|
watchTestDirs: true
|
|
});
|
|
const { status, report } = await testServerConnection.runGlobalSetup({});
|
|
teleSuiteUpdater.processGlobalReport(report);
|
|
if (status !== 'passed')
|
|
return;
|
|
|
|
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert });
|
|
teleSuiteUpdater.processListReport(result.report);
|
|
|
|
testServerConnection.onReport(params => {
|
|
teleSuiteUpdater.processTestReportEvent(params);
|
|
});
|
|
|
|
const { hasBrowsers } = await testServerConnection.checkBrowsers({});
|
|
setHasBrowsers(hasBrowsers);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
})();
|
|
return () => {
|
|
clearTimeout(throttleTimer);
|
|
};
|
|
}, [testServerConnection]);
|
|
|
|
// Update project filter default values.
|
|
React.useEffect(() => {
|
|
if (!testModel)
|
|
return;
|
|
|
|
const { config, rootSuite } = testModel;
|
|
const selectedProjects = config.configFile ? settings.getObject<string[] | undefined>(config.configFile + ':projects', undefined) : undefined;
|
|
const newFilter = new Map(projectFilters);
|
|
for (const projectName of newFilter.keys()) {
|
|
if (!rootSuite.suites.find(s => s.title === projectName))
|
|
newFilter.delete(projectName);
|
|
}
|
|
for (const projectSuite of rootSuite.suites) {
|
|
if (!newFilter.has(projectSuite.title))
|
|
newFilter.set(projectSuite.title, !!selectedProjects?.includes(projectSuite.title));
|
|
}
|
|
if (!selectedProjects && newFilter.size && ![...newFilter.values()].includes(true))
|
|
newFilter.set(newFilter.entries().next().value[0], true);
|
|
if (projectFilters.size !== newFilter.size || [...projectFilters].some(([k, v]) => newFilter.get(k) !== v))
|
|
setProjectFilters(newFilter);
|
|
}, [projectFilters, testModel]);
|
|
|
|
// Update progress.
|
|
React.useEffect(() => {
|
|
if (runningState && testModel?.progress)
|
|
setProgress(testModel.progress);
|
|
else if (!testModel)
|
|
setProgress(undefined);
|
|
}, [testModel, runningState]);
|
|
|
|
// Test tree is built from the model and filters.
|
|
const { testTree } = React.useMemo(() => {
|
|
if (!testModel)
|
|
return { testTree: new TestTree('', new TeleSuite('', 'root'), [], projectFilters, pathSeparator) };
|
|
const testTree = new TestTree('', testModel.rootSuite, testModel.loadErrors, projectFilters, pathSeparator);
|
|
testTree.filterTree(filterText, statusFilters, runningState?.testIds);
|
|
testTree.sortAndPropagateStatus();
|
|
testTree.shortenRoot();
|
|
testTree.flattenForSingleProject();
|
|
setVisibleTestIds(testTree.testIds());
|
|
return { testTree };
|
|
}, [filterText, testModel, statusFilters, projectFilters, setVisibleTestIds, runningState]);
|
|
|
|
const runTests = React.useCallback((mode: 'queue-if-busy' | 'bounce-if-busy', testIds: Set<string>) => {
|
|
if (!testServerConnection || !testModel)
|
|
return;
|
|
if (mode === 'bounce-if-busy' && runningState)
|
|
return;
|
|
|
|
runTestBacklog.current = new Set([...runTestBacklog.current, ...testIds]);
|
|
commandQueue.current = commandQueue.current.then(async () => {
|
|
const testIds = runTestBacklog.current;
|
|
runTestBacklog.current = new Set();
|
|
if (!testIds.size)
|
|
return;
|
|
|
|
// Clear test results.
|
|
{
|
|
for (const test of testModel.rootSuite?.allTests() || []) {
|
|
if (testIds.has(test.id)) {
|
|
test.results = [];
|
|
const result = (test as TeleTestCase)._createTestResult('pending');
|
|
(result as any)[statusEx] = 'scheduled';
|
|
}
|
|
}
|
|
setTestModel({ ...testModel });
|
|
}
|
|
|
|
const time = ' [' + new Date().toLocaleTimeString() + ']';
|
|
xtermDataSource.write('\x1B[2m—'.repeat(Math.max(0, xtermSize.cols - time.length)) + time + '\x1B[22m');
|
|
setProgress({ total: 0, passed: 0, failed: 0, skipped: 0 });
|
|
setRunningState({ testIds });
|
|
|
|
await testServerConnection.runTests({
|
|
locations: queryParams.args,
|
|
grep: queryParams.grep,
|
|
grepInvert: queryParams.grepInvert,
|
|
testIds: [...testIds],
|
|
projects: [...projectFilters].filter(([_, v]) => v).map(([p]) => p),
|
|
workers: runWorkers,
|
|
timeout: queryParams.timeout,
|
|
headed: runHeaded,
|
|
outputDir: queryParams.outputDir,
|
|
updateSnapshots: runUpdateSnapshots,
|
|
reporters: queryParams.reporters,
|
|
trace: 'on',
|
|
});
|
|
// Clear pending tests in case of interrupt.
|
|
for (const test of testModel.rootSuite?.allTests() || []) {
|
|
if (test.results[0]?.duration === -1)
|
|
test.results = [];
|
|
}
|
|
setTestModel({ ...testModel });
|
|
setRunningState(undefined);
|
|
});
|
|
}, [projectFilters, runningState, testModel, testServerConnection, runWorkers, runHeaded, runUpdateSnapshots]);
|
|
|
|
React.useEffect(() => {
|
|
if (!testServerConnection || !teleSuiteUpdater)
|
|
return;
|
|
const disposable = testServerConnection.onTestFilesChanged(async params => {
|
|
// fetch the new list of tests
|
|
commandQueue.current = commandQueue.current.then(async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert });
|
|
teleSuiteUpdater.processListReport(result.report);
|
|
} catch (e) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(e);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
});
|
|
await commandQueue.current;
|
|
|
|
if (params.testFiles.length === 0)
|
|
return;
|
|
|
|
// run affected watched tests
|
|
const testModel = teleSuiteUpdater.asModel();
|
|
const testTree = new TestTree('', testModel.rootSuite, testModel.loadErrors, projectFilters, pathSeparator);
|
|
|
|
const testIds: string[] = [];
|
|
const set = new Set(params.testFiles);
|
|
if (watchAll) {
|
|
const visit = (treeItem: TreeItem) => {
|
|
const fileName = treeItem.location.file;
|
|
if (fileName && set.has(fileName))
|
|
testIds.push(...testTree.collectTestIds(treeItem));
|
|
if (treeItem.kind === 'group' && treeItem.subKind === 'folder')
|
|
treeItem.children.forEach(visit);
|
|
};
|
|
visit(testTree.rootItem);
|
|
} else {
|
|
for (const treeId of watchedTreeIds.value) {
|
|
const treeItem = testTree.treeItemById(treeId);
|
|
const fileName = treeItem?.location.file;
|
|
if (fileName && set.has(fileName))
|
|
testIds.push(...testTree.collectTestIds(treeItem));
|
|
}
|
|
}
|
|
runTests('queue-if-busy', new Set(testIds));
|
|
});
|
|
return () => disposable.dispose();
|
|
}, [runTests, testServerConnection, watchAll, watchedTreeIds, teleSuiteUpdater, projectFilters]);
|
|
|
|
// Shortcuts.
|
|
React.useEffect(() => {
|
|
if (!testServerConnection)
|
|
return;
|
|
const onShortcutEvent = (e: KeyboardEvent) => {
|
|
if (e.code === 'Backquote' && e.ctrlKey) {
|
|
e.preventDefault();
|
|
setIsShowingOutput(!isShowingOutput);
|
|
} else if (e.code === 'F5' && e.shiftKey) {
|
|
e.preventDefault();
|
|
testServerConnection?.stopTestsNoReply({});
|
|
} else if (e.code === 'F5') {
|
|
e.preventDefault();
|
|
runTests('bounce-if-busy', visibleTestIds);
|
|
}
|
|
};
|
|
addEventListener('keydown', onShortcutEvent);
|
|
return () => {
|
|
removeEventListener('keydown', onShortcutEvent);
|
|
};
|
|
}, [runTests, reloadTests, testServerConnection, visibleTestIds, isShowingOutput]);
|
|
|
|
const isRunningTest = !!runningState;
|
|
const dialogRef = React.useRef<HTMLDialogElement>(null);
|
|
const openInstallDialog = React.useCallback((e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dialogRef.current?.showModal();
|
|
}, []);
|
|
const closeInstallDialog = React.useCallback((e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dialogRef.current?.close();
|
|
}, []);
|
|
const installBrowsers = React.useCallback((e: React.MouseEvent) => {
|
|
closeInstallDialog(e);
|
|
setIsShowingOutput(true);
|
|
testServerConnection?.installBrowsers({}).then(async () => {
|
|
setIsShowingOutput(false);
|
|
const { hasBrowsers } = await testServerConnection?.checkBrowsers({});
|
|
setHasBrowsers(hasBrowsers);
|
|
});
|
|
}, [closeInstallDialog, testServerConnection]);
|
|
|
|
return <div className='vbox ui-mode'>
|
|
{!hasBrowsers && <dialog ref={dialogRef}>
|
|
<div className='title'><span className='codicon codicon-lightbulb'></span>Install browsers</div>
|
|
<div className='body'>
|
|
Playwright did not find installed browsers.
|
|
<br></br>
|
|
Would you like to run `playwright install`?
|
|
<br></br>
|
|
<button className='button' onClick={installBrowsers}>Install</button>
|
|
<button className='button secondary' onClick={closeInstallDialog}>Dismiss</button>
|
|
</div>
|
|
</dialog>}
|
|
{isDisconnected && <div className='disconnected'>
|
|
<div className='title'>UI Mode disconnected</div>
|
|
<div><a href='#' onClick={() => window.location.href = '/'}>Reload the page</a> to reconnect</div>
|
|
</div>}
|
|
<SplitView
|
|
sidebarSize={250}
|
|
minSidebarSize={150}
|
|
orientation='horizontal'
|
|
sidebarIsFirst={true}
|
|
settingName='testListSidebar'
|
|
main={<div className='vbox'>
|
|
<div className={clsx('vbox', !isShowingOutput && 'hidden')}>
|
|
<Toolbar>
|
|
<div className='section-title' style={{ flex: 'none' }}>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={clsx('vbox', isShowingOutput && 'hidden')}>
|
|
<TraceView
|
|
item={selectedItem}
|
|
rootDir={testModel?.config?.rootDir}
|
|
revealSource={revealSource}
|
|
onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })}
|
|
/>
|
|
</div>
|
|
</div>}
|
|
sidebar={<div className='vbox ui-mode-sidebar'>
|
|
<Toolbar noShadow={true} noMinHeight={true}>
|
|
<img src='playwright-logo.svg' alt='Playwright logo' />
|
|
<div className='section-title'>Playwright</div>
|
|
<ToolbarButton icon='refresh' title='Reload' onClick={() => reloadTests()} disabled={isRunningTest || isLoading}></ToolbarButton>
|
|
<ToolbarButton icon='terminal' title={'Toggle output — ' + (isMac ? '⌃`' : 'Ctrl + `')} toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }} />
|
|
{!hasBrowsers && <ToolbarButton icon='lightbulb-autofix' style={{ color: 'var(--vscode-list-warningForeground)' }} title='Playwright browsers are missing' onClick={openInstallDialog} />}
|
|
</Toolbar>
|
|
<FiltersView
|
|
filterText={filterText}
|
|
setFilterText={setFilterText}
|
|
statusFilters={statusFilters}
|
|
setStatusFilters={setStatusFilters}
|
|
projectFilters={projectFilters}
|
|
setProjectFilters={setProjectFilters}
|
|
testModel={testModel}
|
|
runTests={() => runTests('bounce-if-busy', visibleTestIds)} />
|
|
<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>}
|
|
<ToolbarButton icon='play' title='Run all — F5' onClick={() => runTests('bounce-if-busy', visibleTestIds)} disabled={isRunningTest || isLoading}></ToolbarButton>
|
|
<ToolbarButton icon='debug-stop' title={'Stop — ' + (isMac ? '⇧F5' : 'Shift + F5')} onClick={() => testServerConnection?.stopTests({})} disabled={!isRunningTest || isLoading}></ToolbarButton>
|
|
<ToolbarButton icon='eye' title='Watch all' toggled={watchAll} onClick={() => {
|
|
setWatchedTreeIds({ value: new Set() });
|
|
setWatchAll(!watchAll);
|
|
}}></ToolbarButton>
|
|
<ToolbarButton icon='collapse-all' title='Collapse all' onClick={() => {
|
|
setCollapseAllCount(collapseAllCount + 1);
|
|
}} />
|
|
</Toolbar>
|
|
<TestListView
|
|
filterText={filterText}
|
|
testModel={testModel}
|
|
testTree={testTree}
|
|
testServerConnection={testServerConnection}
|
|
runningState={runningState}
|
|
runTests={runTests}
|
|
onItemSelected={setSelectedItem}
|
|
watchAll={watchAll}
|
|
watchedTreeIds={watchedTreeIds}
|
|
setWatchedTreeIds={setWatchedTreeIds}
|
|
isLoading={isLoading}
|
|
requestedCollapseAllCount={collapseAllCount}
|
|
setFilterText={setFilterText}
|
|
onRevealSource={onRevealSource}
|
|
/>
|
|
<Toolbar noShadow={true} noMinHeight={true} className='settings-toolbar' onClick={() => setTestingOptionsVisible(!testingOptionsVisible)}>
|
|
<span
|
|
className={`codicon codicon-${testingOptionsVisible ? 'chevron-down' : 'chevron-right'}`}
|
|
style={{ marginLeft: 5 }}
|
|
title={testingOptionsVisible ? 'Hide Testing Options' : 'Show Testing Options'}
|
|
/>
|
|
<div className='section-title'>Testing Options</div>
|
|
</Toolbar>
|
|
{testingOptionsVisible && <SettingsView settings={[
|
|
singleWorkerSetting,
|
|
showBrowserSetting,
|
|
updateSnapshotsSetting,
|
|
]} />}
|
|
<Toolbar noShadow={true} noMinHeight={true} className='settings-toolbar' onClick={() => setSettingsVisible(!settingsVisible)}>
|
|
<span
|
|
className={`codicon codicon-${settingsVisible ? 'chevron-down' : 'chevron-right'}`}
|
|
style={{ marginLeft: 5 }}
|
|
title={settingsVisible ? 'Hide Settings' : 'Show Settings'}
|
|
/>
|
|
<div className='section-title'>Settings</div>
|
|
</Toolbar>
|
|
{settingsVisible && <SettingsView settings={[
|
|
darkModeSetting,
|
|
showRouteActionsSetting,
|
|
]} />}
|
|
</div>
|
|
}
|
|
/>
|
|
</div>;
|
|
};
|