@@ -3,9 +3,10 @@ import { html, css, nothing, type TemplateResult } from 'lit'
33import { customElement } from 'lit/decorators.js'
44import { consume } from '@lit/context'
55import type { TestStats , SuiteStats } from '@wdio/reporter'
6+ import type { Metadata } from '@wdio/devtools-service/types'
67import { repeat } from 'lit/directives/repeat.js'
78import { TestState } from './test-suite.js'
8- import { suiteContext } from '../../controller/DataManager.js'
9+ import { suiteContext , metadataContext } from '../../controller/DataManager.js'
910
1011import '~icons/mdi/play.js'
1112import '~icons/mdi/stop.js'
@@ -16,6 +17,7 @@ import '~icons/mdi/expand-all.js'
1617import './test-suite.js'
1718import { CollapseableEntry } from './collapseableEntry.js'
1819import type { DevtoolsSidebarFilter } from './filter.js'
20+ import type { TestRunDetail } from './test-suite.js'
1921
2022const EXPLORER = 'wdio-devtools-sidebar-explorer'
2123
@@ -25,11 +27,17 @@ interface TestEntry {
2527 label : string
2628 callSource ?: string
2729 children : TestEntry [ ]
30+ type : 'suite' | 'test'
31+ specFile ?: string
32+ fullTitle ?: string
2833}
2934
3035@customElement ( EXPLORER )
3136export class DevtoolsSidebarExplorer extends CollapseableEntry {
3237 #testFilter: DevtoolsSidebarFilter | undefined
38+ #filterListener = this . #filterTests. bind ( this )
39+ #runListener = this . #handleTestRun. bind ( this )
40+ #stopListener = this . #handleTestStop. bind ( this )
3341
3442 static styles = [
3543 ...Element . styles ,
@@ -58,21 +66,139 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
5866 @consume ( { context : suiteContext , subscribe : true } )
5967 suites : Record < string , SuiteStats > [ ] | undefined = undefined
6068
69+ @consume ( { context : metadataContext , subscribe : true } )
70+ metadata : Metadata | undefined = undefined
71+
6172 connectedCallback ( ) : void {
6273 super . connectedCallback ( )
63- window . addEventListener ( 'app-test-filter' , this . #filterTests. bind ( this ) )
74+ window . addEventListener ( 'app-test-filter' , this . #filterListener)
75+ this . addEventListener ( 'app-test-run' , this . #runListener as EventListener )
76+ this . addEventListener ( 'app-test-stop' , this . #stopListener as EventListener )
77+ }
78+
79+ disconnectedCallback ( ) : void {
80+ super . disconnectedCallback ( )
81+ window . removeEventListener ( 'app-test-filter' , this . #filterListener)
82+ this . removeEventListener ( 'app-test-run' , this . #runListener as EventListener )
83+ this . removeEventListener (
84+ 'app-test-stop' ,
85+ this . #stopListener as EventListener
86+ )
6487 }
6588
6689 #filterTests( { detail } : { detail : DevtoolsSidebarFilter } ) {
6790 this . #testFilter = detail
6891 this . requestUpdate ( )
6992 }
7093
94+ async #handleTestRun( event : Event ) {
95+ console . log ( 'handleTestRun' , event )
96+ event . stopPropagation ( )
97+ const detail = ( event as CustomEvent < TestRunDetail > ) . detail
98+ await this . #postToBackend( '/api/tests/run' , {
99+ ...detail ,
100+ runAll : detail . uid === '*' ,
101+ framework : this . #getFramework( ) ,
102+ specFile : detail . specFile || this . #deriveSpecFile( detail ) ,
103+ configFile : this . #getConfigPath( )
104+ } )
105+ }
106+
107+ async #handleTestStop( event : Event ) {
108+ event . stopPropagation ( )
109+ const detail = ( event as CustomEvent < TestRunDetail > ) . detail
110+ await this . #postToBackend( '/api/tests/stop' , { ...detail } )
111+ }
112+
113+ async #postToBackend( path : string , body : Record < string , unknown > ) {
114+ try {
115+ const response = await fetch ( path , {
116+ method : 'POST' ,
117+ headers : {
118+ 'content-type' : 'application/json'
119+ } ,
120+ body : JSON . stringify ( body )
121+ } )
122+ if ( ! response . ok ) {
123+ const errorText = await response . text ( )
124+ throw new Error ( errorText || 'Unknown error' )
125+ }
126+ } catch ( error ) {
127+ console . error ( 'Failed to communicate with backend' , error )
128+ window . dispatchEvent (
129+ new CustomEvent ( 'app-logs' , {
130+ detail : `Test runner error: ${ ( error as Error ) . message } `
131+ } )
132+ )
133+ }
134+ }
135+
136+ #deriveSpecFile( detail : TestRunDetail ) {
137+ if ( detail . specFile ) {
138+ return detail . specFile
139+ }
140+ const source = detail . callSource
141+ if ( source ?. startsWith ( 'file://' ) ) {
142+ try {
143+ return new URL ( source ) . pathname
144+ } catch {
145+ return source
146+ }
147+ }
148+ if ( source ) {
149+ const match = source . match ( / ^ ( .* ?) : \d + : \d + $ / )
150+ if ( match ?. [ 1 ] ) {
151+ return match [ 1 ]
152+ }
153+ return source
154+ }
155+
156+ return undefined
157+ }
158+
159+ #runAllSuites( ) {
160+ console . log ( 'runAllSuites' )
161+ void this . #postToBackend( '/api/tests/run' , {
162+ uid : '*' ,
163+ entryType : 'suite' ,
164+ runAll : true ,
165+ framework : this . #getFramework( ) ,
166+ configFile : this . #getConfigPath( )
167+ } )
168+ }
169+
170+ #stopActiveRun( ) {
171+ void this . #postToBackend( '/api/tests/stop' , {
172+ uid : '*'
173+ } )
174+ }
175+
176+ #getFramework( ) : string | undefined {
177+ const options = this . metadata ?. options as { framework ?: string } | undefined
178+ return options ?. framework
179+ }
180+
181+ #getConfigPath( ) : string | undefined {
182+ const options = this . metadata ?. options as
183+ | {
184+ configFile ?: string
185+ configFilePath ?: string
186+ }
187+ | undefined
188+ console . log ( 'getConfigPath' , options ?. configFilePath , options ?. configFile )
189+ return options ?. configFilePath || options ?. configFile
190+ }
191+
71192 #renderEntry( entry : TestEntry ) : TemplateResult {
72193 return html `
73194 < wdio-test-entry
195+ uid ="${ entry . uid } "
74196 state ="${ entry . state as any } "
75197 call-source ="${ entry . callSource || '' } "
198+ entry-type ="${ entry . type } "
199+ spec-file ="${ entry . specFile || '' } "
200+ full-title ="${ entry . fullTitle || '' } "
201+ label-text ="${ entry . label } "
76202 >
77203 < label slot ="label "> ${ entry . label } </ label >
78204 ${ entry . children && entry . children . length
@@ -120,12 +246,15 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
120246 return {
121247 uid : entry . uid ,
122248 label : entry . title ,
249+ type : 'suite' ,
123250 state : entry . tests . some ( ( t ) => ! t . end )
124251 ? TestState . RUNNING
125252 : entry . tests . find ( ( t ) => t . state === 'failed' )
126253 ? TestState . FAILED
127254 : TestState . PASSED ,
128255 callSource : ( entry as any ) . callSource ,
256+ specFile : ( entry as any ) . file ,
257+ fullTitle : entry . title ,
129258 children : Object . values ( entries )
130259 . map ( this . #getTestEntry. bind ( this ) )
131260 . filter ( this . #filterEntry. bind ( this ) )
@@ -134,12 +263,15 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
134263 return {
135264 uid : entry . uid ,
136265 label : entry . title ,
266+ type : 'test' ,
137267 state : ! entry . end
138268 ? TestState . RUNNING
139269 : entry . state === 'failed'
140270 ? TestState . FAILED
141271 : TestState . PASSED ,
142272 callSource : ( entry as any ) . callSource ,
273+ specFile : ( entry as any ) . file ,
274+ fullTitle : ( entry as any ) . fullTitle || entry . title ,
143275 children : [ ]
144276 }
145277 }
@@ -171,11 +303,13 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
171303 < nav class ="flex ml-auto ">
172304 < button
173305 class ="p-1 rounded hover:bg-toolbarHoverBackground text-sm group "
306+ @click ="${ ( ) => this . #runAllSuites( ) } "
174307 >
175308 < icon-mdi-play class ="group-hover:text-chartsGreen "> </ icon-mdi-play >
176309 </ button >
177310 < button
178311 class ="p-1 rounded hover:bg-toolbarHoverBackground text-sm group "
312+ @click ="${ ( ) => this . #stopActiveRun( ) } "
179313 >
180314 < icon-mdi-stop class ="group-hover:text-chartsRed "> </ icon-mdi-stop >
181315 </ button >
0 commit comments