@@ -109,7 +109,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
109109 // Clear execution data before triggering rerun
110110 this . dispatchEvent (
111111 new CustomEvent ( 'clear-execution-data' , {
112- detail : { uid : detail . uid } ,
112+ detail : { uid : detail . uid , entryType : detail . entryType } ,
113113 bubbles : true ,
114114 composed : true
115115 } )
@@ -188,7 +188,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
188188 // Clear execution data and mark all tests as running
189189 this . dispatchEvent (
190190 new CustomEvent ( 'clear-execution-data' , {
191- detail : { uid : '*' } ,
191+ detail : { uid : '*' , entryType : 'suite' } ,
192192 bubbles : true ,
193193 composed : true
194194 } )
@@ -334,18 +334,60 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
334334
335335 #isRunning( entry : TestStats | SuiteStats ) : boolean {
336336 if ( 'tests' in entry ) {
337- // Check if any immediate test is running
338- if ( entry . tests . some ( ( t ) => ! t . end ) ) {
337+ // Fastest path: any explicitly running descendant
338+ if (
339+ entry . tests . some ( ( t ) => ( t as any ) . state === 'running' ) ||
340+ entry . suites . some ( ( s ) => this . #isRunning( s ) )
341+ ) {
339342 return true
340343 }
341- // Check if any nested suite is running
342- if ( entry . suites . some ( ( s ) => this . #isRunning( s ) ) ) {
344+
345+ const hasPendingTests = entry . tests . some (
346+ ( t ) => ( t as any ) . state === 'pending'
347+ )
348+ const hasPendingSuites = entry . suites . some ( ( s ) => this . #hasPending( s ) )
349+ const suiteState = ( entry as any ) . state
350+
351+ // If the suite was explicitly marked 'running' (e.g. by markTestAsRunning)
352+ // and still has pending children, it's actively executing.
353+ if ( suiteState === 'running' && ( hasPendingTests || hasPendingSuites ) ) {
354+ return true
355+ }
356+
357+ // Mixed terminal + pending children = run is in progress regardless of
358+ // explicit suite state (handles Nightwatch Cucumber where the feature
359+ // suite state may be undefined in the JSON payload).
360+ const allDescendants = [ ...entry . tests , ...entry . suites ]
361+ const hasSomeTerminal = allDescendants . some (
362+ ( t ) =>
363+ ( t as any ) . state === 'passed' ||
364+ ( t as any ) . state === 'failed' ||
365+ ( t as any ) . state === 'skipped'
366+ )
367+ if ( ( hasPendingTests || hasPendingSuites ) && hasSomeTerminal ) {
343368 return true
344369 }
370+
345371 return false
346372 }
347- // For individual tests, check if end is not set
348- return ! entry . end
373+ // For individual tests rely on explicit state only.
374+ return ( entry as any ) . state === 'running'
375+ }
376+
377+ #hasPending( entry : TestStats | SuiteStats ) : boolean {
378+ if ( 'tests' in entry ) {
379+ if ( ( entry as any ) . state === 'pending' ) {
380+ return true
381+ }
382+ if ( entry . tests . some ( ( t ) => ( t as any ) . state === 'pending' ) ) {
383+ return true
384+ }
385+ if ( entry . suites . some ( ( s ) => this . #hasPending( s ) ) ) {
386+ return true
387+ }
388+ return false
389+ }
390+ return ( entry as any ) . state === 'pending'
349391 }
350392
351393 #hasFailed( entry : TestStats | SuiteStats ) : boolean {
@@ -364,7 +406,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
364406 return entry . state === 'failed'
365407 }
366408
367- #computeEntryState( entry : TestStats | SuiteStats ) : TestState {
409+ #computeEntryState( entry : TestStats | SuiteStats ) : TestState | 'pending' {
368410 // For suites, check running state from children FIRST — this ensures that
369411 // a rerun (which clears end times) shows the spinner immediately, even if
370412 // the suite still has a cached 'passed'/'failed' state from the previous run.
@@ -374,7 +416,26 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
374416
375417 const state = ( entry as any ) . state
376418
377- // Check explicit state
419+ // For suites with no explicit terminal state, derive from children.
420+ // A suite with state=undefined or state=pending that has no terminal
421+ // children yet is still in-progress — don't show PASSED prematurely.
422+ if ( 'tests' in entry && ( state == null || state === 'pending' || state === 'running' ) ) {
423+ const allDescendants = [ ...entry . tests , ...entry . suites ]
424+ if ( allDescendants . length > 0 ) {
425+ const allTerminal = allDescendants . every (
426+ ( t ) =>
427+ ( t as any ) . state === 'passed' ||
428+ ( t as any ) . state === 'failed' ||
429+ ( t as any ) . state === 'skipped'
430+ )
431+ if ( ! allTerminal ) {
432+ // Still has non-terminal children — treat as running/loading
433+ return TestState . RUNNING
434+ }
435+ }
436+ }
437+
438+ // Check explicit terminal state
378439 const mappedState = STATE_MAP [ state ]
379440 if ( mappedState ) {
380441 return mappedState
@@ -388,8 +449,13 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
388449 return TestState . PASSED
389450 }
390451
391- // For individual tests, check if still running
392- return ! entry . end ? TestState . RUNNING : TestState . PASSED
452+ // For individual leaf tests: pending = spinner (run is in progress),
453+ // not circle (which implies "never run").
454+ if ( state === 'pending' ) {
455+ return TestState . RUNNING
456+ }
457+
458+ return entry . end ? TestState . PASSED : 'pending'
393459 }
394460
395461 #getTestEntry( entry : TestStats | SuiteStats ) : TestEntry {
0 commit comments