mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00: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,18 +215,7 @@ export const TestList: React.FC<{ | |||||||
|   if (!isVisible) |   if (!isVisible) | ||||||
|     return <></>; |     return <></>; | ||||||
| 
 | 
 | ||||||
|   return <div className='vbox'> |   return <TreeListView | ||||||
|     <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') |  | ||||||
|             runVisibleTests(); |  | ||||||
|         }}></input> |  | ||||||
|     </Toolbar> |  | ||||||
|     <TreeListView |  | ||||||
|     treeState={treeState} |     treeState={treeState} | ||||||
|     setTreeState={setTreeState} |     setTreeState={setTreeState} | ||||||
|     rootItem={rootItem} |     rootItem={rootItem} | ||||||
| @ -233,8 +242,7 @@ export const TestList: React.FC<{ | |||||||
|     onSelected={treeItem => { |     onSelected={treeItem => { | ||||||
|       setSelectedTreeItemId(treeItem.id); |       setSelectedTreeItemId(treeItem.id); | ||||||
|     }} |     }} | ||||||
|       noItemsMessage='No tests' /> |     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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Pavel Feldman
						Pavel Feldman