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' ;
2024-03-05 15:11:56 -08:00
import { baseFullConfig , TeleSuite } from '@testIsomorphic/teleReceiver' ;
import { TeleSuiteUpdater } from './teleSuiteUpdater' ;
import type { Progress } from './teleSuiteUpdater' ;
2023-03-14 15:58:55 -07:00
import type { TeleTestCase } from '@testIsomorphic/teleReceiver' ;
2024-03-05 15:11:56 -08:00
import type * as reporterTypes 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' ;
2024-03-05 15:11:56 -08:00
import { statusEx , TestTree } from '@testIsomorphic/testTree' ;
import type { TreeItem } from '@testIsomorphic/testTree' ;
2023-10-24 16:41:40 -07:00
import { testStatusIcon } from './testUtils' ;
2024-03-19 13:00:49 -07:00
import { TestServerConnection } from './testServerConnection' ;
2023-03-01 15:27:23 -08:00
2024-03-05 15:11:56 -08:00
let updateRootSuite : ( config : reporterTypes.FullConfig , rootSuite : reporterTypes.Suite , loadErrors : reporterTypes.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-03-07 14:24:50 -08:00
const xtermDataSource : XtermDataSource = {
pending : [ ] ,
clear : ( ) = > { } ,
write : data = > xtermDataSource . pending . push ( data ) ,
2024-03-19 13:00:49 -07:00
resize : ( ) = > { } ,
2023-03-07 14:24:50 -08:00
} ;
2023-03-17 09:41:23 -07:00
type TestModel = {
2024-03-05 15:11:56 -08:00
config : reporterTypes.FullConfig | undefined ;
rootSuite : reporterTypes.Suite | undefined ;
loadErrors : reporterTypes.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 > ( ) ;
2024-03-05 15:11:56 -08:00
const [ selectedItem , setSelectedItem ] = React . useState < { treeItem? : TreeItem , testFile? : SourceLocation , testCase? : reporterTypes.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 ) ;
2024-03-19 13:00:49 -07:00
const [ testServerConnection , setTestServerConnection ] = React . useState < TestServerConnection > ( ) ;
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 ( ( ) = > {
2024-03-19 13:00:49 -07:00
const connection = new TestServerConnection ( ) ;
wireConnectionListeners ( connection ) ;
connection . onClose ( ( ) = > setIsDisconnected ( true ) ) ;
setTestServerConnection ( connection ) ;
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 ) ;
2024-03-19 13:00:49 -07:00
( async ( ) = > {
const status = await connection . runGlobalSetup ( ) ;
if ( status === 'passed' )
await refreshRootSuite ( connection ) ;
2023-03-13 22:19:31 -07:00
setIsLoading ( false ) ;
2024-03-19 13:00:49 -07:00
const { hasBrowsers } = await connection . checkBrowsers ( ) ;
2023-08-23 12:26:11 -07:00
setHasBrowsers ( hasBrowsers ) ;
2024-03-19 13:00:49 -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 ) ;
2024-03-19 13:00:49 -07:00
reloadTests ( ) ;
2023-05-08 18:51:27 -07:00
} , [ reloadTests ] ) ;
2023-03-07 12:43:16 -08:00
2024-03-05 15:11:56 -08:00
updateRootSuite = React . useCallback ( ( config : reporterTypes.FullConfig , rootSuite : reporterTypes.Suite , loadErrors : reporterTypes.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 > ) = > {
2024-03-19 13:00:49 -07:00
if ( ! testServerConnection )
return ;
2023-03-20 13:45:35 -07:00
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 } ) ;
2024-03-19 13:00:49 -07:00
await testServerConnection . runTests ( { 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
} ) ;
2024-03-19 13:00:49 -07:00
} , [ projectFilters , runningState , testModel , testServerConnection ] ) ;
2023-03-07 12:43:16 -08:00
2024-03-19 20:36:42 +09:00
React . useEffect ( ( ) = > {
2024-03-19 13:00:49 -07:00
if ( ! testServerConnection )
return ;
2024-03-19 20:36:42 +09:00
const onShortcutEvent = ( e : KeyboardEvent ) = > {
if ( e . code === 'F6' ) {
e . preventDefault ( ) ;
2024-03-19 13:00:49 -07:00
testServerConnection ? . stop ( ) . catch ( ( ) = > { } ) ;
2024-03-19 20:36:42 +09:00
} else if ( e . code === 'F5' ) {
e . preventDefault ( ) ;
reloadTests ( ) ;
}
} ;
addEventListener ( 'keydown' , onShortcutEvent ) ;
return ( ) = > {
removeEventListener ( 'keydown' , onShortcutEvent ) ;
} ;
2024-03-19 13:00:49 -07:00
} , [ runTests , reloadTests , testServerConnection ] ) ;
2024-03-19 20:36:42 +09: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 ) ;
2024-03-19 13:00:49 -07:00
testServerConnection ? . installBrowsers ( ) . then ( async ( ) = > {
2023-09-07 18:34:59 -07:00
setIsShowingOutput ( false ) ;
2024-03-19 13:00:49 -07:00
const { hasBrowsers } = await testServerConnection ? . checkBrowsers ( ) ;
2023-09-07 18:34:59 -07:00
setHasBrowsers ( hasBrowsers ) ;
} ) ;
2024-03-19 13:00:49 -07:00
} , [ closeInstallDialog , testServerConnection ] ) ;
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 >
2024-03-19 13:00:49 -07:00
< ToolbarButton icon = 'debug-stop' title = 'Stop' onClick = { ( ) = > testServerConnection ? . 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 }
2024-03-19 13:00:49 -07:00
requestedCollapseAllCount = { collapseAllCount }
testServerConnection = { testServerConnection } / >
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 ,
2024-03-05 15:11:56 -08:00
onItemSelected : ( item : { treeItem? : TreeItem , testCase? : reporterTypes.TestCase , testFile? : SourceLocation } ) = > void ,
2023-04-19 18:16:18 -07:00
requestedCollapseAllCount : number ,
2024-03-19 13:00:49 -07:00
testServerConnection : TestServerConnection | undefined ,
} > = ( { statusFilters , projectFilters , filterText , testModel , runTests , runningState , watchAll , watchedTreeIds , setWatchedTreeIds , isLoading , onItemSelected , setVisibleTestIds , requestedCollapseAllCount , testServerConnection } ) = > {
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.
2024-03-05 15:11:56 -08:00
const { testTree } = React . useMemo ( ( ) = > {
2024-03-14 15:44:35 -07:00
const testTree = new TestTree ( '' , testModel . rootSuite , testModel . loadErrors , projectFilters , pathSeparator ) ;
2024-03-05 15:11:56 -08:00
testTree . filterTree ( filterText , statusFilters , runningState ? . testIds ) ;
testTree . sortAndPropagateStatus ( ) ;
testTree . shortenRoot ( ) ;
2024-03-14 15:44:35 -07:00
testTree . flattenForSingleProject ( ) ;
setVisibleTestIds ( testTree . testIds ( ) ) ;
2024-03-05 15:11:56 -08:00
return { testTree } ;
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 ( ) ;
2024-03-14 15:44:35 -07:00
for ( const item of testTree . flatTreeItems ( ) )
treeState . expandedItems . set ( item . id , false ) ;
2023-04-19 18:16:18 -07:00
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 ;
}
} ;
2024-03-05 15:11:56 -08:00
visit ( testTree . rootItem ) ;
2023-03-12 10:42:02 -07:00
if ( selectedTreeItem )
setSelectedTreeItemId ( selectedTreeItem . id ) ;
2024-03-05 15:11:56 -08:00
} , [ runningState , setSelectedTreeItemId , testTree , collapseAllCount , setCollapseAllCount , requestedCollapseAllCount , treeState , setTreeState ] ) ;
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 ( ( ) = > {
2024-03-14 15:44:35 -07:00
const selectedTreeItem = selectedTreeItemId ? testTree . treeItemById ( 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 ,
}
} ;
}
2024-03-05 15:11:56 -08:00
let selectedTest : reporterTypes.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 } ;
2024-03-05 15:11:56 -08:00
} , [ onItemSelected , selectedTreeItemId , testModel , testTree ] ) ;
2023-03-02 13:45:15 -08:00
2023-03-19 14:50:09 -07:00
// Update watch all.
React . useEffect ( ( ) = > {
2024-03-19 13:00:49 -07:00
if ( isLoading || ! testServerConnection )
2023-06-06 08:31:52 -07:00
return ;
2023-03-19 14:50:09 -07:00
if ( watchAll ) {
2024-03-19 13:00:49 -07:00
testServerConnection . watch ( { fileNames : testTree.fileNames ( ) } ) . catch ( ( ) = > { } ) ;
2023-03-19 14:50:09 -07:00
} else {
const fileNames = new Set < string > ( ) ;
for ( const itemId of watchedTreeIds . value ) {
2024-03-14 15:44:35 -07:00
const treeItem = testTree . treeItemById ( itemId ) ;
2023-03-21 12:03:26 -07:00
const fileName = treeItem ? . location . file ;
2023-03-19 14:50:09 -07:00
if ( fileName )
fileNames . add ( fileName ) ;
}
2024-03-19 13:00:49 -07:00
testServerConnection . watch ( { fileNames : [ . . . fileNames ] } ) . catch ( ( ) = > { } ) ;
2023-03-12 10:50:21 -07:00
}
2024-03-19 13:00:49 -07:00
} , [ isLoading , testTree , watchAll , watchedTreeIds , testServerConnection ] ) ;
2023-03-07 15:07:52 -08:00
2023-03-04 16:28:30 -08:00
const runTreeItem = ( treeItem : TreeItem ) = > {
setSelectedTreeItemId ( treeItem . id ) ;
2024-03-05 15:11:56 -08:00
runTests ( 'bounce-if-busy' , testTree . 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 ) )
2024-03-05 15:11:56 -08:00
testIds . push ( . . . testTree . 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
} ;
2024-03-05 15:11:56 -08:00
visit ( testTree . rootItem ) ;
2023-03-19 14:50:09 -07:00
} else {
for ( const treeId of watchedTreeIds . value ) {
2024-03-14 15:44:35 -07:00
const treeItem = testTree . treeItemById ( treeId ) ;
2023-03-21 12:03:26 -07:00
const fileName = treeItem ? . location . file ;
2023-03-19 14:50:09 -07:00
if ( fileName && set . has ( fileName ) )
2024-03-05 15:11:56 -08:00
testIds . push ( . . . testTree . collectTestIds ( treeItem ) ) ;
2023-03-19 14:50:09 -07:00
}
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 }
2024-03-05 15:11:56 -08:00
rootItem = { testTree . 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 >
2024-03-19 13:00:49 -07:00
< ToolbarButton icon = 'go-to-file' title = 'Open in VS Code' onClick = { ( ) = > testServerConnection ? . open ( { location : treeItem.location } ) . catch ( ( ) = > { } ) } 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 < {
2024-03-05 15:11:56 -08:00
item : { treeItem? : TreeItem , testFile? : SourceLocation , testCase? : reporterTypes.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
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
} ;
2024-03-05 15:11:56 -08:00
let teleSuiteUpdater : TeleSuiteUpdater | undefined ;
2023-03-05 13:46:21 -08:00
2023-03-10 17:01:19 -08:00
let throttleTimer : NodeJS.Timeout | undefined ;
2024-03-05 15:11:56 -08:00
let throttleData : { config : reporterTypes.FullConfig , rootSuite : reporterTypes.Suite , loadErrors : reporterTypes.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
} ;
2024-03-05 15:11:56 -08:00
const throttleUpdateRootSuite = ( config : reporterTypes.FullConfig , rootSuite : reporterTypes.Suite , loadErrors : reporterTypes.TestError [ ] , progress : Progress , immediate = false ) = > {
2023-05-08 18:51:27 -07:00
throttleData = { config , rootSuite , loadErrors , progress } ;
2023-03-10 17:01:19 -08:00
if ( immediate )
throttledAction ( ) ;
else if ( ! throttleTimer )
throttleTimer = setTimeout ( throttledAction , 250 ) ;
} ;
2024-03-19 13:00:49 -07:00
const refreshRootSuite = async ( testServerConnection : TestServerConnection ) : Promise < void > = > {
2024-03-05 15:11:56 -08:00
teleSuiteUpdater = new TeleSuiteUpdater ( {
onUpdate : ( source , immediate ) = > {
throttleUpdateRootSuite ( source . config ! , source . rootSuite || new TeleSuite ( '' , 'root' ) , source . loadErrors , source . progress , immediate ) ;
2023-03-10 17:01:19 -08:00
} ,
2024-03-05 15:11:56 -08:00
onError : error = > {
2023-03-21 12:03:26 -07:00
xtermDataSource . write ( ( error . stack || error . value || '' ) + '\n' ) ;
} ,
2024-03-14 15:44:35 -07:00
pathSeparator ,
2024-03-04 08:46:32 -08:00
} ) ;
2024-03-19 13:00:49 -07:00
return testServerConnection . listTests ( { } ) ;
2023-03-05 13:46:21 -08:00
} ;
2023-03-01 15:27:23 -08:00
2024-03-19 13:00:49 -07:00
const wireConnectionListeners = ( testServerConnection : TestServerConnection ) = > {
testServerConnection . onListChanged ( ( ) = > {
testServerConnection . listTests ( { } ) . catch ( ( ) = > { } ) ;
2023-03-01 15:27:23 -08:00
} ) ;
2023-06-06 08:31:52 -07:00
2024-03-19 13:00:49 -07:00
testServerConnection . onTestFilesChanged ( testFiles = > {
runWatchedTests ( testFiles ) ;
} ) ;
2023-06-06 08:31:52 -07:00
2024-03-19 13:00:49 -07:00
testServerConnection . onStdio ( params = > {
2023-06-06 08:31:52 -07:00
if ( params . buffer ) {
const data = atob ( params . buffer ) ;
xtermDataSource . write ( data ) ;
} else {
2024-03-19 13:00:49 -07:00
xtermDataSource . write ( params . text ! ) ;
2023-06-06 08:31:52 -07:00
}
2024-03-19 13:00:49 -07:00
} ) ;
2023-06-06 08:31:52 -07:00
2024-03-19 13:00:49 -07:00
testServerConnection . onListReport ( params = > {
2024-03-05 15:11:56 -08:00
teleSuiteUpdater ? . dispatch ( 'list' , params ) ;
2024-03-19 13:00:49 -07:00
} ) ;
testServerConnection . onTestReport ( params = > {
2024-03-05 15:11:56 -08:00
teleSuiteUpdater ? . dispatch ( 'test' , params ) ;
2024-03-19 13:00:49 -07:00
} ) ;
xtermDataSource . resize = ( cols , rows ) = > {
xtermSize = { cols , rows } ;
testServerConnection . resizeTerminal ( { cols , rows } ) . catch ( ( ) = > { } ) ;
} ;
2023-06-06 08:31:52 -07:00
} ;
2024-03-05 15:11:56 -08:00
const outputDirForTestCase = ( testCase : reporterTypes.TestCase ) : string | undefined = > {
for ( let suite : reporterTypes.Suite | undefined = testCase . parent ; suite ; suite = suite . parent ) {
2023-03-16 20:09:09 -07:00
if ( suite . project ( ) )
return suite . project ( ) ? . outputDir ;
}
return undefined ;
} ;
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 ) ;
}
2024-03-14 15:44:35 -07:00
export const pathSeparator = navigator . userAgent . toLowerCase ( ) . includes ( 'windows' ) ? '\\' : '/' ;