mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 13:50:25 +03:00
chore: animate the running progress (#21554)
https://github.com/microsoft/playwright/issues/21541
This commit is contained in:
parent
0106a54e6e
commit
c3b4820f1e
@ -57,6 +57,15 @@
|
|||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
color: var(--vscode-statusBar-foreground);
|
color: var(--vscode-statusBar-foreground);
|
||||||
background-color: var(--vscode-statusBar-background);
|
background-color: var(--vscode-statusBar-background);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-line > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-view-entry:not(.selected):not(.highlighted) .toolbar-button {
|
.list-view-entry:not(.selected):not(.highlighted) .toolbar-button {
|
||||||
|
@ -36,7 +36,6 @@ import { XtermWrapper } from '@web/components/xtermWrapper';
|
|||||||
let updateRootSuite: (rootSuite: Suite, progress: Progress) => void = () => {};
|
let updateRootSuite: (rootSuite: Suite, progress: Progress) => void = () => {};
|
||||||
let updateStepsProgress: () => void = () => {};
|
let updateStepsProgress: () => void = () => {};
|
||||||
let runWatchedTests = () => {};
|
let runWatchedTests = () => {};
|
||||||
let runVisibleTests = () => {};
|
|
||||||
let xtermSize = { cols: 80, rows: 24 };
|
let xtermSize = { cols: 80, rows: 24 };
|
||||||
|
|
||||||
const xtermDataSource: XtermDataSource = {
|
const xtermDataSource: XtermDataSource = {
|
||||||
@ -54,12 +53,20 @@ export const WatchModeView: React.FC<{}> = ({
|
|||||||
const [projects, setProjects] = React.useState<Map<string, boolean>>(new Map());
|
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, skipped: 0 });
|
||||||
const [selectedTest, setSelectedTest] = React.useState<TestCase | undefined>(undefined);
|
const [selectedTest, setSelectedTest] = React.useState<TestCase | undefined>(undefined);
|
||||||
const [settingsVisible, setSettingsVisible] = React.useState<boolean>(false);
|
const [settingsVisible, setSettingsVisible] = React.useState<boolean>(false);
|
||||||
const [isWatchingFiles, setIsWatchingFiles] = React.useState<boolean>(true);
|
const [isWatchingFiles, setIsWatchingFiles] = React.useState<boolean>(true);
|
||||||
|
const [visibleTestIds, setVisibleTestIds] = React.useState<string[]>([]);
|
||||||
|
const [filterText, setFilterText] = React.useState<string>('');
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
updateRootSuite = (rootSuite: Suite, { passed, failed }: Progress) => {
|
React.useEffect(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
refreshRootSuite(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
updateRootSuite = (rootSuite: Suite, newProgress: Progress) => {
|
||||||
for (const projectName of projects.keys()) {
|
for (const projectName of projects.keys()) {
|
||||||
if (!rootSuite.suites.find(s => s.title === projectName))
|
if (!rootSuite.suites.find(s => s.title === projectName))
|
||||||
projects.delete(projectName);
|
projects.delete(projectName);
|
||||||
@ -71,12 +78,9 @@ export const WatchModeView: React.FC<{}> = ({
|
|||||||
if (![...projects.values()].includes(true))
|
if (![...projects.values()].includes(true))
|
||||||
projects.set(projects.entries().next().value[0], true);
|
projects.set(projects.entries().next().value[0], true);
|
||||||
|
|
||||||
progress.passed = passed;
|
|
||||||
progress.failed = failed;
|
|
||||||
|
|
||||||
setRootSuite({ value: rootSuite });
|
setRootSuite({ value: rootSuite });
|
||||||
setProjects(new Map(projects));
|
setProjects(new Map(projects));
|
||||||
setProgress({ ...progress });
|
setProgress(newProgress);
|
||||||
};
|
};
|
||||||
|
|
||||||
const runTests = (testIds: string[]) => {
|
const runTests = (testIds: string[]) => {
|
||||||
@ -92,7 +96,7 @@ export const WatchModeView: React.FC<{}> = ({
|
|||||||
|
|
||||||
const time = ' [' + new Date().toLocaleTimeString() + ']';
|
const time = ' [' + new Date().toLocaleTimeString() + ']';
|
||||||
xtermDataSource.write('\x1B[2m—'.repeat(Math.max(0, xtermSize.cols - time.length)) + time + '\x1B[22m');
|
xtermDataSource.write('\x1B[2m—'.repeat(Math.max(0, xtermSize.cols - time.length)) + time + '\x1B[22m');
|
||||||
setProgress({ total: testIds.length, passed: 0, failed: 0 });
|
setProgress({ total: testIds.length, passed: 0, failed: 0, skipped: 0 });
|
||||||
setIsRunningTest(true);
|
setIsRunningTest(true);
|
||||||
sendMessage('run', { testIds }).then(() => {
|
sendMessage('run', { testIds }).then(() => {
|
||||||
setIsRunningTest(false);
|
setIsRunningTest(false);
|
||||||
@ -106,26 +110,43 @@ export const WatchModeView: React.FC<{}> = ({
|
|||||||
<div className='vbox watch-mode-sidebar'>
|
<div className='vbox watch-mode-sidebar'>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<div className='section-title' style={{ cursor: 'pointer' }} onClick={() => setSettingsVisible(false)}>Tests</div>
|
<div className='section-title' style={{ cursor: 'pointer' }} onClick={() => setSettingsVisible(false)}>Tests</div>
|
||||||
<ToolbarButton icon='play' title='Run' onClick={() => runVisibleTests()} disabled={isRunningTest}></ToolbarButton>
|
<ToolbarButton icon='play' title='Run' onClick={() => runTests(visibleTestIds)} disabled={isRunningTest}></ToolbarButton>
|
||||||
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest}></ToolbarButton>
|
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest}></ToolbarButton>
|
||||||
<ToolbarButton icon='refresh' title='Reload' onClick={() => refreshRootSuite(true)} disabled={isRunningTest}></ToolbarButton>
|
<ToolbarButton icon='refresh' title='Reload' onClick={() => refreshRootSuite(true)} disabled={isRunningTest}></ToolbarButton>
|
||||||
<ToolbarButton icon='eye-watch' title='Watch' toggled={isWatchingFiles} onClick={() => setIsWatchingFiles(!isWatchingFiles)}></ToolbarButton>
|
<ToolbarButton icon='eye-watch' title='Watch' toggled={isWatchingFiles} onClick={() => setIsWatchingFiles(!isWatchingFiles)}></ToolbarButton>
|
||||||
<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>
|
||||||
|
<Toolbar>
|
||||||
|
<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(visibleTestIds);
|
||||||
|
}}></input>
|
||||||
|
</Toolbar>
|
||||||
<TestList
|
<TestList
|
||||||
projects={projects}
|
projects={projects}
|
||||||
|
filterText={filterText}
|
||||||
rootSuite={rootSuite}
|
rootSuite={rootSuite}
|
||||||
isRunningTest={isRunningTest}
|
isRunningTest={isRunningTest}
|
||||||
isWatchingFiles={isWatchingFiles}
|
isWatchingFiles={isWatchingFiles}
|
||||||
runTests={runTests}
|
runTests={runTests}
|
||||||
onTestSelected={setSelectedTest}
|
onTestSelected={setSelectedTest}
|
||||||
isVisible={!settingsVisible} />
|
isVisible={!settingsVisible}
|
||||||
|
setVisibleTestIds={setVisibleTestIds} />
|
||||||
{settingsVisible && <SettingsView projects={projects} setProjects={setProjects} onClose={() => setSettingsVisible(false)}></SettingsView>}
|
{settingsVisible && <SettingsView projects={projects} setProjects={setProjects} onClose={() => setSettingsVisible(false)}></SettingsView>}
|
||||||
</div>
|
</div>
|
||||||
</SplitView>
|
</SplitView>
|
||||||
<div className='status-line'>
|
<div className='status-line'>
|
||||||
Running: {progress.total} tests | {progress.passed} passed | {progress.failed} failed
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
@ -134,20 +155,19 @@ const TreeListView = TreeView<TreeItem>;
|
|||||||
|
|
||||||
export const TestList: React.FC<{
|
export const TestList: React.FC<{
|
||||||
projects: Map<string, boolean>,
|
projects: Map<string, boolean>,
|
||||||
|
filterText: string,
|
||||||
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
|
isVisible: boolean,
|
||||||
|
setVisibleTestIds: (testIds: string[]) => void,
|
||||||
onTestSelected: (test: TestCase | undefined) => void,
|
onTestSelected: (test: TestCase | undefined) => void,
|
||||||
}> = ({ projects, rootSuite, runTests, isRunningTest, isWatchingFiles, isVisible, onTestSelected }) => {
|
}> = ({ projects, filterText, rootSuite, runTests, isRunningTest, isWatchingFiles, isVisible, onTestSelected, setVisibleTestIds }) => {
|
||||||
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
||||||
const [filterText, setFilterText] = React.useState<string>('');
|
|
||||||
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
|
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
inputRef.current?.focus();
|
|
||||||
refreshRootSuite(true);
|
refreshRootSuite(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -164,9 +184,9 @@ export const TestList: React.FC<{
|
|||||||
treeItemMap.set(treeItem.id, treeItem);
|
treeItemMap.set(treeItem.id, treeItem);
|
||||||
};
|
};
|
||||||
visit(rootItem);
|
visit(rootItem);
|
||||||
runVisibleTests = () => runTests([...visibleTestIds]);
|
setVisibleTestIds([...visibleTestIds]);
|
||||||
return { rootItem, treeItemMap };
|
return { rootItem, treeItemMap };
|
||||||
}, [filterText, rootSuite, projects, runTests]);
|
}, [filterText, rootSuite, projects, setVisibleTestIds]);
|
||||||
|
|
||||||
const { selectedTreeItem } = React.useMemo(() => {
|
const { selectedTreeItem } = React.useMemo(() => {
|
||||||
const selectedTreeItem = selectedTreeItemId ? treeItemMap.get(selectedTreeItemId) : undefined;
|
const selectedTreeItem = selectedTreeItemId ? treeItemMap.get(selectedTreeItemId) : undefined;
|
||||||
@ -195,46 +215,34 @@ export const TestList: React.FC<{
|
|||||||
if (!isVisible)
|
if (!isVisible)
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|
||||||
return <div className='vbox'>
|
return <TreeListView
|
||||||
<Toolbar>
|
treeState={treeState}
|
||||||
<input ref={inputRef} type='search' placeholder='Filter (e.g. text, @tag)' spellCheck={false} value={filterText}
|
setTreeState={setTreeState}
|
||||||
onChange={e => {
|
rootItem={rootItem}
|
||||||
setFilterText(e.target.value);
|
render={treeItem => {
|
||||||
}}
|
return <div className='hbox watch-mode-list-item'>
|
||||||
onKeyDown={e => {
|
<div className='watch-mode-list-item-title'>{treeItem.title}</div>
|
||||||
if (e.key === 'Enter')
|
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={isRunningTest}></ToolbarButton>
|
||||||
runVisibleTests();
|
<ToolbarButton icon='go-to-file' title='Open in VS Code' onClick={() => sendMessageNoReply('open', { location: locationToOpen(treeItem) })}></ToolbarButton>
|
||||||
}}></input>
|
</div>;
|
||||||
</Toolbar>
|
}}
|
||||||
<TreeListView
|
icon={treeItem => {
|
||||||
treeState={treeState}
|
if (treeItem.status === 'running')
|
||||||
setTreeState={setTreeState}
|
return 'codicon-loading';
|
||||||
rootItem={rootItem}
|
if (treeItem.status === 'failed')
|
||||||
render={treeItem => {
|
return 'codicon-error';
|
||||||
return <div className='hbox watch-mode-list-item'>
|
if (treeItem.status === 'passed')
|
||||||
<div className='watch-mode-list-item-title'>{treeItem.title}</div>
|
return 'codicon-check';
|
||||||
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={isRunningTest}></ToolbarButton>
|
if (treeItem.status === 'skipped')
|
||||||
<ToolbarButton icon='go-to-file' title='Open in VS Code' onClick={() => sendMessageNoReply('open', { location: locationToOpen(treeItem) })}></ToolbarButton>
|
return 'codicon-circle-slash';
|
||||||
</div>;
|
return 'codicon-circle-outline';
|
||||||
}}
|
}}
|
||||||
icon={treeItem => {
|
selectedItem={selectedTreeItem}
|
||||||
if (treeItem.status === 'running')
|
onAccepted={runTreeItem}
|
||||||
return 'codicon-loading';
|
onSelected={treeItem => {
|
||||||
if (treeItem.status === 'failed')
|
setSelectedTreeItemId(treeItem.id);
|
||||||
return 'codicon-error';
|
}}
|
||||||
if (treeItem.status === 'passed')
|
noItemsMessage='No tests' />;
|
||||||
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' />
|
|
||||||
</div>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsView: React.FC<{
|
export const SettingsView: React.FC<{
|
||||||
@ -326,13 +334,16 @@ const refreshRootSuite = (eraseResults: boolean) => {
|
|||||||
total: 0,
|
total: 0,
|
||||||
passed: 0,
|
passed: 0,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
|
skipped: 0,
|
||||||
};
|
};
|
||||||
receiver = new TeleReporterReceiver({
|
receiver = new TeleReporterReceiver({
|
||||||
onBegin: (config: FullConfig, suite: Suite) => {
|
onBegin: (config: FullConfig, suite: Suite) => {
|
||||||
if (!rootSuite)
|
if (!rootSuite)
|
||||||
rootSuite = suite;
|
rootSuite = suite;
|
||||||
|
progress.total = suite.allTests().length;
|
||||||
progress.passed = 0;
|
progress.passed = 0;
|
||||||
progress.failed = 0;
|
progress.failed = 0;
|
||||||
|
progress.skipped = 0;
|
||||||
updateRootSuite(rootSuite, progress);
|
updateRootSuite(rootSuite, progress);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -341,7 +352,9 @@ const refreshRootSuite = (eraseResults: boolean) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onTestEnd: (test: TestCase) => {
|
onTestEnd: (test: TestCase) => {
|
||||||
if (test.outcome() === 'unexpected')
|
if (test.outcome() === 'skipped')
|
||||||
|
++progress.skipped;
|
||||||
|
else if (test.outcome() === 'unexpected')
|
||||||
++progress.failed;
|
++progress.failed;
|
||||||
else
|
else
|
||||||
++progress.passed;
|
++progress.passed;
|
||||||
@ -426,6 +439,7 @@ type Progress = {
|
|||||||
total: number;
|
total: number;
|
||||||
passed: number;
|
passed: number;
|
||||||
failed: number;
|
failed: number;
|
||||||
|
skipped: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TreeItemBase = {
|
type TreeItemBase = {
|
||||||
|
@ -139,3 +139,13 @@ body.dark-mode ::-webkit-scrollbar-thumb:hover {
|
|||||||
body.dark-mode ::-webkit-scrollbar-track:hover {
|
body.dark-mode ::-webkit-scrollbar-track:hover {
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.codicon-loading {
|
||||||
|
animation: spin 1s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user