@@ -2,10 +2,13 @@ import { Element } from '@core/element'
22import { html , css , nothing , type TemplateResult } from 'lit'
33import { customElement , property } from 'lit/decorators.js'
44import { consume } from '@lit/context'
5- import type { TestStats , SuiteStats } from '@wdio/reporter'
65import type { Metadata } from '@wdio/devtools-service/types'
76import { repeat } from 'lit/directives/repeat.js'
87import { suiteContext , metadataContext } from '../../controller/context.js'
8+ import type {
9+ SuiteStatsFragment ,
10+ TestStatsFragment
11+ } from '../../controller/types.js'
912import type {
1013 TestEntry ,
1114 RunCapabilities ,
@@ -64,7 +67,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
6467
6568 @consume ( { context : suiteContext , subscribe : true } )
6669 @property ( { type : Array } )
67- suites : Record < string , SuiteStats > [ ] | undefined = undefined
70+ suites : Record < string , SuiteStatsFragment > [ ] | undefined = undefined
6871
6972 @consume ( { context : metadataContext , subscribe : true } )
7073 metadata : Metadata | undefined = undefined
@@ -278,7 +281,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
278281 return html `
279282 < wdio-test-entry
280283 uid ="${ entry . uid } "
281- state ="${ entry . state as any } "
284+ state ="${ entry . state ?? '' } "
282285 call-source ="${ entry . callSource || '' } "
283286 entry-type ="${ entry . type } "
284287 spec-file ="${ entry . specFile || '' } "
@@ -329,21 +332,23 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
329332 )
330333 }
331334
332- #isRunning( entry : TestStats | SuiteStats ) : boolean {
335+ #isRunning( entry : TestStatsFragment | SuiteStatsFragment ) : boolean {
333336 if ( 'tests' in entry ) {
334337 // Fastest path: any explicitly running descendant
335338 if (
336- entry . tests . some ( ( t ) => ( t as any ) . state === 'running' ) ||
337- entry . suites . some ( ( s ) => this . #isRunning( s ) )
339+ ( entry . tests ?? [ ] ) . some ( ( t ) => t . state === 'running' ) ||
340+ ( entry . suites ?? [ ] ) . some ( ( s ) => this . #isRunning( s ) )
338341 ) {
339342 return true
340343 }
341344
342- const hasPendingTests = entry . tests . some (
343- ( t ) => ( t as any ) . state === 'pending'
345+ const hasPendingTests = ( entry . tests ?? [ ] ) . some (
346+ ( t ) => t . state === 'pending'
347+ )
348+ const hasPendingSuites = ( entry . suites ?? [ ] ) . some ( ( s ) =>
349+ this . #hasPending( s )
344350 )
345- const hasPendingSuites = entry . suites . some ( ( s ) => this . #hasPending( s ) )
346- const suiteState = ( entry as any ) . state
351+ const suiteState = entry . state
347352
348353 // If the suite was explicitly marked 'running' (e.g. by markTestAsRunning)
349354 // and still has pending children, it's actively executing.
@@ -354,12 +359,10 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
354359 // Mixed terminal + pending children = run is in progress regardless of
355360 // explicit suite state (handles Nightwatch Cucumber where the feature
356361 // suite state may be undefined in the JSON payload).
357- const allDescendants = [ ...entry . tests , ...entry . suites ]
362+ const allDescendants = [ ...( entry . tests ?? [ ] ) , ...( entry . suites ?? [ ] ) ]
358363 const hasSomeTerminal = allDescendants . some (
359364 ( t ) =>
360- ( t as any ) . state === 'passed' ||
361- ( t as any ) . state === 'failed' ||
362- ( t as any ) . state === 'skipped'
365+ t . state === 'passed' || t . state === 'failed' || t . state === 'skipped'
363366 )
364367 if ( ( hasPendingTests || hasPendingSuites ) && hasSomeTerminal ) {
365368 return true
@@ -368,33 +371,33 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
368371 return false
369372 }
370373 // For individual tests rely on explicit state only.
371- return ( entry as any ) . state === 'running'
374+ return entry . state === 'running'
372375 }
373376
374- #hasPending( entry : TestStats | SuiteStats ) : boolean {
377+ #hasPending( entry : TestStatsFragment | SuiteStatsFragment ) : boolean {
375378 if ( 'tests' in entry ) {
376- if ( ( entry as any ) . state === 'pending' ) {
379+ if ( entry . state === 'pending' ) {
377380 return true
378381 }
379- if ( entry . tests . some ( ( t ) => ( t as any ) . state === 'pending' ) ) {
382+ if ( ( entry . tests ?? [ ] ) . some ( ( t ) => t . state === 'pending' ) ) {
380383 return true
381384 }
382- if ( entry . suites . some ( ( s ) => this . #hasPending( s ) ) ) {
385+ if ( ( entry . suites ?? [ ] ) . some ( ( s ) => this . #hasPending( s ) ) ) {
383386 return true
384387 }
385388 return false
386389 }
387- return ( entry as any ) . state === 'pending'
390+ return entry . state === 'pending'
388391 }
389392
390- #hasFailed( entry : TestStats | SuiteStats ) : boolean {
393+ #hasFailed( entry : TestStatsFragment | SuiteStatsFragment ) : boolean {
391394 if ( 'tests' in entry ) {
392395 // Check if any immediate test failed
393- if ( entry . tests . find ( ( t ) => t . state === 'failed' ) ) {
396+ if ( ( entry . tests ?? [ ] ) . find ( ( t ) => t . state === 'failed' ) ) {
394397 return true
395398 }
396399 // Check if any nested suite has failures
397- if ( entry . suites . some ( ( s ) => this . #hasFailed( s ) ) ) {
400+ if ( ( entry . suites ?? [ ] ) . some ( ( s ) => this . #hasFailed( s ) ) ) {
398401 return true
399402 }
400403 return false
@@ -403,30 +406,37 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
403406 return entry . state === 'failed'
404407 }
405408
406- #computeEntryState( entry : TestStats | SuiteStats ) : TestState | 'pending' {
409+ #computeEntryState(
410+ entry : TestStatsFragment | SuiteStatsFragment
411+ ) : TestState | 'pending' {
407412 // For suites, check running state from children FIRST — this ensures that
408413 // a rerun (which clears end times) shows the spinner immediately, even if
409414 // the suite still has a cached 'passed'/'failed' state from the previous run.
410415 if ( 'tests' in entry && this . #isRunning( entry ) ) {
411416 return TestState . RUNNING
412417 }
413418
414- const state = ( entry as any ) . state
419+ const state = entry . state
420+
421+ // A suite with an explicit 'pending' state is always in-progress from the
422+ // UI's perspective — the backend uses 'pending' to signal a new run is
423+ // starting. Skip the children check: stale terminal children from the
424+ // previous run must not cause the suite to appear as passed.
425+ if ( 'tests' in entry && state === 'pending' ) {
426+ return TestState . RUNNING
427+ }
415428
416429 // For suites with no explicit terminal state, derive from children.
417- // A suite with state=undefined or state=pending that has no terminal
430+ // A suite with state=undefined or state=running that has no terminal
418431 // children yet is still in-progress — don't show PASSED prematurely.
419- if (
420- 'tests' in entry &&
421- ( state === null || state === 'pending' || state === 'running' )
422- ) {
423- const allDescendants = [ ...entry . tests , ...entry . suites ]
432+ if ( 'tests' in entry && ( state === null || state === 'running' ) ) {
433+ const allDescendants = [ ...( entry . tests ?? [ ] ) , ...( entry . suites ?? [ ] ) ]
424434 if ( allDescendants . length > 0 ) {
425435 const allTerminal = allDescendants . every (
426436 ( t ) =>
427- ( t as any ) . state === 'passed' ||
428- ( t as any ) . state === 'failed' ||
429- ( t as any ) . state === 'skipped'
437+ t . state === 'passed' ||
438+ t . state === 'failed' ||
439+ t . state === 'skipped'
430440 )
431441 if ( ! allTerminal ) {
432442 // Still has non-terminal children — treat as running/loading
@@ -436,7 +446,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
436446 }
437447
438448 // Check explicit terminal state
439- const mappedState = STATE_MAP [ state ]
449+ const mappedState = state ? STATE_MAP [ state ] : undefined
440450 if ( mappedState ) {
441451 return mappedState
442452 }
@@ -458,27 +468,25 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
458468 return entry . end ? TestState . PASSED : 'pending'
459469 }
460470
461- #getTestEntry( entry : TestStats | SuiteStats ) : TestEntry {
471+ #getTestEntry( entry : TestStatsFragment | SuiteStatsFragment ) : TestEntry {
462472 if ( 'tests' in entry ) {
463- const entries = [ ...entry . tests , ...entry . suites ]
473+ const entries = [ ...( entry . tests ?? [ ] ) , ...( entry . suites ?? [ ] ) ]
464474 // A suite whose children are themselves suites is a feature/file-level
465475 // container (Cucumber feature or test file). Tag it as 'feature' so the
466476 // backend runner can distinguish it from a scenario/spec-level suite and
467477 // avoid applying a --name filter that would match no scenarios.
468478 const hasChildSuites = entry . suites && entry . suites . length > 0
469- const derivedType = hasChildSuites
470- ? 'feature'
471- : ( entry as any ) . type || 'suite'
479+ const derivedType = hasChildSuites ? 'feature' : entry . type || 'suite'
472480 return {
473481 uid : entry . uid ,
474- label : entry . title ,
482+ label : entry . title ?? '' ,
475483 type : 'suite' ,
476484 state : this . #computeEntryState( entry ) ,
477- callSource : ( entry as any ) . callSource ,
478- specFile : ( entry as any ) . file ,
479- fullTitle : entry . title ,
480- featureFile : ( entry as any ) . featureFile ,
481- featureLine : ( entry as any ) . featureLine ,
485+ callSource : entry . callSource ,
486+ specFile : entry . file ,
487+ fullTitle : entry . title ?? '' ,
488+ featureFile : entry . featureFile ,
489+ featureLine : entry . featureLine ,
482490 suiteType : derivedType ,
483491 children : Object . values ( entries )
484492 . map ( this . #getTestEntry. bind ( this ) )
@@ -487,14 +495,14 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
487495 }
488496 return {
489497 uid : entry . uid ,
490- label : entry . title ,
498+ label : entry . title ?? '' ,
491499 type : 'test' ,
492500 state : this . #computeEntryState( entry ) ,
493- callSource : ( entry as any ) . callSource ,
494- specFile : ( entry as any ) . file ,
495- fullTitle : ( entry as any ) . fullTitle || entry . title ,
496- featureFile : ( entry as any ) . featureFile ,
497- featureLine : ( entry as any ) . featureLine ,
501+ callSource : entry . callSource ,
502+ specFile : entry . file ,
503+ fullTitle : entry . fullTitle || entry . title ,
504+ featureFile : entry . featureFile ,
505+ featureLine : entry . featureLine ,
498506 children : [ ]
499507 }
500508 }
0 commit comments