2023-03-01 15:27:23 -08:00
/ * *
* 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' ;
2023-03-06 12:25:00 -08:00
import { Workbench } from './workbench' ;
2023-03-01 15:27:23 -08:00
import '@web/common.css' ;
import React from 'react' ;
2023-03-08 17:33:27 -08:00
import { TreeView } from '@web/components/treeView' ;
import type { TreeState } from '@web/components/treeView' ;
2023-03-17 09:41:23 -07:00
import { baseFullConfig , TeleReporterReceiver , TeleSuite } from '@testIsomorphic/teleReceiver' ;
2023-03-14 15:58:55 -07:00
import type { TeleTestCase } from '@testIsomorphic/teleReceiver' ;
2024-03-04 19:52:20 -08:00
import type { FullConfig , Suite , TestCase , Location , TestError , TestResult } from 'playwright/types/testReporter' ;
2023-03-01 15:27:23 -08:00
import { SplitView } from '@web/components/splitView' ;
2023-03-31 18:34:51 -07:00
import { idForAction , MultiTraceModel } from './modelUtil' ;
2023-05-08 18:51:27 -07:00
import type { SourceLocation } from './modelUtil' ;
2023-04-19 16:51:42 -07:00
import './uiModeView.css' ;
2023-03-02 13:45:15 -08:00
import { ToolbarButton } from '@web/components/toolbarButton' ;
2023-03-04 15:05:41 -08:00
import { Toolbar } from '@web/components/toolbar' ;
2023-03-06 12:25:00 -08:00
import type { ContextEntry } from '../entries' ;
2023-03-07 14:24:50 -08:00
import type { XtermDataSource } from '@web/components/xtermWrapper' ;
import { XtermWrapper } from '@web/components/xtermWrapper' ;
2023-03-10 12:41:00 -08:00
import { Expandable } from '@web/components/expandable' ;
2023-03-13 22:19:31 -07:00
import { toggleTheme } from '@web/theme' ;
2023-03-15 22:33:40 -07:00
import { artifactsFolderName } from '@testIsomorphic/folders' ;
2023-03-20 17:12:02 -07:00
import { msToString , settings , useSetting } from '@web/uiUtils' ;
2023-03-31 18:34:51 -07:00
import type { ActionTraceEvent } from '@trace/trace' ;
2023-06-06 18:36:05 -07:00
import { connect } from './wsPort' ;
2023-10-24 16:41:40 -07:00
import { testStatusIcon } from './testUtils' ;
import type { UITestStatus } from './testUtils' ;
2023-03-01 15:27:23 -08:00
2023-05-08 18:51:27 -07:00
let updateRootSuite : ( config : FullConfig , rootSuite : Suite , loadErrors : TestError [ ] , progress : Progress | undefined ) = > void = ( ) = > { } ;
2023-03-19 14:50:09 -07:00
let runWatchedTests = ( fileNames : string [ ] ) = > { } ;
2023-03-09 20:02:42 -08:00
let xtermSize = { cols : 80 , rows : 24 } ;
2023-03-01 15:27:23 -08:00
2023-06-06 18:36:05 -07:00
let sendMessage : ( method : string , params? : any ) = > Promise < any > = async ( ) = > { } ;
2023-03-07 14:24:50 -08:00
const xtermDataSource : XtermDataSource = {
pending : [ ] ,
clear : ( ) = > { } ,
write : data = > xtermDataSource . pending . push ( data ) ,
2023-03-09 20:02:42 -08:00
resize : ( cols : number , rows : number ) = > {
xtermSize = { cols , rows } ;
sendMessageNoReply ( 'resizeTerminal' , { cols , rows } ) ;
} ,
2023-03-07 14:24:50 -08:00
} ;
2023-03-17 09:41:23 -07:00
type TestModel = {
config : FullConfig | undefined ;
rootSuite : Suite | undefined ;
2023-05-08 18:51:27 -07:00
loadErrors : TestError [ ] ;
2023-03-17 09:41:23 -07:00
} ;
2023-04-19 16:51:42 -07:00
export const UIModeView : React.FC < { } > = ( {
2023-03-01 15:27:23 -08:00
} ) = > {
2023-03-13 22:19:31 -07:00
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 ( ) ) ;
2023-05-08 18:51:27 -07:00
const [ testModel , setTestModel ] = React . useState < TestModel > ( { config : undefined , rootSuite : undefined , loadErrors : [ ] } ) ;
2023-03-19 12:04:19 -07:00
const [ progress , setProgress ] = React . useState < Progress & { total : number } | undefined > ( ) ;
2023-10-24 16:41:40 -07:00
const [ selectedItem , setSelectedItem ] = React . useState < { treeItem? : TreeItem , testFile? : SourceLocation , testCase? : TestCase } > ( { } ) ;
2023-03-20 13:45:35 -07:00
const [ visibleTestIds , setVisibleTestIds ] = React . useState < Set < string > > ( new Set ( ) ) ;
2023-03-13 22:19:31 -07:00
const [ isLoading , setIsLoading ] = React . useState < boolean > ( false ) ;
2023-03-19 12:04:19 -07:00
const [ runningState , setRunningState ] = React . useState < { testIds : Set < string > , itemSelectedByUser? : boolean } | undefined > ( ) ;
2023-03-19 14:50:09 -07:00
const [ watchAll , setWatchAll ] = useSetting < boolean > ( 'watch-all' , false ) ;
2023-03-20 17:12:02 -07:00
const [ watchedTreeIds , setWatchedTreeIds ] = React . useState < { value : Set < string > } > ( { value : new Set ( ) } ) ;
2023-03-20 13:45:35 -07:00
const runTestPromiseChain = React . useRef ( Promise . resolve ( ) ) ;
2023-03-20 21:25:55 -07:00
const runTestBacklog = React . useRef < Set < string > > ( new Set ( ) ) ;
2023-04-19 18:16:18 -07:00
const [ collapseAllCount , setCollapseAllCount ] = React . useState ( 0 ) ;
2023-06-06 08:31:52 -07:00
const [ isDisconnected , setIsDisconnected ] = React . useState ( false ) ;
2023-08-23 12:26:11 -07:00
const [ hasBrowsers , setHasBrowsers ] = React . useState ( true ) ;
2023-03-12 10:50:21 -07:00
2023-03-09 21:45:57 -08:00
const inputRef = React . useRef < HTMLInputElement > ( null ) ;
2023-05-08 18:51:27 -07:00
const reloadTests = React . useCallback ( ( ) = > {
2023-03-13 22:19:31 -07:00
setIsLoading ( true ) ;
2023-03-20 17:12:02 -07:00
setWatchedTreeIds ( { value : new Set ( ) } ) ;
2023-05-08 18:51:27 -07:00
updateRootSuite ( baseFullConfig , new TeleSuite ( '' , 'root' ) , [ ] , undefined ) ;
2023-08-23 12:26:11 -07:00
refreshRootSuite ( true ) . then ( async ( ) = > {
2023-03-13 22:19:31 -07:00
setIsLoading ( false ) ;
2023-08-23 12:26:11 -07:00
const { hasBrowsers } = await sendMessage ( 'checkBrowsers' ) ;
setHasBrowsers ( hasBrowsers ) ;
2023-03-13 22:19:31 -07:00
} ) ;
2023-05-08 18:51:27 -07:00
} , [ ] ) ;
2023-03-13 22:19:31 -07:00
2023-03-09 21:45:57 -08:00
React . useEffect ( ( ) = > {
inputRef . current ? . focus ( ) ;
2023-06-06 14:24:42 -07:00
setIsLoading ( true ) ;
2023-06-06 18:36:05 -07:00
connect ( { onEvent : dispatchEvent , onClose : ( ) = > setIsDisconnected ( true ) } ) . then ( send = > {
2023-10-19 15:53:57 -07:00
sendMessage = async ( method , params ) = > {
const logForTest = ( window as any ) . __logForTest ;
logForTest ? . ( { method , params } ) ;
await send ( method , params ) ;
} ;
2023-06-06 18:36:05 -07:00
reloadTests ( ) ;
} ) ;
2023-05-08 18:51:27 -07:00
} , [ reloadTests ] ) ;
2023-03-07 12:43:16 -08:00
2023-05-08 18:51:27 -07:00
updateRootSuite = React . useCallback ( ( config : FullConfig , rootSuite : Suite , loadErrors : TestError [ ] , newProgress : Progress | undefined ) = > {
2023-03-17 09:41:23 -07:00
const selectedProjects = config . configFile ? settings . getObject < string [ ] | undefined > ( config . configFile + ':projects' , undefined ) : undefined ;
2023-03-13 22:19:31 -07:00
for ( const projectName of projectFilters . keys ( ) ) {
2023-03-07 17:20:41 -08:00
if ( ! rootSuite . suites . find ( s = > s . title === projectName ) )
2023-03-13 22:19:31 -07:00
projectFilters . delete ( projectName ) ;
2023-03-07 17:20:41 -08:00
}
for ( const projectSuite of rootSuite . suites ) {
2023-03-13 22:19:31 -07:00
if ( ! projectFilters . has ( projectSuite . title ) )
2023-03-17 09:41:23 -07:00
projectFilters . set ( projectSuite . title , ! ! selectedProjects ? . includes ( projectSuite . title ) ) ;
2023-03-07 17:20:41 -08:00
}
2023-03-17 09:41:23 -07:00
if ( ! selectedProjects && projectFilters . size && ! [ . . . projectFilters . values ( ) ] . includes ( true ) )
2023-03-13 22:19:31 -07:00
projectFilters . set ( projectFilters . entries ( ) . next ( ) . value [ 0 ] , true ) ;
2023-03-07 17:20:41 -08:00
2023-05-08 18:51:27 -07:00
setTestModel ( { config , rootSuite , loadErrors } ) ;
2023-03-13 22:19:31 -07:00
setProjectFilters ( new Map ( projectFilters ) ) ;
2023-03-19 12:04:19 -07:00
if ( runningState && newProgress )
2023-10-19 20:07:47 -07:00
setProgress ( newProgress ) ;
2023-03-19 12:04:19 -07:00
else if ( ! newProgress )
setProgress ( undefined ) ;
2023-05-08 18:51:27 -07:00
} , [ projectFilters , runningState ] ) ;
2023-03-07 12:43:16 -08:00
2023-03-20 13:45:35 -07:00
const runTests = React . useCallback ( ( mode : 'queue-if-busy' | 'bounce-if-busy' , testIds : Set < string > ) = > {
if ( mode === 'bounce-if-busy' && runningState )
return ;
2023-03-20 21:25:55 -07:00
runTestBacklog . current = new Set ( [ . . . runTestBacklog . current , . . . testIds ] ) ;
2023-03-20 13:45:35 -07:00
runTestPromiseChain . current = runTestPromiseChain . current . then ( async ( ) = > {
2023-03-20 21:25:55 -07:00
const testIds = runTestBacklog . current ;
runTestBacklog . current = new Set ( ) ;
if ( ! testIds . size )
return ;
2023-03-20 13:45:35 -07:00
// Clear test results.
{
for ( const test of testModel . rootSuite ? . allTests ( ) || [ ] ) {
2023-04-06 08:33:17 -07:00
if ( testIds . has ( test . id ) ) {
( test as TeleTestCase ) . _clearResults ( ) ;
2024-03-04 19:52:20 -08:00
const result = ( test as TeleTestCase ) . _createTestResult ( 'pending' ) ;
( result as any ) [ statusEx ] = 'scheduled' ;
2023-04-06 08:33:17 -07:00
}
2023-03-20 13:45:35 -07:00
}
setTestModel ( { . . . testModel } ) ;
2023-03-09 20:02:42 -08:00
}
2023-03-20 13:45:35 -07:00
const time = ' [' + new Date ( ) . toLocaleTimeString ( ) + ']' ;
xtermDataSource . write ( '\x1B[2m—' . repeat ( Math . max ( 0 , xtermSize . cols - time . length ) ) + time + '\x1B[22m' ) ;
2023-10-19 20:07:47 -07:00
setProgress ( { total : 0 , passed : 0 , failed : 0 , skipped : 0 } ) ;
2023-03-20 13:45:35 -07:00
setRunningState ( { testIds } ) ;
2023-07-15 15:11:31 -07:00
await sendMessage ( 'run' , { testIds : [ . . . testIds ] , projects : [ . . . projectFilters ] . filter ( ( [ _ , v ] ) = > v ) . map ( ( [ p ] ) = > p ) } ) ;
2023-03-17 14:10:25 -07:00
// Clear pending tests in case of interrupt.
for ( const test of testModel . rootSuite ? . allTests ( ) || [ ] ) {
if ( test . results [ 0 ] ? . duration === - 1 )
( test as TeleTestCase ) . _clearResults ( ) ;
}
setTestModel ( { . . . testModel } ) ;
2023-03-12 10:42:02 -07:00
setRunningState ( undefined ) ;
2023-03-07 12:43:16 -08:00
} ) ;
2023-07-15 15:11:31 -07:00
} , [ projectFilters , runningState , testModel ] ) ;
2023-03-07 12:43:16 -08:00
2023-03-12 10:42:02 -07:00
const isRunningTest = ! ! runningState ;
2023-09-07 18:34:59 -07:00
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 ) ;
sendMessage ( 'installBrowsers' ) . then ( async ( ) = > {
setIsShowingOutput ( false ) ;
const { hasBrowsers } = await sendMessage ( 'checkBrowsers' ) ;
setHasBrowsers ( hasBrowsers ) ;
} ) ;
} , [ closeInstallDialog ] ) ;
2023-03-15 11:17:03 -07:00
2023-04-19 16:51:42 -07:00
return < div className = 'vbox ui-mode' >
2023-09-11 19:01:00 -07:00
{ ! 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 > }
2023-12-18 17:32:57 +01:00
{ isDisconnected && < div className = 'disconnected' >
2023-06-26 22:21:44 +02:00
< div className = 'title' > UI Mode disconnected < / div >
2023-12-18 17:32:57 +01:00
< div > < a href = '#' onClick = { ( ) = > window . location . href = '/' } > Reload the page < / a > to reconnect < / div >
2023-06-06 08:31:52 -07:00
< / div > }
2023-09-11 19:01:00 -07:00
< SplitView sidebarSize = { 250 } minSidebarSize = { 150 } orientation = 'horizontal' sidebarIsFirst = { true } settingName = 'testListSidebar' >
2023-03-11 11:43:33 -08:00
< div className = 'vbox' >
< div className = { 'vbox' + ( isShowingOutput ? '' : ' hidden' ) } >
< Toolbar >
2023-03-19 12:04:19 -07:00
< div className = 'section-title' style = { { flex : 'none' } } > Output < / div >
2023-03-11 11:43:33 -08:00
< 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 >
2023-03-19 12:04:19 -07:00
< XtermWrapper source = { xtermDataSource } > < / XtermWrapper >
2023-03-11 11:43:33 -08:00
< / div >
< div className = { 'vbox' + ( isShowingOutput ? ' hidden' : '' ) } >
2023-03-19 12:04:19 -07:00
< TraceView item = { selectedItem } rootDir = { testModel . config ? . rootDir } / >
2023-03-11 11:43:33 -08:00
< / div >
< / div >
2023-04-19 16:51:42 -07:00
< div className = 'vbox ui-mode-sidebar' >
2023-03-20 20:45:32 -07:00
< Toolbar noShadow = { true } noMinHeight = { true } >
2023-10-31 16:35:13 +01:00
< img src = 'playwright-logo.svg' alt = 'Playwright logo' / >
2023-03-13 22:19:31 -07:00
< div className = 'section-title' > Playwright < / div >
2023-03-19 14:50:09 -07:00
< ToolbarButton icon = 'color-mode' title = 'Toggle color mode' onClick = { ( ) = > toggleTheme ( ) } / >
2023-03-13 22:19:31 -07:00
< ToolbarButton icon = 'refresh' title = 'Reload' onClick = { ( ) = > reloadTests ( ) } disabled = { isRunningTest || isLoading } > < / ToolbarButton >
< ToolbarButton icon = 'terminal' title = 'Toggle output' toggled = { isShowingOutput } onClick = { ( ) = > { setIsShowingOutput ( ! isShowingOutput ) ; } } / >
2023-09-11 19:01:00 -07:00
{ ! hasBrowsers && < ToolbarButton icon = 'lightbulb-autofix' style = { { color : 'var(--vscode-list-warningForeground)' } } title = 'Playwright browsers are missing' onClick = { openInstallDialog } / > }
2023-03-13 22:19:31 -07:00
< / Toolbar >
< FiltersView
filterText = { filterText }
setFilterText = { setFilterText }
statusFilters = { statusFilters }
setStatusFilters = { setStatusFilters }
projectFilters = { projectFilters }
setProjectFilters = { setProjectFilters }
2023-03-17 09:41:23 -07:00
testModel = { testModel }
2023-03-20 13:45:35 -07:00
runTests = { ( ) = > runTests ( 'bounce-if-busy' , visibleTestIds ) } / >
2023-03-19 12:04:19 -07:00
< 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 > }
2023-03-20 13:45:35 -07:00
< ToolbarButton icon = 'play' title = 'Run all' onClick = { ( ) = > runTests ( 'bounce-if-busy' , visibleTestIds ) } disabled = { isRunningTest || isLoading } > < / ToolbarButton >
2023-03-13 22:19:31 -07:00
< ToolbarButton icon = 'debug-stop' title = 'Stop' onClick = { ( ) = > sendMessageNoReply ( 'stop' ) } disabled = { ! isRunningTest || isLoading } > < / ToolbarButton >
2023-10-25 17:05:06 -07:00
< ToolbarButton icon = 'eye' title = 'Watch all' toggled = { watchAll } onClick = { ( ) = > {
setWatchedTreeIds ( { value : new Set ( ) } ) ;
setWatchAll ( ! watchAll ) ;
} } > < / ToolbarButton >
2023-04-19 18:16:18 -07:00
< ToolbarButton icon = 'collapse-all' title = 'Collapse all' onClick = { ( ) = > {
setCollapseAllCount ( collapseAllCount + 1 ) ;
} } / >
2023-03-07 14:24:50 -08:00
< / Toolbar >
2023-03-07 17:20:41 -08:00
< TestList
2023-03-13 22:19:31 -07:00
statusFilters = { statusFilters }
projectFilters = { projectFilters }
2023-03-09 21:45:57 -08:00
filterText = { filterText }
2023-03-17 09:41:23 -07:00
testModel = { testModel }
2023-03-12 10:42:02 -07:00
runningState = { runningState }
2023-03-07 14:24:50 -08:00
runTests = { runTests }
2023-03-19 12:04:19 -07:00
onItemSelected = { setSelectedItem }
2023-03-19 14:50:09 -07:00
setVisibleTestIds = { setVisibleTestIds }
2023-03-19 22:52:48 -07:00
watchAll = { watchAll }
2023-03-20 17:12:02 -07:00
watchedTreeIds = { watchedTreeIds }
setWatchedTreeIds = { setWatchedTreeIds }
2023-04-19 18:16:18 -07:00
isLoading = { isLoading }
requestedCollapseAllCount = { collapseAllCount } / >
2023-03-07 14:24:50 -08:00
< / div >
< / SplitView >
< / div > ;
2023-03-07 12:43:16 -08:00
} ;
2023-03-13 22:19:31 -07:00
const FiltersView : React.FC < {
filterText : string ;
setFilterText : ( text : string ) = > void ;
statusFilters : Map < string , boolean > ;
setStatusFilters : ( filters : Map < string , boolean > ) = > void ;
projectFilters : Map < string , boolean > ;
setProjectFilters : ( filters : Map < string , boolean > ) = > void ;
2023-03-17 09:41:23 -07:00
testModel : TestModel | undefined ,
2023-03-13 22:19:31 -07:00
runTests : ( ) = > void ;
2023-03-17 09:41:23 -07:00
} > = ( { filterText , setFilterText , statusFilters , setStatusFilters , projectFilters , setProjectFilters , testModel , runTests } ) = > {
2023-03-13 22:19:31 -07:00
const [ expanded , setExpanded ] = React . useState ( false ) ;
const inputRef = React . useRef < HTMLInputElement > ( null ) ;
React . useEffect ( ( ) = > {
inputRef . current ? . focus ( ) ;
} , [ ] ) ;
const statusLine = [ . . . statusFilters . entries ( ) ] . filter ( ( [ _ , v ] ) = > v ) . map ( ( [ s ] ) = > s ) . join ( ' ' ) || 'all' ;
const projectsLine = [ . . . projectFilters . entries ( ) ] . filter ( ( [ _ , v ] ) = > v ) . map ( ( [ p ] ) = > p ) . join ( ' ' ) || 'all' ;
return < div className = 'filters' >
< Expandable
expanded = { expanded }
setExpanded = { setExpanded }
title = { < 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 ( ) ;
} } / > } >
< / Expandable >
2023-03-20 20:45:32 -07:00
< div className = 'filter-summary' title = { 'Status: ' + statusLine + '\nProjects: ' + projectsLine } onClick = { ( ) = > setExpanded ( ! expanded ) } >
2023-03-13 22:19:31 -07:00
< span className = 'filter-label' > Status : < / span > { statusLine }
< span className = 'filter-label' > Projects : < / span > { projectsLine }
2023-03-20 20:45:32 -07:00
< / div >
2023-11-30 13:26:03 -08:00
{ expanded && < div className = 'hbox' style = { { marginLeft : 14 , maxHeight : 200 , overflowY : 'auto' } } >
2023-03-20 20:45:32 -07:00
< div className = 'filter-list' >
{ [ . . . statusFilters . entries ( ) ] . map ( ( [ status , value ] ) = > {
return < div className = 'filter-entry' >
< label >
< input type = 'checkbox' checked = { value } onClick = { ( ) = > {
const copy = new Map ( statusFilters ) ;
copy . set ( status , ! copy . get ( status ) ) ;
setStatusFilters ( copy ) ;
} } / >
< div > { status } < / div >
< / label >
< / div > ;
} ) }
< / div >
< div className = 'filter-list' >
{ [ . . . projectFilters . entries ( ) ] . map ( ( [ projectName , value ] ) = > {
return < div className = 'filter-entry' >
< label >
< input type = 'checkbox' checked = { value } onClick = { ( ) = > {
const copy = new Map ( projectFilters ) ;
copy . set ( projectName , ! copy . get ( projectName ) ) ;
setProjectFilters ( copy ) ;
const configFile = testModel ? . config ? . configFile ;
if ( configFile )
settings . setObject ( configFile + ':projects' , [ . . . copy . entries ( ) ] . filter ( ( [ _ , v ] ) = > v ) . map ( ( [ k ] ) = > k ) ) ;
} } / >
2023-09-11 19:01:00 -07:00
< div > { projectName || 'untitled' } < / div >
2023-03-20 20:45:32 -07:00
< / label >
< / div > ;
} ) }
< / div >
2023-03-13 22:19:31 -07:00
< / div > }
< / div > ;
} ;
2023-03-13 12:14:51 -07:00
const TestTreeView = TreeView < TreeItem > ;
2023-03-08 17:33:27 -08:00
2023-03-13 22:19:31 -07:00
const TestList : React.FC < {
statusFilters : Map < string , boolean > ,
projectFilters : Map < string , boolean > ,
2023-03-09 21:45:57 -08:00
filterText : string ,
2023-05-08 18:51:27 -07:00
testModel : TestModel ,
2023-03-20 13:45:35 -07:00
runTests : ( mode : 'bounce-if-busy' | 'queue-if-busy' , testIds : Set < string > ) = > void ,
2023-03-12 10:42:02 -07:00
runningState ? : { testIds : Set < string > , itemSelectedByUser? : boolean } ,
2023-03-20 17:12:02 -07:00
watchAll : boolean ,
watchedTreeIds : { value : Set < string > } ,
setWatchedTreeIds : ( ids : { value : Set < string > } ) = > void ,
2023-03-19 22:52:48 -07:00
isLoading? : boolean ,
2023-03-20 13:45:35 -07:00
setVisibleTestIds : ( testIds : Set < string > ) = > void ,
2023-10-24 16:41:40 -07:00
onItemSelected : ( item : { treeItem? : TreeItem , testCase? : TestCase , testFile? : SourceLocation } ) = > void ,
2023-04-19 18:16:18 -07:00
requestedCollapseAllCount : number ,
} > = ( { statusFilters , projectFilters , filterText , testModel , runTests , runningState , watchAll , watchedTreeIds , setWatchedTreeIds , isLoading , onItemSelected , setVisibleTestIds , requestedCollapseAllCount } ) = > {
2023-03-08 17:33:27 -08:00
const [ treeState , setTreeState ] = React . useState < TreeState > ( { expandedItems : new Map ( ) } ) ;
2023-03-07 12:43:16 -08:00
const [ selectedTreeItemId , setSelectedTreeItemId ] = React . useState < string | undefined > ( ) ;
2023-04-19 18:16:18 -07:00
const [ collapseAllCount , setCollapseAllCount ] = React . useState ( requestedCollapseAllCount ) ;
2023-03-01 15:27:23 -08:00
2023-03-19 14:50:09 -07:00
// Build the test tree.
const { rootItem , treeItemMap , fileNames } = React . useMemo ( ( ) = > {
2023-05-08 18:51:27 -07:00
let rootItem = createTree ( testModel . rootSuite , testModel . loadErrors , projectFilters ) ;
2023-03-20 17:12:02 -07:00
filterTree ( rootItem , filterText , statusFilters , runningState ? . testIds ) ;
2023-03-23 13:29:52 -07:00
sortAndPropagateStatus ( rootItem ) ;
rootItem = shortenRoot ( rootItem ) ;
2023-03-09 20:02:42 -08:00
hideOnlyTests ( rootItem ) ;
2023-03-04 16:28:30 -08:00
const treeItemMap = new Map < string , TreeItem > ( ) ;
const visibleTestIds = new Set < string > ( ) ;
2023-03-19 14:50:09 -07:00
const fileNames = new Set < string > ( ) ;
2023-03-04 16:28:30 -08:00
const visit = ( treeItem : TreeItem ) = > {
2023-03-19 14:50:09 -07:00
if ( treeItem . kind === 'group' && treeItem . location . file )
fileNames . add ( treeItem . location . file ) ;
2023-03-09 20:02:42 -08:00
if ( treeItem . kind === 'case' )
treeItem . tests . forEach ( t = > visibleTestIds . add ( t . id ) ) ;
treeItem . children . forEach ( visit ) ;
2023-03-04 16:28:30 -08:00
treeItemMap . set ( treeItem . id , treeItem ) ;
} ;
2023-03-08 17:33:27 -08:00
visit ( rootItem ) ;
2023-03-20 13:45:35 -07:00
setVisibleTestIds ( visibleTestIds ) ;
2023-03-19 14:50:09 -07:00
return { rootItem , treeItemMap , fileNames } ;
2023-03-20 17:12:02 -07:00
} , [ filterText , testModel , statusFilters , projectFilters , setVisibleTestIds , runningState ] ) ;
2023-03-04 19:39:55 -08:00
2023-03-19 14:50:09 -07:00
// Look for a first failure within the run batch to select it.
2023-03-12 10:42:02 -07:00
React . useEffect ( ( ) = > {
2023-04-19 18:16:18 -07:00
// If collapse was requested, clear the expanded items and return w/o selected item.
if ( collapseAllCount !== requestedCollapseAllCount ) {
treeState . expandedItems . clear ( ) ;
for ( const item of treeItemMap . keys ( ) )
treeState . expandedItems . set ( item , false ) ;
setCollapseAllCount ( requestedCollapseAllCount ) ;
setSelectedTreeItemId ( undefined ) ;
setTreeState ( { . . . treeState } ) ;
return ;
}
2023-03-12 10:42:02 -07:00
if ( ! runningState || runningState . itemSelectedByUser )
return ;
let selectedTreeItem : TreeItem | undefined ;
const visit = ( treeItem : TreeItem ) = > {
2023-03-13 22:19:31 -07:00
treeItem . children . forEach ( visit ) ;
2023-03-12 10:42:02 -07:00
if ( selectedTreeItem )
return ;
if ( treeItem . status === 'failed' ) {
if ( treeItem . kind === 'test' && runningState . testIds . has ( treeItem . test . id ) )
selectedTreeItem = treeItem ;
else if ( treeItem . kind === 'case' && runningState . testIds . has ( treeItem . tests [ 0 ] ? . id ) )
selectedTreeItem = treeItem ;
}
} ;
visit ( rootItem ) ;
if ( selectedTreeItem )
setSelectedTreeItemId ( selectedTreeItem . id ) ;
2023-04-19 18:16:18 -07:00
} , [ runningState , setSelectedTreeItemId , rootItem , collapseAllCount , setCollapseAllCount , requestedCollapseAllCount , treeState , setTreeState , treeItemMap ] ) ;
2023-03-12 10:42:02 -07:00
2023-03-19 14:50:09 -07:00
// Compute selected item.
2023-03-08 17:33:27 -08:00
const { selectedTreeItem } = React . useMemo ( ( ) = > {
2023-03-06 22:35:57 -08:00
const selectedTreeItem = selectedTreeItemId ? treeItemMap . get ( selectedTreeItemId ) : undefined ;
2023-05-08 18:51:27 -07:00
let testFile : SourceLocation | undefined ;
if ( selectedTreeItem ) {
testFile = {
file : selectedTreeItem.location.file ,
line : selectedTreeItem.location.line ,
source : {
errors : testModel.loadErrors.filter ( e = > e . location ? . file === selectedTreeItem . location . file ) . map ( e = > ( { line : e.location ! . line , message : e.message ! } ) ) ,
content : undefined ,
}
} ;
}
2023-03-08 17:33:27 -08:00
let selectedTest : TestCase | undefined ;
2023-03-06 22:35:57 -08:00
if ( selectedTreeItem ? . kind === 'test' )
2023-03-08 17:33:27 -08:00
selectedTest = selectedTreeItem . test ;
else if ( selectedTreeItem ? . kind === 'case' && selectedTreeItem . tests . length === 1 )
selectedTest = selectedTreeItem . tests [ 0 ] ;
2023-10-24 16:41:40 -07:00
onItemSelected ( { treeItem : selectedTreeItem , testCase : selectedTest , testFile } ) ;
2023-03-08 17:33:27 -08:00
return { selectedTreeItem } ;
2023-05-08 18:51:27 -07:00
} , [ onItemSelected , selectedTreeItemId , testModel , treeItemMap ] ) ;
2023-03-02 13:45:15 -08:00
2023-03-19 14:50:09 -07:00
// Update watch all.
React . useEffect ( ( ) = > {
2023-06-06 14:24:42 -07:00
if ( isLoading )
2023-06-06 08:31:52 -07:00
return ;
2023-03-19 14:50:09 -07:00
if ( watchAll ) {
sendMessageNoReply ( 'watch' , { fileNames : [ . . . fileNames ] } ) ;
} else {
const fileNames = new Set < string > ( ) ;
for ( const itemId of watchedTreeIds . value ) {
2023-03-21 12:03:26 -07:00
const treeItem = treeItemMap . get ( itemId ) ;
const fileName = treeItem ? . location . file ;
2023-03-19 14:50:09 -07:00
if ( fileName )
fileNames . add ( fileName ) ;
}
sendMessageNoReply ( 'watch' , { fileNames : [ . . . fileNames ] } ) ;
2023-03-12 10:50:21 -07:00
}
2023-06-06 14:24:42 -07:00
} , [ isLoading , rootItem , fileNames , watchAll , watchedTreeIds , treeItemMap ] ) ;
2023-03-07 15:07:52 -08:00
2023-03-04 16:28:30 -08:00
const runTreeItem = ( treeItem : TreeItem ) = > {
setSelectedTreeItemId ( treeItem . id ) ;
2023-03-20 13:45:35 -07:00
runTests ( 'bounce-if-busy' , collectTestIds ( treeItem ) ) ;
2023-03-04 15:05:41 -08:00
} ;
2023-03-20 13:45:35 -07:00
runWatchedTests = ( changedTestFiles : string [ ] ) = > {
2023-03-12 10:50:21 -07:00
const testIds : string [ ] = [ ] ;
2023-03-20 13:45:35 -07:00
const set = new Set ( changedTestFiles ) ;
2023-03-19 14:50:09 -07:00
if ( watchAll ) {
const visit = ( treeItem : TreeItem ) = > {
2023-03-20 13:45:35 -07:00
const fileName = treeItem . location . file ;
2023-03-19 14:50:09 -07:00
if ( fileName && set . has ( fileName ) )
testIds . push ( . . . collectTestIds ( treeItem ) ) ;
2023-03-20 13:45:35 -07:00
if ( treeItem . kind === 'group' && treeItem . subKind === 'folder' )
treeItem . children . forEach ( visit ) ;
2023-03-19 14:50:09 -07:00
} ;
visit ( rootItem ) ;
} else {
for ( const treeId of watchedTreeIds . value ) {
2023-03-21 12:03:26 -07:00
const treeItem = treeItemMap . get ( treeId ) ;
const fileName = treeItem ? . location . file ;
2023-03-19 14:50:09 -07:00
if ( fileName && set . has ( fileName ) )
testIds . push ( . . . collectTestIds ( treeItem ) ) ;
}
2023-03-12 10:50:21 -07:00
}
2023-03-20 13:45:35 -07:00
runTests ( 'queue-if-busy' , new Set ( testIds ) ) ;
2023-03-04 15:05:41 -08:00
} ;
2023-03-13 12:14:51 -07:00
return < TestTreeView
2023-09-07 17:14:39 -07:00
name = 'tests'
2023-03-09 21:45:57 -08:00
treeState = { treeState }
setTreeState = { setTreeState }
rootItem = { rootItem }
2023-03-12 15:18:47 -07:00
dataTestId = 'test-tree'
2023-03-09 21:45:57 -08:00
render = { treeItem = > {
2023-04-19 16:51:42 -07:00
return < div className = 'hbox ui-mode-list-item' >
2023-09-22 10:43:44 -07:00
< div className = 'ui-mode-list-item-title' title = { treeItem . title } > { treeItem . title } < / div >
2023-04-19 16:51:42 -07:00
{ ! ! treeItem . duration && treeItem . status !== 'skipped' && < div className = 'ui-mode-list-item-time' > { msToString ( treeItem . duration ) } < / div > }
2023-03-20 20:45:32 -07:00
< Toolbar noMinHeight = { true } noShadow = { true } >
< ToolbarButton icon = 'play' title = 'Run' onClick = { ( ) = > runTreeItem ( treeItem ) } disabled = { ! ! runningState } > < / ToolbarButton >
2023-06-05 22:09:45 +02:00
< ToolbarButton icon = 'go-to-file' title = 'Open in VS Code' onClick = { ( ) = > sendMessageNoReply ( 'open' , { location : locationToOpen ( treeItem ) } ) } style = { ( treeItem . kind === 'group' && treeItem . subKind === 'folder' ) ? { visibility : 'hidden' } : { } } > < / ToolbarButton >
2023-03-20 20:45:32 -07:00
{ ! watchAll && < ToolbarButton icon = 'eye' title = 'Watch' onClick = { ( ) = > {
if ( watchedTreeIds . value . has ( treeItem . id ) )
watchedTreeIds . value . delete ( treeItem . id ) ;
else
watchedTreeIds . value . add ( treeItem . id ) ;
setWatchedTreeIds ( { . . . watchedTreeIds } ) ;
} } toggled = { watchedTreeIds . value . has ( treeItem . id ) } > < / ToolbarButton > }
< / Toolbar >
2023-03-09 21:45:57 -08:00
< / div > ;
} }
2023-10-24 16:41:40 -07:00
icon = { treeItem = > testStatusIcon ( treeItem . status ) }
2023-03-09 21:45:57 -08:00
selectedItem = { selectedTreeItem }
onAccepted = { runTreeItem }
onSelected = { treeItem = > {
2023-03-12 10:42:02 -07:00
if ( runningState )
runningState . itemSelectedByUser = true ;
2023-03-09 21:45:57 -08:00
setSelectedTreeItemId ( treeItem . id ) ;
} }
2023-05-08 18:51:27 -07:00
isError = { treeItem = > treeItem . kind === 'group' ? treeItem.hasLoadErrors : false }
2023-05-05 15:12:18 -07:00
autoExpandDepth = { filterText ? 5 : 1 }
2023-03-19 22:52:48 -07:00
noItemsMessage = { isLoading ? 'Loading\u2026' : 'No tests' } / > ;
2023-03-07 14:24:50 -08:00
} ;
2023-03-15 11:17:03 -07:00
const TraceView : React.FC < {
2023-10-24 16:41:40 -07:00
item : { treeItem? : TreeItem , testFile? : SourceLocation , testCase? : TestCase } ,
2023-03-19 12:04:19 -07:00
rootDir? : string ,
} > = ( { item , rootDir } ) = > {
2023-05-18 15:52:44 -07:00
const [ model , setModel ] = React . useState < { model : MultiTraceModel , isLive : boolean } | undefined > ( ) ;
2023-03-15 22:33:40 -07:00
const [ counter , setCounter ] = React . useState ( 0 ) ;
2023-03-15 11:17:03 -07:00
const pollTimer = React . useRef < NodeJS.Timeout | null > ( null ) ;
2023-03-01 15:27:23 -08:00
2023-04-28 16:56:28 -07:00
const { outputDir } = React . useMemo ( ( ) = > {
2023-03-19 12:04:19 -07:00
const outputDir = item . testCase ? outputDirForTestCase ( item . testCase ) : undefined ;
2023-04-28 16:56:28 -07:00
return { outputDir } ;
2023-03-19 12:04:19 -07:00
} , [ item ] ) ;
2023-03-31 18:34:51 -07:00
// Preserve user selection upon live-reloading trace model by persisting the action id.
// This avoids auto-selection of the last action every time we reload the model.
const [ selectedActionId , setSelectedActionId ] = React . useState < string | undefined > ( ) ;
const onSelectionChanged = React . useCallback ( ( action : ActionTraceEvent ) = > setSelectedActionId ( idForAction ( action ) ) , [ setSelectedActionId ] ) ;
2023-05-18 15:52:44 -07:00
const initialSelection = selectedActionId ? model ? . model . actions . find ( a = > idForAction ( a ) === selectedActionId ) : undefined ;
2023-03-31 18:34:51 -07:00
2023-03-01 15:27:23 -08:00
React . useEffect ( ( ) = > {
2023-03-15 11:17:03 -07:00
if ( pollTimer . current )
clearTimeout ( pollTimer . current ) ;
2023-03-09 20:02:42 -08:00
2023-04-28 16:56:28 -07:00
const result = item . testCase ? . results [ 0 ] ;
2023-03-15 22:33:40 -07:00
if ( ! result ) {
setModel ( undefined ) ;
return ;
}
2023-03-09 20:02:42 -08:00
// Test finished.
2023-03-15 22:33:40 -07:00
const attachment = result && result . duration >= 0 && result . attachments . find ( a = > a . name === 'trace' ) ;
if ( attachment && attachment . path ) {
2023-05-18 15:52:44 -07:00
loadSingleTraceFile ( attachment . path ) . then ( model = > setModel ( { model , isLive : false } ) ) ;
2023-03-15 11:17:03 -07:00
return ;
}
2023-03-16 20:09:09 -07:00
if ( ! outputDir ) {
setModel ( undefined ) ;
return ;
}
2023-03-19 12:04:19 -07:00
const traceLocation = ` ${ outputDir } / ${ artifactsFolderName ( result ! . workerIndex ) } /traces/ ${ item . testCase ? . id } .json ` ;
2023-03-15 11:17:03 -07:00
// Start polling running test.
2023-03-15 22:33:40 -07:00
pollTimer . current = setTimeout ( async ( ) = > {
try {
const model = await loadSingleTraceFile ( traceLocation ) ;
2023-05-18 15:52:44 -07:00
setModel ( { model , isLive : true } ) ;
2023-03-15 22:33:40 -07:00
} catch {
setModel ( undefined ) ;
} finally {
setCounter ( counter + 1 ) ;
}
2023-04-19 07:29:28 -07:00
} , 500 ) ;
2023-03-15 22:33:40 -07:00
return ( ) = > {
if ( pollTimer . current )
clearTimeout ( pollTimer . current ) ;
} ;
2023-04-28 16:56:28 -07:00
} , [ outputDir , item , setModel , counter , setCounter ] ) ;
2023-03-19 12:04:19 -07:00
return < Workbench
key = 'workbench'
2023-05-18 15:52:44 -07:00
model = { model ? . model }
2023-03-19 12:04:19 -07:00
hideStackFrames = { true }
showSourcesFirst = { true }
rootDir = { rootDir }
2023-03-31 18:34:51 -07:00
initialSelection = { initialSelection }
onSelectionChanged = { onSelectionChanged }
2023-05-18 15:52:44 -07:00
fallbackLocation = { item . testFile }
2023-10-24 16:41:40 -07:00
isLive = { model ? . isLive }
status = { item . treeItem ? . status } / > ;
2023-03-01 15:27:23 -08:00
} ;
2023-03-05 13:46:21 -08:00
let receiver : TeleReporterReceiver | undefined ;
2023-10-19 20:07:47 -07:00
let lastRunReceiver : TeleReporterReceiver | undefined ;
let lastRunTestCount : number ;
2023-03-05 13:46:21 -08:00
2023-03-10 17:01:19 -08:00
let throttleTimer : NodeJS.Timeout | undefined ;
2023-05-08 18:51:27 -07:00
let throttleData : { config : FullConfig , rootSuite : Suite , loadErrors : TestError [ ] , progress : Progress } | undefined ;
2023-03-10 17:01:19 -08:00
const throttledAction = ( ) = > {
clearTimeout ( throttleTimer ) ;
throttleTimer = undefined ;
2023-05-08 18:51:27 -07:00
updateRootSuite ( throttleData ! . config , throttleData ! . rootSuite , throttleData ! . loadErrors , throttleData ! . progress ) ;
2023-03-10 17:01:19 -08:00
} ;
2023-05-08 18:51:27 -07:00
const throttleUpdateRootSuite = ( config : FullConfig , rootSuite : Suite , loadErrors : TestError [ ] , progress : Progress , immediate = false ) = > {
throttleData = { config , rootSuite , loadErrors , progress } ;
2023-03-10 17:01:19 -08:00
if ( immediate )
throttledAction ( ) ;
else if ( ! throttleTimer )
throttleTimer = setTimeout ( throttledAction , 250 ) ;
} ;
2023-03-13 22:19:31 -07:00
const refreshRootSuite = ( eraseResults : boolean ) : Promise < void > = > {
if ( ! eraseResults )
return sendMessage ( 'list' , { } ) ;
2023-03-07 20:34:57 -08:00
2023-03-04 19:39:55 -08:00
let rootSuite : Suite ;
2023-06-30 13:36:50 -07:00
const loadErrors : TestError [ ] = [ ] ;
2023-03-05 13:46:21 -08:00
const progress : Progress = {
2023-10-19 20:07:47 -07:00
total : 0 ,
2023-03-05 13:46:21 -08:00
passed : 0 ,
failed : 0 ,
2023-03-09 21:45:57 -08:00
skipped : 0 ,
2023-03-05 13:46:21 -08:00
} ;
2023-03-17 09:41:23 -07:00
let config : FullConfig ;
2024-03-04 08:46:32 -08:00
receiver = new TeleReporterReceiver ( {
2023-06-30 16:21:31 -07:00
version : ( ) = > 'v2' ,
2023-06-30 13:36:50 -07:00
onConfigure : ( c : FullConfig ) = > {
2023-03-17 09:41:23 -07:00
config = c ;
2023-10-19 20:07:47 -07:00
// TeleReportReceiver is merging everything into a single suite, so when we
// run one test, we still get many tests via rootSuite.allTests().length.
// To work around that, have a dedicated per-run receiver that will only have
// suite for a single test run, and hence will have correct total.
2024-03-04 08:46:32 -08:00
lastRunReceiver = new TeleReporterReceiver ( {
2023-10-19 20:07:47 -07:00
onBegin : ( suite : Suite ) = > {
lastRunTestCount = suite . allTests ( ) . length ;
lastRunReceiver = undefined ;
}
2024-03-04 08:46:32 -08:00
} , {
mergeProjects : true ,
2024-03-04 19:52:20 -08:00
mergeTestCases : false ,
resolvePath : ( rootDir , relativePath ) = > rootDir + pathSeparator + relativePath ,
2024-03-04 08:46:32 -08:00
} ) ;
2023-06-30 13:36:50 -07:00
} ,
onBegin : ( suite : Suite ) = > {
if ( ! rootSuite )
rootSuite = suite ;
2023-10-19 20:07:47 -07:00
progress . total = lastRunTestCount ;
2023-03-05 13:46:21 -08:00
progress . passed = 0 ;
progress . failed = 0 ;
2023-03-09 21:45:57 -08:00
progress . skipped = 0 ;
2023-05-08 18:51:27 -07:00
throttleUpdateRootSuite ( config , rootSuite , loadErrors , progress , true ) ;
2023-03-10 17:01:19 -08:00
} ,
onEnd : ( ) = > {
2023-05-08 18:51:27 -07:00
throttleUpdateRootSuite ( config , rootSuite , loadErrors , progress , true ) ;
2023-03-04 19:39:55 -08:00
} ,
2024-03-04 19:52:20 -08:00
onTestBegin : ( test : TestCase , testResult : TestResult ) = > {
( testResult as any ) [ statusEx ] = 'running' ;
2023-05-08 18:51:27 -07:00
throttleUpdateRootSuite ( config , rootSuite , loadErrors , progress ) ;
2023-03-04 19:39:55 -08:00
} ,
2024-03-04 19:52:20 -08:00
onTestEnd : ( test : TestCase , testResult : TestResult ) = > {
2023-03-09 21:45:57 -08:00
if ( test . outcome ( ) === 'skipped' )
++ progress . skipped ;
else if ( test . outcome ( ) === 'unexpected' )
2023-03-05 13:46:21 -08:00
++ progress . failed ;
else
++ progress . passed ;
2024-03-04 19:52:20 -08:00
( testResult as any ) [ statusEx ] = testResult . status ;
2023-05-08 18:51:27 -07:00
throttleUpdateRootSuite ( config , rootSuite , loadErrors , progress ) ;
2023-03-04 19:39:55 -08:00
} ,
2023-03-21 12:03:26 -07:00
onError : ( error : TestError ) = > {
xtermDataSource . write ( ( error . stack || error . value || '' ) + '\n' ) ;
2023-05-08 18:51:27 -07:00
loadErrors . push ( error ) ;
2023-06-30 13:36:50 -07:00
throttleUpdateRootSuite ( config , rootSuite ? ? new TeleSuite ( '' , 'root' ) , loadErrors , progress ) ;
} ,
printsToStdio : ( ) = > {
return false ;
2023-03-21 12:03:26 -07:00
} ,
2023-06-30 13:36:50 -07:00
onStdOut : ( ) = > { } ,
onStdErr : ( ) = > { } ,
onExit : ( ) = > { } ,
onStepBegin : ( ) = > { } ,
onStepEnd : ( ) = > { } ,
2024-03-04 08:46:32 -08:00
} , {
mergeProjects : true ,
mergeTestCases : true ,
2024-03-04 19:52:20 -08:00
resolvePath : ( rootDir , relativePath ) = > rootDir + pathSeparator + relativePath ,
2024-03-04 08:46:32 -08:00
} ) ;
2023-04-06 08:33:17 -07:00
receiver . _setClearPreviousResultsWhenTestBegins ( ) ;
2023-03-13 22:19:31 -07:00
return sendMessage ( 'list' , { } ) ;
2023-03-05 13:46:21 -08:00
} ;
2023-03-01 15:27:23 -08:00
2023-03-04 15:05:41 -08:00
const sendMessageNoReply = ( method : string , params? : any ) = > {
2023-03-21 12:13:20 -07:00
if ( ( window as any ) . _overrideProtocolForTest ) {
( window as any ) . _overrideProtocolForTest ( { method , params } ) . catch ( ( ) = > { } ) ;
return ;
}
2023-03-04 15:05:41 -08:00
sendMessage ( method , params ) . catch ( ( e : Error ) = > {
// eslint-disable-next-line no-console
console . error ( e ) ;
2023-03-01 15:27:23 -08:00
} ) ;
2023-03-04 15:05:41 -08:00
} ;
2023-06-06 18:36:05 -07:00
const dispatchEvent = ( method : string , params? : any ) = > {
2023-06-06 08:31:52 -07:00
if ( method === 'listChanged' ) {
refreshRootSuite ( false ) . catch ( ( ) = > { } ) ;
return ;
}
if ( method === 'testFilesChanged' ) {
runWatchedTests ( params . testFileNames ) ;
return ;
}
if ( method === 'stdio' ) {
if ( params . buffer ) {
const data = atob ( params . buffer ) ;
xtermDataSource . write ( data ) ;
} else {
xtermDataSource . write ( params . text ) ;
}
return ;
}
2024-03-01 13:14:12 -08:00
if ( method === 'listReport' )
receiver ? . dispatch ( 'list' , params ) ? . catch ( ( ) = > { } ) ;
if ( method === 'testReport' ) {
// The order of receiver dispatches matters here, we want to assign `lastRunTestCount`
// before we use it.
lastRunReceiver ? . dispatch ( 'test' , params ) ? . catch ( ( ) = > { } ) ;
receiver ? . dispatch ( 'test' , params ) ? . catch ( ( ) = > { } ) ;
}
2023-06-06 08:31:52 -07:00
} ;
2023-03-16 20:09:09 -07:00
const outputDirForTestCase = ( testCase : TestCase ) : string | undefined = > {
for ( let suite : Suite | undefined = testCase . parent ; suite ; suite = suite . parent ) {
if ( suite . project ( ) )
return suite . project ( ) ? . outputDir ;
}
return undefined ;
} ;
2023-03-07 17:20:41 -08:00
const locationToOpen = ( treeItem? : TreeItem ) = > {
if ( ! treeItem )
return ;
2023-03-08 19:50:32 -08:00
return treeItem . location . file + ':' + treeItem . location . line ;
2023-03-07 17:20:41 -08:00
} ;
2023-03-20 13:45:35 -07:00
const collectTestIds = ( treeItem? : TreeItem ) : Set < string > = > {
const testIds = new Set < string > ( ) ;
2023-03-04 16:28:30 -08:00
if ( ! treeItem )
2023-03-20 13:45:35 -07:00
return testIds ;
2023-03-04 16:28:30 -08:00
const visit = ( treeItem : TreeItem ) = > {
2023-03-08 17:33:27 -08:00
if ( treeItem . kind === 'case' )
2023-03-20 13:45:35 -07:00
treeItem . tests . map ( t = > t . id ) . forEach ( id = > testIds . add ( id ) ) ;
2023-03-08 17:33:27 -08:00
else if ( treeItem . kind === 'test' )
2023-03-20 13:45:35 -07:00
testIds . add ( treeItem . id ) ;
2023-03-08 17:33:27 -08:00
else
treeItem . children ? . forEach ( visit ) ;
2023-03-04 16:28:30 -08:00
} ;
visit ( treeItem ) ;
2023-03-04 15:05:41 -08:00
return testIds ;
} ;
2023-03-04 16:28:30 -08:00
2023-03-05 13:46:21 -08:00
type Progress = {
2023-10-19 20:07:47 -07:00
total : number ;
2023-03-05 13:46:21 -08:00
passed : number ;
failed : number ;
2023-03-09 21:45:57 -08:00
skipped : number ;
2023-03-05 13:46:21 -08:00
} ;
2023-03-04 16:28:30 -08:00
type TreeItemBase = {
2023-03-08 19:50:32 -08:00
kind : 'root' | 'group' | 'case' | 'test' ,
2023-03-04 16:28:30 -08:00
id : string ;
title : string ;
2023-03-08 19:50:32 -08:00
location : Location ,
2023-03-20 17:12:02 -07:00
duration : number ;
2023-03-12 10:42:02 -07:00
parent : TreeItem | undefined ;
2023-03-08 17:33:27 -08:00
children : TreeItem [ ] ;
2023-10-24 16:41:40 -07:00
status : UITestStatus ;
2023-03-04 16:28:30 -08:00
} ;
2023-03-08 19:50:32 -08:00
type GroupItem = TreeItemBase & {
2023-03-19 18:51:09 -07:00
kind : 'group' ;
subKind : 'folder' | 'file' | 'describe' ;
2023-05-08 18:51:27 -07:00
hasLoadErrors : boolean ;
2023-03-08 19:50:32 -08:00
children : ( TestCaseItem | GroupItem ) [ ] ;
2023-03-04 16:28:30 -08:00
} ;
type TestCaseItem = TreeItemBase & {
kind : 'case' ,
2023-03-08 17:33:27 -08:00
tests : TestCase [ ] ;
2023-03-10 12:41:00 -08:00
children : TestItem [ ] ;
2023-03-04 16:28:30 -08:00
} ;
type TestItem = TreeItemBase & {
kind : 'test' ,
test : TestCase ;
2023-03-10 12:41:00 -08:00
project : string ;
2023-03-04 16:28:30 -08:00
} ;
2023-03-08 19:50:32 -08:00
type TreeItem = GroupItem | TestCaseItem | TestItem ;
2023-03-04 16:28:30 -08:00
2023-03-19 18:51:09 -07:00
function getFileItem ( rootItem : GroupItem , filePath : string [ ] , isFile : boolean , fileItems : Map < string , GroupItem > ) : GroupItem {
if ( filePath . length === 0 )
return rootItem ;
2023-03-19 22:52:48 -07:00
const fileName = filePath . join ( pathSeparator ) ;
2023-03-19 18:51:09 -07:00
const existingFileItem = fileItems . get ( fileName ) ;
if ( existingFileItem )
return existingFileItem ;
const parentFileItem = getFileItem ( rootItem , filePath . slice ( 0 , filePath . length - 1 ) , false , fileItems ) ;
const fileItem : GroupItem = {
kind : 'group' ,
subKind : isFile ? 'file' : 'folder' ,
id : fileName ,
title : filePath [ filePath . length - 1 ] ,
location : { file : fileName , line : 0 , column : 0 } ,
2023-03-20 17:12:02 -07:00
duration : 0 ,
2023-03-19 18:51:09 -07:00
parent : parentFileItem ,
children : [ ] ,
status : 'none' ,
2023-05-08 18:51:27 -07:00
hasLoadErrors : false ,
2023-03-19 18:51:09 -07:00
} ;
parentFileItem . children . push ( fileItem ) ;
fileItems . set ( fileName , fileItem ) ;
return fileItem ;
}
2023-05-08 18:51:27 -07:00
function createTree ( rootSuite : Suite | undefined , loadErrors : TestError [ ] , projectFilters : Map < string , boolean > ) : GroupItem {
2023-03-13 22:19:31 -07:00
const filterProjects = [ . . . projectFilters . values ( ) ] . some ( Boolean ) ;
2023-03-08 19:50:32 -08:00
const rootItem : GroupItem = {
kind : 'group' ,
2023-03-19 18:51:09 -07:00
subKind : 'folder' ,
2023-03-08 17:33:27 -08:00
id : 'root' ,
title : '' ,
2023-03-08 19:50:32 -08:00
location : { file : '' , line : 0 , column : 0 } ,
2023-03-20 17:12:02 -07:00
duration : 0 ,
2023-03-12 10:42:02 -07:00
parent : undefined ,
2023-03-08 17:33:27 -08:00
children : [ ] ,
2023-03-08 18:24:45 -08:00
status : 'none' ,
2023-05-08 18:51:27 -07:00
hasLoadErrors : false ,
2023-03-08 17:33:27 -08:00
} ;
2023-03-08 19:50:32 -08:00
const visitSuite = ( projectName : string , parentSuite : Suite , parentGroup : GroupItem ) = > {
for ( const suite of parentSuite . suites ) {
2023-03-24 17:09:11 -07:00
const title = suite . title || '<anonymous>' ;
2023-11-02 20:50:08 -07:00
let group = parentGroup . children . find ( item = > item . kind === 'group' && item . title === title ) as GroupItem | undefined ;
2023-03-08 19:50:32 -08:00
if ( ! group ) {
group = {
kind : 'group' ,
2023-03-19 18:51:09 -07:00
subKind : 'describe' ,
2023-11-02 20:50:08 -07:00
id : 'suite:' + parentSuite . titlePath ( ) . join ( '\x1e' ) + '\x1e' + title , // account for anonymous suites
2023-03-08 19:50:32 -08:00
title ,
location : suite.location ! ,
2023-03-20 17:12:02 -07:00
duration : 0 ,
2023-03-12 10:42:02 -07:00
parent : parentGroup ,
2023-03-04 16:28:30 -08:00
children : [ ] ,
2023-03-08 18:24:45 -08:00
status : 'none' ,
2023-05-08 18:51:27 -07:00
hasLoadErrors : false ,
2023-03-04 16:28:30 -08:00
} ;
2023-03-08 19:50:32 -08:00
parentGroup . children . push ( group ) ;
2023-03-04 16:28:30 -08:00
}
2023-03-08 19:50:32 -08:00
visitSuite ( projectName , suite , group ) ;
}
2023-03-04 16:28:30 -08:00
2023-03-08 19:50:32 -08:00
for ( const test of parentSuite . tests ) {
const title = test . title ;
2023-11-02 20:50:08 -07:00
let testCaseItem = parentGroup . children . find ( t = > t . kind !== 'group' && t . title === title ) as TestCaseItem ;
2023-03-08 19:50:32 -08:00
if ( ! testCaseItem ) {
testCaseItem = {
kind : 'case' ,
2023-11-02 20:50:08 -07:00
id : 'test:' + test . titlePath ( ) . join ( '\x1e' ) ,
2023-03-08 19:50:32 -08:00
title ,
2023-03-12 10:42:02 -07:00
parent : parentGroup ,
2023-03-08 17:33:27 -08:00
children : [ ] ,
2023-03-08 19:50:32 -08:00
tests : [ ] ,
location : test.location ,
2023-03-20 17:12:02 -07:00
duration : 0 ,
2023-03-08 19:50:32 -08:00
status : 'none' ,
} ;
parentGroup . children . push ( testCaseItem ) ;
2023-03-04 16:28:30 -08:00
}
2023-03-08 19:50:32 -08:00
2024-03-04 19:52:20 -08:00
const result = test . results [ 0 ] ;
2023-03-20 17:12:02 -07:00
let status : 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped' = 'none' ;
2024-03-04 19:52:20 -08:00
if ( ( result as any ) ? . [ statusEx ] === 'scheduled' )
2023-03-20 17:12:02 -07:00
status = 'scheduled' ;
2024-03-04 19:52:20 -08:00
else if ( ( result as any ) ? . [ statusEx ] === 'running' )
2023-03-08 19:50:32 -08:00
status = 'running' ;
2023-03-23 13:29:52 -07:00
else if ( result ? . status === 'skipped' )
2023-03-09 20:02:42 -08:00
status = 'skipped' ;
2023-03-23 13:29:52 -07:00
else if ( result ? . status === 'interrupted' )
2023-03-17 14:10:25 -07:00
status = 'none' ;
2023-03-23 13:29:52 -07:00
else if ( result && test . outcome ( ) !== 'expected' )
2023-03-08 19:50:32 -08:00
status = 'failed' ;
2023-03-23 13:29:52 -07:00
else if ( result && test . outcome ( ) === 'expected' )
2023-03-08 19:50:32 -08:00
status = 'passed' ;
testCaseItem . tests . push ( test ) ;
testCaseItem . children . push ( {
kind : 'test' ,
id : test.id ,
title : projectName ,
location : test.location ! ,
test ,
2023-03-12 10:42:02 -07:00
parent : testCaseItem ,
2023-03-08 19:50:32 -08:00
children : [ ] ,
status ,
2023-03-20 17:12:02 -07:00
duration : test.results.length ? Math . max ( 0 , test . results [ 0 ] . duration ) : 0 ,
project : projectName ,
2023-03-08 19:50:32 -08:00
} ) ;
2023-03-20 17:12:02 -07:00
testCaseItem . duration = ( testCaseItem . children as TestItem [ ] ) . reduce ( ( a , b ) = > a + b . duration , 0 ) ;
2023-03-04 16:28:30 -08:00
}
2023-03-08 19:50:32 -08:00
} ;
2023-03-19 18:51:09 -07:00
const fileMap = new Map < string , GroupItem > ( ) ;
2023-03-08 19:50:32 -08:00
for ( const projectSuite of rootSuite ? . suites || [ ] ) {
2023-03-13 22:19:31 -07:00
if ( filterProjects && ! projectFilters . get ( projectSuite . title ) )
2023-03-08 19:50:32 -08:00
continue ;
2023-03-19 18:51:09 -07:00
for ( const fileSuite of projectSuite . suites ) {
2023-03-19 22:52:48 -07:00
const fileItem = getFileItem ( rootItem , fileSuite . location ! . file . split ( pathSeparator ) , true , fileMap ) ;
2023-03-19 18:51:09 -07:00
visitSuite ( projectSuite . title , fileSuite , fileItem ) ;
}
2023-07-13 17:54:08 -07:00
}
for ( const loadError of loadErrors ) {
if ( ! loadError . location )
continue ;
const fileItem = getFileItem ( rootItem , loadError . location . file . split ( pathSeparator ) , true , fileMap ) ;
fileItem . hasLoadErrors = true ;
2023-03-04 16:28:30 -08:00
}
2023-03-23 13:29:52 -07:00
return rootItem ;
2023-03-04 16:28:30 -08:00
}
2023-03-20 17:12:02 -07:00
function filterTree ( rootItem : GroupItem , filterText : string , statusFilters : Map < string , boolean > , runningTestIds : Set < string > | undefined ) {
2023-03-13 22:19:31 -07:00
const tokens = filterText . trim ( ) . toLowerCase ( ) . split ( ' ' ) ;
const filtersStatuses = [ . . . statusFilters . values ( ) ] . some ( Boolean ) ;
2023-03-10 12:41:00 -08:00
const filter = ( testCase : TestCaseItem ) = > {
2024-03-05 10:58:55 -08:00
const titleWithTags = [ . . . testCase . tests [ 0 ] . titlePath ( ) , . . . testCase . tests [ 0 ] . tags ] . join ( ' ' ) . toLowerCase ( ) ;
if ( ! tokens . every ( token = > titleWithTags . includes ( token ) ) && ! testCase . tests . some ( t = > runningTestIds ? . has ( t . id ) ) )
2023-03-10 12:41:00 -08:00
return false ;
2023-03-20 17:12:02 -07:00
testCase . children = ( testCase . children as TestItem [ ] ) . filter ( test = > {
2023-11-02 20:50:08 -07:00
return ! filtersStatuses || runningTestIds ? . has ( test . test . id ) || statusFilters . get ( test . status ) ;
2023-03-20 17:12:02 -07:00
} ) ;
2023-03-10 12:41:00 -08:00
testCase . tests = ( testCase . children as TestItem [ ] ) . map ( c = > c . test ) ;
return ! ! testCase . children . length ;
} ;
2023-03-08 19:50:32 -08:00
const visit = ( treeItem : GroupItem ) = > {
const newChildren : ( GroupItem | TestCaseItem ) [ ] = [ ] ;
for ( const child of treeItem . children ) {
if ( child . kind === 'case' ) {
2023-03-10 12:41:00 -08:00
if ( filter ( child ) )
2023-03-08 19:50:32 -08:00
newChildren . push ( child ) ;
} else {
visit ( child ) ;
2023-05-08 18:51:27 -07:00
if ( child . children . length || child . hasLoadErrors )
2023-03-08 19:50:32 -08:00
newChildren . push ( child ) ;
2023-03-04 16:28:30 -08:00
}
}
2023-03-08 19:50:32 -08:00
treeItem . children = newChildren ;
} ;
visit ( rootItem ) ;
2023-03-04 16:28:30 -08:00
}
2023-03-23 13:29:52 -07:00
function sortAndPropagateStatus ( treeItem : TreeItem ) {
for ( const child of treeItem . children )
sortAndPropagateStatus ( child ) ;
if ( treeItem . kind === 'group' ) {
treeItem . children . sort ( ( a , b ) = > {
const fc = a . location . file . localeCompare ( b . location . file ) ;
return fc || a . location . line - b . location . line ;
} ) ;
}
let allPassed = treeItem . children . length > 0 ;
let allSkipped = treeItem . children . length > 0 ;
let hasFailed = false ;
let hasRunning = false ;
let hasScheduled = false ;
for ( const child of treeItem . children ) {
allSkipped = allSkipped && child . status === 'skipped' ;
allPassed = allPassed && ( child . status === 'passed' || child . status === 'skipped' ) ;
hasFailed = hasFailed || child . status === 'failed' ;
hasRunning = hasRunning || child . status === 'running' ;
hasScheduled = hasScheduled || child . status === 'scheduled' ;
}
if ( hasRunning )
treeItem . status = 'running' ;
else if ( hasScheduled )
treeItem . status = 'scheduled' ;
else if ( hasFailed )
treeItem . status = 'failed' ;
else if ( allSkipped )
treeItem . status = 'skipped' ;
else if ( allPassed )
treeItem . status = 'passed' ;
}
function shortenRoot ( rootItem : GroupItem ) : GroupItem {
let shortRoot = rootItem ;
while ( shortRoot . children . length === 1 && shortRoot . children [ 0 ] . kind === 'group' && shortRoot . children [ 0 ] . subKind === 'folder' )
shortRoot = shortRoot . children [ 0 ] ;
shortRoot . location = rootItem . location ;
return shortRoot ;
}
2023-03-08 19:50:32 -08:00
function hideOnlyTests ( rootItem : GroupItem ) {
2023-03-08 17:33:27 -08:00
const visit = ( treeItem : TreeItem ) = > {
if ( treeItem . kind === 'case' && treeItem . children . length === 1 )
treeItem . children = [ ] ;
else
treeItem . children . forEach ( visit ) ;
} ;
visit ( rootItem ) ;
2023-03-04 16:28:30 -08:00
}
2023-03-06 12:25:00 -08:00
async function loadSingleTraceFile ( url : string ) : Promise < MultiTraceModel > {
const params = new URLSearchParams ( ) ;
params . set ( 'trace' , url ) ;
const response = await fetch ( ` contexts? ${ params . toString ( ) } ` ) ;
const contextEntries = await response . json ( ) as ContextEntry [ ] ;
return new MultiTraceModel ( contextEntries ) ;
}
2023-03-19 22:52:48 -07:00
const pathSeparator = navigator . userAgent . toLowerCase ( ) . includes ( 'windows' ) ? '\\' : '/' ;
2024-03-04 19:52:20 -08:00
const statusEx = Symbol ( 'statusEx' ) ;