chore: animate the running progress (#21554)

https://github.com/microsoft/playwright/issues/21541
This commit is contained in:
Pavel Feldman 2023-03-09 21:45:57 -08:00 committed by GitHub
parent 0106a54e6e
commit c3b4820f1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 92 additions and 59 deletions

View File

@ -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 {

View File

@ -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 = {

View File

@ -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);
}
}