chore: open location in vscode (#21472)
This commit is contained in:
parent
de0b66c888
commit
f5894ed089
|
|
@ -28,6 +28,7 @@ import type { TaskRunnerState } from './tasks';
|
||||||
import { createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
import { createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
||||||
import { chokidar } from '../utilsBundle';
|
import { chokidar } from '../utilsBundle';
|
||||||
import type { FSWatcher } from 'chokidar';
|
import type { FSWatcher } from 'chokidar';
|
||||||
|
import { open } from '../utilsBundle';
|
||||||
|
|
||||||
class UIMode {
|
class UIMode {
|
||||||
private _config: FullConfigInternal;
|
private _config: FullConfigInternal;
|
||||||
|
|
@ -88,6 +89,8 @@ class UIMode {
|
||||||
this._stopTests();
|
this._stopTests();
|
||||||
if (method === 'watch')
|
if (method === 'watch')
|
||||||
this._watchFile(params.fileName);
|
this._watchFile(params.fileName);
|
||||||
|
if (method === 'open' && params.location)
|
||||||
|
open.openApp('code', { arguments: ['--goto', params.location] }).catch(() => {});
|
||||||
if (method === 'resizeTerminal') {
|
if (method === 'resizeTerminal') {
|
||||||
process.stdout.columns = params.cols;
|
process.stdout.columns = params.cols;
|
||||||
process.stdout.rows = params.rows;
|
process.stdout.rows = params.rows;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import '@web/common.css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ListView } from '@web/components/listView';
|
import { ListView } from '@web/components/listView';
|
||||||
import { TeleReporterReceiver } from '../../../playwright-test/src/isomorphic/teleReceiver';
|
import { TeleReporterReceiver } from '../../../playwright-test/src/isomorphic/teleReceiver';
|
||||||
import type { FullConfig, Suite, TestCase, TestResult, TestStep } from '../../../playwright-test/types/testReporter';
|
import type { FullConfig, Suite, TestCase, TestResult, TestStep, Location } from '../../../playwright-test/types/testReporter';
|
||||||
import { SplitView } from '@web/components/splitView';
|
import { SplitView } from '@web/components/splitView';
|
||||||
import { MultiTraceModel } from './modelUtil';
|
import { MultiTraceModel } from './modelUtil';
|
||||||
import './watchMode.css';
|
import './watchMode.css';
|
||||||
|
|
@ -46,7 +46,7 @@ const xtermDataSource: XtermDataSource = {
|
||||||
|
|
||||||
export const WatchModeView: React.FC<{}> = ({
|
export const WatchModeView: React.FC<{}> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [projectNames, setProjectNames] = React.useState<string[]>([]);
|
const [projects, setProjects] = React.useState<Map<string, boolean>>(new Map());
|
||||||
const [rootSuite, setRootSuite] = React.useState<{ value: Suite | undefined }>({ value: undefined });
|
const [rootSuite, setRootSuite] = React.useState<{ value: Suite | undefined }>({ value: undefined });
|
||||||
const [isRunningTest, setIsRunningTest] = React.useState<boolean>(false);
|
const [isRunningTest, setIsRunningTest] = React.useState<boolean>(false);
|
||||||
const [progress, setProgress] = React.useState<Progress>({ total: 0, passed: 0, failed: 0 });
|
const [progress, setProgress] = React.useState<Progress>({ total: 0, passed: 0, failed: 0 });
|
||||||
|
|
@ -55,9 +55,22 @@ export const WatchModeView: React.FC<{}> = ({
|
||||||
const [isWatchingFiles, setIsWatchingFiles] = React.useState<boolean>(true);
|
const [isWatchingFiles, setIsWatchingFiles] = React.useState<boolean>(true);
|
||||||
|
|
||||||
updateRootSuite = (rootSuite: Suite, { passed, failed }: Progress) => {
|
updateRootSuite = (rootSuite: Suite, { passed, failed }: Progress) => {
|
||||||
setRootSuite({ value: rootSuite });
|
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);
|
||||||
|
|
||||||
progress.passed = passed;
|
progress.passed = passed;
|
||||||
progress.failed = failed;
|
progress.failed = failed;
|
||||||
|
|
||||||
|
setRootSuite({ value: rootSuite });
|
||||||
|
setProjects(new Map(projects));
|
||||||
setProgress({ ...progress });
|
setProgress({ ...progress });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -69,11 +82,6 @@ export const WatchModeView: React.FC<{}> = ({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (projectNames.length === 0 && rootSuite.value?.suites.length)
|
|
||||||
setProjectNames([rootSuite.value?.suites[0].title]);
|
|
||||||
}, [projectNames, rootSuite]);
|
|
||||||
|
|
||||||
return <div className='vbox'>
|
return <div className='vbox'>
|
||||||
<SplitView sidebarSize={250} orientation='horizontal' sidebarIsFirst={true}>
|
<SplitView sidebarSize={250} orientation='horizontal' sidebarIsFirst={true}>
|
||||||
<TraceView testItem={selectedTestItem}></TraceView>
|
<TraceView testItem={selectedTestItem}></TraceView>
|
||||||
|
|
@ -87,14 +95,15 @@ export const WatchModeView: React.FC<{}> = ({
|
||||||
<div className='spacer'></div>
|
<div className='spacer'></div>
|
||||||
<ToolbarButton icon='gear' title='Toggle color mode' toggled={settingsVisible} onClick={() => { setSettingsVisible(!settingsVisible); }}></ToolbarButton>
|
<ToolbarButton icon='gear' title='Toggle color mode' toggled={settingsVisible} onClick={() => { setSettingsVisible(!settingsVisible); }}></ToolbarButton>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
{ !settingsVisible && <TestList
|
<TestList
|
||||||
projectNames={projectNames}
|
projects={projects}
|
||||||
rootSuite={rootSuite}
|
rootSuite={rootSuite}
|
||||||
isRunningTest={isRunningTest}
|
isRunningTest={isRunningTest}
|
||||||
isWatchingFiles={isWatchingFiles}
|
isWatchingFiles={isWatchingFiles}
|
||||||
runTests={runTests}
|
runTests={runTests}
|
||||||
onTestItemSelected={setSelectedTestItem} />}
|
onTestItemSelected={setSelectedTestItem}
|
||||||
{settingsVisible && <SettingsView projectNames={projectNames} setProjectNames={setProjectNames} onClose={() => setSettingsVisible(false)}></SettingsView>}
|
isVisible={!settingsVisible} />
|
||||||
|
{settingsVisible && <SettingsView projects={projects} setProjects={setProjects} onClose={() => setSettingsVisible(false)}></SettingsView>}
|
||||||
</div>
|
</div>
|
||||||
</SplitView>
|
</SplitView>
|
||||||
<div className='status-line'>
|
<div className='status-line'>
|
||||||
|
|
@ -104,13 +113,14 @@ export const WatchModeView: React.FC<{}> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TestList: React.FC<{
|
export const TestList: React.FC<{
|
||||||
projectNames: string[],
|
projects: Map<string, boolean>,
|
||||||
rootSuite: { value: Suite | undefined },
|
rootSuite: { value: Suite | undefined },
|
||||||
runTests: (testIds: string[]) => void,
|
runTests: (testIds: string[]) => void,
|
||||||
isRunningTest: boolean,
|
isRunningTest: boolean,
|
||||||
isWatchingFiles: boolean,
|
isWatchingFiles: boolean,
|
||||||
|
isVisible: boolean
|
||||||
onTestItemSelected: (test: TestItem | undefined) => void,
|
onTestItemSelected: (test: TestItem | undefined) => void,
|
||||||
}> = ({ projectNames, rootSuite, runTests, isRunningTest, isWatchingFiles, onTestItemSelected }) => {
|
}> = ({ projects, rootSuite, runTests, isRunningTest, isWatchingFiles, isVisible, onTestItemSelected }) => {
|
||||||
const [filterText, setFilterText] = React.useState<string>('');
|
const [filterText, setFilterText] = React.useState<string>('');
|
||||||
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
|
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
|
||||||
const [expandedItems, setExpandedItems] = React.useState<Map<string, boolean>>(new Map());
|
const [expandedItems, setExpandedItems] = React.useState<Map<string, boolean>>(new Map());
|
||||||
|
|
@ -122,7 +132,7 @@ export const TestList: React.FC<{
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { filteredItems, treeItemMap, visibleTestIds } = React.useMemo(() => {
|
const { filteredItems, treeItemMap, visibleTestIds } = React.useMemo(() => {
|
||||||
const treeItems = createTree(rootSuite.value, projectNames);
|
const treeItems = createTree(rootSuite.value, projects);
|
||||||
const filteredItems = filterTree(treeItems, filterText);
|
const filteredItems = filterTree(treeItems, filterText);
|
||||||
|
|
||||||
const treeItemMap = new Map<string, TreeItem>();
|
const treeItemMap = new Map<string, TreeItem>();
|
||||||
|
|
@ -135,7 +145,7 @@ export const TestList: React.FC<{
|
||||||
};
|
};
|
||||||
filteredItems.forEach(visit);
|
filteredItems.forEach(visit);
|
||||||
return { treeItemMap, visibleTestIds, filteredItems };
|
return { treeItemMap, visibleTestIds, filteredItems };
|
||||||
}, [filterText, rootSuite, projectNames]);
|
}, [filterText, rootSuite, projects]);
|
||||||
|
|
||||||
runVisibleTests = () => runTests([...visibleTestIds]);
|
runVisibleTests = () => runTests([...visibleTestIds]);
|
||||||
|
|
||||||
|
|
@ -170,6 +180,9 @@ export const TestList: React.FC<{
|
||||||
runTests(collectTestIds(selectedTreeItem));
|
runTests(collectTestIds(selectedTreeItem));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isVisible)
|
||||||
|
return <></>;
|
||||||
|
|
||||||
return <div className='vbox'>
|
return <div className='vbox'>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<input ref={inputRef} type='search' placeholder='Filter (e.g. text, @tag)' spellCheck={false} value={filterText}
|
<input ref={inputRef} type='search' placeholder='Filter (e.g. text, @tag)' spellCheck={false} value={filterText}
|
||||||
|
|
@ -188,6 +201,7 @@ export const TestList: React.FC<{
|
||||||
return <div className='hbox watch-mode-list-item'>
|
return <div className='hbox watch-mode-list-item'>
|
||||||
<div className='watch-mode-list-item-title'>{treeItem.title}</div>
|
<div className='watch-mode-list-item-title'>{treeItem.title}</div>
|
||||||
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={isRunningTest}></ToolbarButton>
|
<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>;
|
</div>;
|
||||||
}}
|
}}
|
||||||
itemIcon={(treeItem: TreeItem) => {
|
itemIcon={(treeItem: TreeItem) => {
|
||||||
|
|
@ -241,25 +255,24 @@ export const TestList: React.FC<{
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsView: React.FC<{
|
export const SettingsView: React.FC<{
|
||||||
projectNames: string[],
|
projects: Map<string, boolean>,
|
||||||
setProjectNames: (projectNames: string[]) => void,
|
setProjects: (projectNames: Map<string, boolean>) => void,
|
||||||
onClose: () => void,
|
onClose: () => void,
|
||||||
}> = ({ projectNames, setProjectNames, onClose }) => {
|
}> = ({ projects, setProjects, onClose }) => {
|
||||||
return <div className='vbox'>
|
return <div className='vbox'>
|
||||||
<div className='hbox' style={{ flex: 'none' }}>
|
<div className='hbox' style={{ flex: 'none' }}>
|
||||||
<div className='section-title' style={{ marginTop: 10 }}>Projects</div>
|
<div className='section-title' style={{ marginTop: 10 }}>Projects</div>
|
||||||
<div className='spacer'></div>
|
<div className='spacer'></div>
|
||||||
<ToolbarButton icon='close' title='Close settings' toggled={false} onClick={onClose}></ToolbarButton>
|
<ToolbarButton icon='close' title='Close settings' toggled={false} onClick={onClose}></ToolbarButton>
|
||||||
</div>
|
</div>
|
||||||
{projectNames.map(projectName => {
|
{[...projects.entries()].map(([projectName, value]) => {
|
||||||
return <div style={{ display: 'flex', alignItems: 'center', lineHeight: '24px' }}>
|
return <div style={{ display: 'flex', alignItems: 'center', lineHeight: '24px', cursor: 'pointer' }}>
|
||||||
<input id={`project-${projectName}`} type='checkbox' checked={projectNames.includes(projectName)} onClick={() => {
|
<input id={`project-${projectName}`} type='checkbox' checked={value} onClick={() => {
|
||||||
const copy = [...projectNames];
|
const copy = new Map(projects);
|
||||||
if (copy.includes(projectName))
|
copy.set(projectName, !copy.get(projectName));
|
||||||
copy.splice(copy.indexOf(projectName), 1);
|
if (![...copy.values()].includes(true))
|
||||||
else
|
copy.set(projectName, true);
|
||||||
copy.push(projectName);
|
setProjects(copy);
|
||||||
setProjectNames(copy);
|
|
||||||
}} style={{ margin: '0 5px 0 10px' }} />
|
}} style={{ margin: '0 5px 0 10px' }} />
|
||||||
<label htmlFor={`project-${projectName}`}>
|
<label htmlFor={`project-${projectName}`}>
|
||||||
{projectName}
|
{projectName}
|
||||||
|
|
@ -387,6 +400,17 @@ const fileName = (treeItem?: TreeItem): string | undefined => {
|
||||||
return fileName(treeItem.parent || undefined);
|
return fileName(treeItem.parent || undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const locationToOpen = (treeItem?: TreeItem) => {
|
||||||
|
if (!treeItem)
|
||||||
|
return;
|
||||||
|
if (treeItem.kind === 'test')
|
||||||
|
return treeItem.test.location.file + ':' + treeItem.test.location.line;
|
||||||
|
if (treeItem.kind === 'case')
|
||||||
|
return treeItem.location.file + ':' + treeItem.location.line;
|
||||||
|
if (treeItem.kind === 'file')
|
||||||
|
return treeItem.file;
|
||||||
|
};
|
||||||
|
|
||||||
const collectTestIds = (treeItem?: TreeItem): string[] => {
|
const collectTestIds = (treeItem?: TreeItem): string[] => {
|
||||||
if (!treeItem)
|
if (!treeItem)
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -422,6 +446,7 @@ type FileItem = TreeItemBase & {
|
||||||
|
|
||||||
type TestCaseItem = TreeItemBase & {
|
type TestCaseItem = TreeItemBase & {
|
||||||
kind: 'case',
|
kind: 'case',
|
||||||
|
location: Location,
|
||||||
};
|
};
|
||||||
|
|
||||||
type TestItem = TreeItemBase & {
|
type TestItem = TreeItemBase & {
|
||||||
|
|
@ -431,10 +456,10 @@ type TestItem = TreeItemBase & {
|
||||||
|
|
||||||
type TreeItem = FileItem | TestCaseItem | TestItem;
|
type TreeItem = FileItem | TestCaseItem | TestItem;
|
||||||
|
|
||||||
function createTree(rootSuite: Suite | undefined, projectNames: string[]): FileItem[] {
|
function createTree(rootSuite: Suite | undefined, projects: Map<string, boolean>): FileItem[] {
|
||||||
const fileItems = new Map<string, FileItem>();
|
const fileItems = new Map<string, FileItem>();
|
||||||
for (const projectSuite of rootSuite?.suites || []) {
|
for (const projectSuite of rootSuite?.suites || []) {
|
||||||
if (!projectNames.includes(projectSuite.title))
|
if (!projects.get(projectSuite.title))
|
||||||
continue;
|
continue;
|
||||||
for (const fileSuite of projectSuite.suites) {
|
for (const fileSuite of projectSuite.suites) {
|
||||||
const file = fileSuite.location!.file;
|
const file = fileSuite.location!.file;
|
||||||
|
|
@ -464,6 +489,7 @@ function createTree(rootSuite: Suite | undefined, projectNames: string[]): FileI
|
||||||
parent: fileItem,
|
parent: fileItem,
|
||||||
children: [],
|
children: [],
|
||||||
expanded: false,
|
expanded: false,
|
||||||
|
location: test.location,
|
||||||
};
|
};
|
||||||
fileItem.children!.push(testCaseItem);
|
fileItem.children!.push(testCaseItem);
|
||||||
}
|
}
|
||||||
|
|
@ -506,7 +532,7 @@ function flattenTree(fileItems: FileItem[], expandedItems: Map<string, boolean |
|
||||||
result.push(fileItem);
|
result.push(fileItem);
|
||||||
const expandState = expandedItems.get(fileItem.id);
|
const expandState = expandedItems.get(fileItem.id);
|
||||||
const autoExpandMatches = result.length < 100 && (hasFilter && expandState !== false);
|
const autoExpandMatches = result.length < 100 && (hasFilter && expandState !== false);
|
||||||
fileItem.expanded = expandState || autoExpandMatches;
|
fileItem.expanded = expandState || autoExpandMatches || fileItems.length < 10;
|
||||||
if (fileItem.expanded) {
|
if (fileItem.expanded) {
|
||||||
for (const testCaseItem of fileItem.children!) {
|
for (const testCaseItem of fileItem.children!) {
|
||||||
result.push(testCaseItem);
|
result.push(testCaseItem);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue