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' ;
import '@web/common.css' ;
import React from 'react' ;
2024-03-20 21:09:49 -07:00
import { TeleSuite } from '@testIsomorphic/teleReceiver' ;
2024-08-22 17:29:10 +02:00
import { TeleSuiteUpdater , type TeleSuiteUpdaterProgress , type TeleSuiteUpdaterTestModel } from '@testIsomorphic/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-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-07 14:24:50 -08:00
import type { XtermDataSource } from '@web/components/xtermWrapper' ;
import { XtermWrapper } from '@web/components/xtermWrapper' ;
2024-07-25 11:23:43 -07:00
import { useDarkModeSetting } from '@web/theme' ;
2024-07-31 12:12:06 +02:00
import { clsx , settings , useSetting } from '@web/uiUtils' ;
2024-03-05 15:11:56 -08:00
import { statusEx , TestTree } from '@testIsomorphic/testTree' ;
import type { TreeItem } from '@testIsomorphic/testTree' ;
2024-08-23 14:58:34 +02:00
import { TestServerConnection , WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection' ;
2024-03-20 16:00:35 -07:00
import { FiltersView } from './uiModeFiltersView' ;
import { TestListView } from './uiModeTestListView' ;
import { TraceView } from './uiModeTraceView' ;
2024-07-25 11:23:43 -07:00
import { SettingsView } from './settingsView' ;
2023-03-01 15:27:23 -08:00
2024-08-22 17:29:10 +02:00
const pathSeparator = navigator . userAgent . toLowerCase ( ) . includes ( 'windows' ) ? '\\' : '/' ;
2023-03-09 20:02:42 -08:00
let xtermSize = { cols : 80 , rows : 24 } ;
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
} ;
2024-03-22 13:49:28 -07:00
const searchParams = new URLSearchParams ( window . location . search ) ;
const guid = searchParams . get ( 'ws' ) ;
const wsURL = new URL ( ` ../ ${ guid } ` , window . location . toString ( ) ) ;
wsURL . protocol = ( window . location . protocol === 'https:' ? 'wss:' : 'ws:' ) ;
const queryParams = {
args : searchParams.getAll ( 'arg' ) ,
grep : searchParams.get ( 'grep' ) || undefined ,
grepInvert : searchParams.get ( 'grepInvert' ) || undefined ,
projects : searchParams.getAll ( 'project' ) ,
workers : searchParams.get ( 'workers' ) || undefined ,
timeout : searchParams.has ( 'timeout' ) ? + searchParams . get ( 'timeout' ) ! : undefined ,
headed : searchParams.has ( 'headed' ) ,
2024-05-21 14:36:31 -07:00
outputDir : searchParams.get ( 'outputDir' ) || undefined ,
2024-07-08 01:08:57 -07:00
updateSnapshots : ( searchParams . get ( 'updateSnapshots' ) as 'all' | 'none' | 'missing' | undefined ) || undefined ,
2024-03-22 13:49:28 -07:00
reporters : searchParams.has ( 'reporter' ) ? searchParams . getAll ( 'reporter' ) : undefined ,
} ;
2024-07-08 01:08:57 -07:00
if ( queryParams . updateSnapshots && ! [ 'all' , 'none' , 'missing' ] . includes ( queryParams . updateSnapshots ) )
queryParams . updateSnapshots = undefined ;
2024-03-22 13:49:28 -07:00
2024-03-25 19:48:20 +01:00
const isMac = navigator . platform === 'MacIntel' ;
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 ( ) ) ;
2024-08-22 17:29:10 +02:00
const [ testModel , setTestModel ] = React . useState < TeleSuiteUpdaterTestModel > ( ) ;
const [ progress , setProgress ] = React . useState < TeleSuiteUpdaterProgress & { 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 ) ;
2024-08-12 13:50:11 +02:00
const [ runningState , setRunningState ] = React . useState < { testIds : Set < string > , itemSelectedByUser? : boolean , completed? : boolean } | undefined > ( ) ;
const isRunningTest = runningState && ! runningState . completed ;
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 ( ) } ) ;
2024-03-21 14:28:07 -07:00
const commandQueue = 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 > ( ) ;
2024-07-30 14:17:41 +02:00
const [ teleSuiteUpdater , setTeleSuiteUpdater ] = React . useState < TeleSuiteUpdater > ( ) ;
2024-07-25 11:23:43 -07:00
const [ settingsVisible , setSettingsVisible ] = React . useState ( false ) ;
const [ testingOptionsVisible , setTestingOptionsVisible ] = React . useState ( false ) ;
2024-07-29 07:32:13 -07:00
const [ revealSource , setRevealSource ] = React . useState ( false ) ;
const onRevealSource = React . useCallback ( ( ) = > setRevealSource ( true ) , [ setRevealSource ] ) ;
2024-08-01 05:36:19 -07:00
const showTestingOptions = false ;
2024-07-25 11:23:43 -07:00
const [ runWorkers , setRunWorkers ] = React . useState ( queryParams . workers ) ;
const singleWorkerSetting = React . useMemo ( ( ) = > {
return [
runWorkers === '1' ,
( value : boolean ) = > {
// When started with `--workers=1`, the setting allows to undo that.
// Otherwise, fallback to the cli `--workers=X` argument.
setRunWorkers ( value ? '1' : ( queryParams . workers === '1' ? undefined : queryParams . workers ) ) ;
} ,
'Single worker' ,
] as const ;
} , [ runWorkers , setRunWorkers ] ) ;
const [ runHeaded , setRunHeaded ] = React . useState ( queryParams . headed ) ;
const showBrowserSetting = React . useMemo ( ( ) = > [ runHeaded , setRunHeaded , 'Show browser' ] as const , [ runHeaded , setRunHeaded ] ) ;
const [ runUpdateSnapshots , setRunUpdateSnapshots ] = React . useState ( queryParams . updateSnapshots ) ;
const updateSnapshotsSetting = React . useMemo ( ( ) = > {
return [
runUpdateSnapshots === 'all' ,
( value : boolean ) = > setRunUpdateSnapshots ( value ? 'all' : 'missing' ) ,
'Update snapshots' ,
] as const ;
} , [ runUpdateSnapshots , setRunUpdateSnapshots ] ) ;
const [ , , showRouteActionsSetting ] = useSetting ( 'show-route-actions' , true , 'Show route actions' ) ;
const darkModeSetting = useDarkModeSetting ( ) ;
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-08-23 14:58:34 +02:00
setTestServerConnection ( new TestServerConnection ( new WebSocketTestServerTransport ( wsURL ) ) ) ;
2023-05-08 18:51:27 -07:00
} , [ ] ) ;
2023-03-13 22:19:31 -07:00
2024-03-20 21:09:49 -07:00
// Load tests on startup.
React . useEffect ( ( ) = > {
inputRef . current ? . focus ( ) ;
setIsLoading ( true ) ;
reloadTests ( ) ;
} , [ reloadTests ] ) ;
// Wire server connection to the auxiliary UI features.
2024-03-20 16:00:35 -07:00
React . useEffect ( ( ) = > {
if ( ! testServerConnection )
return ;
const disposables = [
2024-03-20 21:09:49 -07:00
testServerConnection . onStdio ( params = > {
if ( params . buffer ) {
const data = atob ( params . buffer ) ;
xtermDataSource . write ( data ) ;
} else {
xtermDataSource . write ( params . text ! ) ;
}
} ) ,
2024-03-20 16:00:35 -07:00
testServerConnection . onClose ( ( ) = > setIsDisconnected ( true ) )
] ;
2024-03-20 21:09:49 -07:00
xtermDataSource . resize = ( cols , rows ) = > {
xtermSize = { cols , rows } ;
testServerConnection . resizeTerminalNoReply ( { cols , rows } ) ;
} ;
2024-03-20 16:00:35 -07:00
return ( ) = > {
for ( const disposable of disposables )
disposable . dispose ( ) ;
} ;
} , [ testServerConnection ] ) ;
2024-03-20 21:09:49 -07:00
// This is the main routine, every time connection updates it starts the
// whole workflow.
2023-03-09 21:45:57 -08:00
React . useEffect ( ( ) = > {
2024-03-20 21:09:49 -07:00
if ( ! testServerConnection )
return ;
let throttleTimer : NodeJS.Timeout | undefined ;
const teleSuiteUpdater = new TeleSuiteUpdater ( {
onUpdate : immediate = > {
clearTimeout ( throttleTimer ) ;
throttleTimer = undefined ;
if ( immediate ) {
setTestModel ( teleSuiteUpdater . asModel ( ) ) ;
} else if ( ! throttleTimer ) {
throttleTimer = setTimeout ( ( ) = > {
setTestModel ( teleSuiteUpdater . asModel ( ) ) ;
} , 250 ) ;
}
} ,
onError : error = > {
xtermDataSource . write ( ( error . stack || error . value || '' ) + '\n' ) ;
} ,
pathSeparator ,
} ) ;
2024-07-30 14:17:41 +02:00
setTeleSuiteUpdater ( teleSuiteUpdater ) ;
2024-03-20 21:09:49 -07:00
setTestModel ( undefined ) ;
2023-06-06 14:24:42 -07:00
setIsLoading ( true ) ;
2024-03-20 21:09:49 -07:00
setWatchedTreeIds ( { value : new Set ( ) } ) ;
( async ( ) = > {
2024-04-24 18:54:48 -07:00
try {
await testServerConnection . initialize ( {
interceptStdio : true ,
watchTestDirs : true
} ) ;
const { status , report } = await testServerConnection . runGlobalSetup ( { } ) ;
teleSuiteUpdater . processGlobalReport ( report ) ;
if ( status !== 'passed' )
return ;
2024-07-23 15:29:08 +02:00
const result = await testServerConnection . listTests ( { projects : queryParams.projects , locations : queryParams.args , grep : queryParams.grep , grepInvert : queryParams.grepInvert } ) ;
2024-04-24 18:54:48 -07:00
teleSuiteUpdater . processListReport ( result . report ) ;
testServerConnection . onReport ( params = > {
teleSuiteUpdater . processTestReportEvent ( params ) ;
} ) ;
const { hasBrowsers } = await testServerConnection . checkBrowsers ( { } ) ;
setHasBrowsers ( hasBrowsers ) ;
} finally {
setIsLoading ( false ) ;
}
2024-03-20 21:09:49 -07:00
} ) ( ) ;
return ( ) = > {
clearTimeout ( throttleTimer ) ;
} ;
} , [ testServerConnection ] ) ;
2023-03-07 12:43:16 -08:00
2024-03-20 21:09:49 -07:00
// Update project filter default values.
React . useEffect ( ( ) = > {
if ( ! testModel )
return ;
const { config , rootSuite } = testModel ;
2023-03-17 09:41:23 -07:00
const selectedProjects = config . configFile ? settings . getObject < string [ ] | undefined > ( config . configFile + ':projects' , undefined ) : undefined ;
2024-03-20 21:09:49 -07:00
const newFilter = new Map ( projectFilters ) ;
for ( const projectName of newFilter . keys ( ) ) {
2023-03-07 17:20:41 -08:00
if ( ! rootSuite . suites . find ( s = > s . title === projectName ) )
2024-03-20 21:09:49 -07:00
newFilter . delete ( projectName ) ;
2023-03-07 17:20:41 -08:00
}
for ( const projectSuite of rootSuite . suites ) {
2024-03-20 21:09:49 -07:00
if ( ! newFilter . has ( projectSuite . title ) )
newFilter . set ( projectSuite . title , ! ! selectedProjects ? . includes ( projectSuite . title ) ) ;
2023-03-07 17:20:41 -08:00
}
2024-03-20 21:09:49 -07:00
if ( ! selectedProjects && newFilter . size && ! [ . . . newFilter . values ( ) ] . includes ( true ) )
newFilter . set ( newFilter . entries ( ) . next ( ) . value [ 0 ] , true ) ;
if ( projectFilters . size !== newFilter . size || [ . . . projectFilters ] . some ( ( [ k , v ] ) = > newFilter . get ( k ) !== v ) )
setProjectFilters ( newFilter ) ;
} , [ projectFilters , testModel ] ) ;
2023-03-07 17:20:41 -08:00
2024-03-20 21:09:49 -07:00
// Update progress.
React . useEffect ( ( ) = > {
2024-08-12 13:50:11 +02:00
if ( isRunningTest && testModel ? . progress )
2024-03-20 21:09:49 -07:00
setProgress ( testModel . progress ) ;
else if ( ! testModel )
2023-03-19 12:04:19 -07:00
setProgress ( undefined ) ;
2024-08-12 13:50:11 +02:00
} , [ testModel , isRunningTest ] ) ;
2023-03-07 12:43:16 -08:00
2024-03-20 21:09:49 -07:00
// Test tree is built from the model and filters.
2024-03-20 16:00:35 -07:00
const { testTree } = React . useMemo ( ( ) = > {
2024-03-20 21:09:49 -07:00
if ( ! testModel )
return { testTree : new TestTree ( '' , new TeleSuite ( '' , 'root' ) , [ ] , projectFilters , pathSeparator ) } ;
2024-03-20 16:00:35 -07:00
const testTree = new TestTree ( '' , testModel . rootSuite , testModel . loadErrors , projectFilters , pathSeparator ) ;
2024-08-12 13:50:11 +02:00
testTree . filterTree ( filterText , statusFilters , isRunningTest ? runningState?.testIds : undefined ) ;
2024-03-20 16:00:35 -07:00
testTree . sortAndPropagateStatus ( ) ;
testTree . shortenRoot ( ) ;
testTree . flattenForSingleProject ( ) ;
setVisibleTestIds ( testTree . testIds ( ) ) ;
return { testTree } ;
2024-08-12 13:50:11 +02:00
} , [ filterText , testModel , statusFilters , projectFilters , setVisibleTestIds , runningState , isRunningTest ] ) ;
2024-03-20 16:00:35 -07: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-20 21:09:49 -07:00
if ( ! testServerConnection || ! testModel )
2024-03-19 13:00:49 -07:00
return ;
2024-08-12 13:50:11 +02:00
if ( mode === 'bounce-if-busy' && isRunningTest )
2023-03-20 13:45:35 -07:00
return ;
2023-03-20 21:25:55 -07:00
runTestBacklog . current = new Set ( [ . . . runTestBacklog . current , . . . testIds ] ) ;
2024-03-21 14:28:07 -07:00
commandQueue . current = commandQueue . 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 ) ) {
2024-04-26 10:11:01 -07:00
test . results = [ ] ;
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-22 13:49:28 -07:00
await testServerConnection . runTests ( {
locations : queryParams.args ,
grep : queryParams.grep ,
grepInvert : queryParams.grepInvert ,
testIds : [ . . . testIds ] ,
projects : [ . . . projectFilters ] . filter ( ( [ _ , v ] ) = > v ) . map ( ( [ p ] ) = > p ) ,
2024-07-25 11:23:43 -07:00
workers : runWorkers ,
2024-03-22 13:49:28 -07:00
timeout : queryParams.timeout ,
2024-07-25 11:23:43 -07:00
headed : runHeaded ,
2024-05-21 14:36:31 -07:00
outputDir : queryParams.outputDir ,
2024-07-25 11:23:43 -07:00
updateSnapshots : runUpdateSnapshots ,
2024-03-22 13:49:28 -07:00
reporters : queryParams.reporters ,
trace : 'on' ,
} ) ;
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 )
2024-04-26 10:11:01 -07:00
test . results = [ ] ;
2023-03-17 14:10:25 -07:00
}
setTestModel ( { . . . testModel } ) ;
2024-08-12 13:50:11 +02:00
setRunningState ( oldState = > oldState ? ( { . . . oldState , completed : true } ) : undefined ) ;
2023-03-07 12:43:16 -08:00
} ) ;
2024-08-12 13:50:11 +02:00
} , [ projectFilters , isRunningTest , testModel , testServerConnection , runWorkers , runHeaded , runUpdateSnapshots ] ) ;
2023-03-07 12:43:16 -08:00
2024-03-20 16:00:35 -07:00
React . useEffect ( ( ) = > {
2024-07-30 14:17:41 +02:00
if ( ! testServerConnection || ! teleSuiteUpdater )
2024-03-20 16:00:35 -07:00
return ;
2024-07-30 14:17:41 +02:00
const disposable = testServerConnection . onTestFilesChanged ( async params = > {
// fetch the new list of tests
commandQueue . current = commandQueue . current . then ( async ( ) = > {
setIsLoading ( true ) ;
try {
const result = await testServerConnection . listTests ( { projects : queryParams.projects , locations : queryParams.args , grep : queryParams.grep , grepInvert : queryParams.grepInvert } ) ;
teleSuiteUpdater . processListReport ( result . report ) ;
} catch ( e ) {
// eslint-disable-next-line no-console
console . log ( e ) ;
} finally {
setIsLoading ( false ) ;
}
} ) ;
await commandQueue . current ;
if ( params . testFiles . length === 0 )
return ;
// run affected watched tests
const testModel = teleSuiteUpdater . asModel ( ) ;
const testTree = new TestTree ( '' , testModel . rootSuite , testModel . loadErrors , projectFilters , pathSeparator ) ;
2024-03-20 16:00:35 -07:00
const testIds : string [ ] = [ ] ;
const set = new Set ( params . testFiles ) ;
if ( watchAll ) {
const visit = ( treeItem : TreeItem ) = > {
const fileName = treeItem . location . file ;
if ( fileName && set . has ( fileName ) )
testIds . push ( . . . testTree . collectTestIds ( treeItem ) ) ;
if ( treeItem . kind === 'group' && treeItem . subKind === 'folder' )
treeItem . children . forEach ( visit ) ;
} ;
visit ( testTree . rootItem ) ;
} else {
for ( const treeId of watchedTreeIds . value ) {
const treeItem = testTree . treeItemById ( treeId ) ;
const fileName = treeItem ? . location . file ;
if ( fileName && set . has ( fileName ) )
testIds . push ( . . . testTree . collectTestIds ( treeItem ) ) ;
}
}
runTests ( 'queue-if-busy' , new Set ( testIds ) ) ;
} ) ;
return ( ) = > disposable . dispose ( ) ;
2024-07-30 14:17:41 +02:00
} , [ runTests , testServerConnection , watchAll , watchedTreeIds , teleSuiteUpdater , projectFilters ] ) ;
2024-03-20 16:00:35 -07:00
2024-03-20 21:09:49 -07:00
// Shortcuts.
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 ) = > {
2024-03-25 19:48:20 +01:00
if ( e . code === 'Backquote' && e . ctrlKey ) {
e . preventDefault ( ) ;
setIsShowingOutput ( ! isShowingOutput ) ;
} else if ( e . code === 'F5' && e . shiftKey ) {
2024-03-19 20:36:42 +09:00
e . preventDefault ( ) ;
2024-03-22 13:49:28 -07:00
testServerConnection ? . stopTestsNoReply ( { } ) ;
2024-03-19 20:36:42 +09:00
} else if ( e . code === 'F5' ) {
e . preventDefault ( ) ;
2024-03-25 19:48:20 +01:00
runTests ( 'bounce-if-busy' , visibleTestIds ) ;
2024-03-19 20:36:42 +09:00
}
} ;
addEventListener ( 'keydown' , onShortcutEvent ) ;
return ( ) = > {
removeEventListener ( 'keydown' , onShortcutEvent ) ;
} ;
2024-03-25 19:48:20 +01:00
} , [ runTests , reloadTests , testServerConnection , visibleTestIds , isShowingOutput ] ) ;
2024-03-19 20:36:42 +09:00
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-22 13:49:28 -07:00
testServerConnection ? . installBrowsers ( { } ) . then ( async ( ) = > {
2023-09-07 18:34:59 -07:00
setIsShowingOutput ( false ) ;
2024-03-22 13:49:28 -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 > }
2024-07-31 12:48:46 +02:00
< SplitView
sidebarSize = { 250 }
minSidebarSize = { 150 }
orientation = 'horizontal'
sidebarIsFirst = { true }
settingName = 'testListSidebar'
main = { < div className = 'vbox' >
2024-07-31 12:12:06 +02:00
< div className = { clsx ( 'vbox' , ! isShowingOutput && 'hidden' ) } >
2023-03-11 11:43:33 -08:00
< 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 >
2024-07-31 12:12:06 +02:00
< div className = { clsx ( 'vbox' , isShowingOutput && 'hidden' ) } >
2024-07-29 07:32:13 -07:00
< TraceView
item = { selectedItem }
rootDir = { testModel ? . config ? . rootDir }
revealSource = { revealSource }
onOpenExternally = { location = > testServerConnection ? . openNoReply ( { location : { file : location.file , line : location.line , column : location.column } } ) }
/ >
2023-03-11 11:43:33 -08:00
< / div >
2024-07-31 12:48:46 +02:00
< / div > }
sidebar = { < 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 >
< ToolbarButton icon = 'refresh' title = 'Reload' onClick = { ( ) = > reloadTests ( ) } disabled = { isRunningTest || isLoading } > < / ToolbarButton >
2024-03-25 19:48:20 +01:00
< ToolbarButton icon = 'terminal' title = { 'Toggle output — ' + ( isMac ? '⌃`' : 'Ctrl + `' ) } 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 > }
2024-03-25 19:48:20 +01:00
< ToolbarButton icon = 'play' title = 'Run all — F5' onClick = { ( ) = > runTests ( 'bounce-if-busy' , visibleTestIds ) } disabled = { isRunningTest || isLoading } > < / ToolbarButton >
< ToolbarButton icon = 'debug-stop' title = { 'Stop — ' + ( isMac ? '⇧F5' : 'Shift + F5' ) } onClick = { ( ) = > testServerConnection ? . stopTests ( { } ) } 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 >
2024-03-20 16:00:35 -07:00
< TestListView
2023-03-09 21:45:57 -08:00
filterText = { filterText }
2023-03-17 09:41:23 -07:00
testModel = { testModel }
2024-03-20 16:00:35 -07:00
testTree = { testTree }
testServerConnection = { testServerConnection }
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 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-26 01:06:22 +01:00
requestedCollapseAllCount = { collapseAllCount }
setFilterText = { setFilterText }
2024-07-29 07:32:13 -07:00
onRevealSource = { onRevealSource }
2024-03-26 01:06:22 +01:00
/ >
2024-08-01 05:36:19 -07:00
{ showTestingOptions && < >
< Toolbar noShadow = { true } noMinHeight = { true } className = 'settings-toolbar' onClick = { ( ) = > setTestingOptionsVisible ( ! testingOptionsVisible ) } >
< span
className = { ` codicon codicon- ${ testingOptionsVisible ? 'chevron-down' : 'chevron-right' } ` }
style = { { marginLeft : 5 } }
title = { testingOptionsVisible ? 'Hide Testing Options' : 'Show Testing Options' }
/ >
< div className = 'section-title' > Testing Options < / div >
< / Toolbar >
{ testingOptionsVisible && < SettingsView settings = { [
singleWorkerSetting ,
showBrowserSetting ,
updateSnapshotsSetting ,
] } / > }
< / > }
2024-07-25 11:23:43 -07:00
< Toolbar noShadow = { true } noMinHeight = { true } className = 'settings-toolbar' onClick = { ( ) = > setSettingsVisible ( ! settingsVisible ) } >
< span
className = { ` codicon codicon- ${ settingsVisible ? 'chevron-down' : 'chevron-right' } ` }
style = { { marginLeft : 5 } }
title = { settingsVisible ? 'Hide Settings' : 'Show Settings' }
/ >
< div className = 'section-title' > Settings < / div >
< / Toolbar >
{ settingsVisible && < SettingsView settings = { [
darkModeSetting ,
showRouteActionsSetting ,
] } / > }
2023-03-07 14:24:50 -08:00
< / div >
2024-07-31 12:48:46 +02:00
}
/ >
2023-03-07 14:24:50 -08:00
< / div > ;
2023-03-07 12:43:16 -08:00
} ;