Skip to content

Commit 93a2922

Browse files
committed
Test rerun test suite/case state fix
1 parent 31199bd commit 93a2922

9 files changed

Lines changed: 138 additions & 16 deletions

File tree

example/wdio.conf.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const config: Options.Testrunner = {
6363
capabilities: [
6464
{
6565
browserName: 'chrome',
66-
browserVersion: '146.0.7680.72', // specify chromium browser version for testing
66+
browserVersion: '146.0.7680.178', // specify chromium browser version for testing
6767
'goog:chromeOptions': {
6868
args: [
6969
'--headless',

packages/app/src/controller/DataManager.ts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from './context.js'
2020
import { CACHE_ID } from './constants.js'
2121
import { getTimestamp } from '../utils/helpers.js'
22+
import { rerunState } from './rerunState.js'
2223
import type {
2324
TestStatsFragment,
2425
SuiteStatsFragment,
@@ -87,16 +88,30 @@ export class DataManagerController implements ReactiveController {
8788
}
8889

8990
clearExecutionData(uid?: string, entryType?: 'suite' | 'test') {
90-
this.#resetExecutionData()
91+
// If we are already tracking a feature-level rerun and this clear is for
92+
// a child scenario (not the top-level rerun trigger itself), skip resetting
93+
// execution data so previously-completed scenarios' data is preserved.
94+
const isChildOfActiveRerun = !!(uid && rerunState.activeRerunSuiteUid && uid !== rerunState.activeRerunSuiteUid)
95+
96+
if (!isChildOfActiveRerun) {
97+
this.#resetExecutionData()
98+
}
9199

92100
// When the backend sends clearExecutionData with no uid (e.g. a full Nightwatch
93101
// rerun), immediately mark all suites as running so the spinner shows instead
94102
// of the previous run's terminal state (passed/failed).
95103
if (!uid) {
104+
rerunState.activeRerunSuiteUid = undefined
96105
this.#markTestAsRunning('*', 'suite')
97106
return
98107
}
99108

109+
// Track the top-level rerun suite uid so we can identify child-scenario
110+
// clears (from the Nightwatch backend) and skip their data wipes.
111+
if (!isChildOfActiveRerun && entryType === 'suite' && uid !== '*') {
112+
rerunState.activeRerunSuiteUid = uid
113+
}
114+
100115
// Track explicit single-test reruns so merge logic can keep sibling tests
101116
// stable while the backend emits suite-level "pending" snapshots.
102117
if (entryType === 'test' && uid !== '*') {
@@ -221,8 +236,12 @@ export class DataManagerController implements ReactiveController {
221236
...(matched
222237
? {
223238
state: 'running' as const,
224-
start: runStart,
225-
end: undefined
239+
// Don't reset the parent's start/end when it is already
240+
// running — subsequent child-scenario marks would otherwise
241+
// reset the feature's original run timestamp.
242+
...(s.state !== 'running'
243+
? { start: runStart, end: undefined }
244+
: {})
226245
}
227246
: {}),
228247
tests: updatedTests || [],
@@ -329,6 +348,33 @@ export class DataManagerController implements ReactiveController {
329348
}
330349

331350
#shouldResetForNewRun(data: unknown): boolean {
351+
// During a UI-triggered rerun, suppress auto-detection so sibling-scenario
352+
// updates don't wipe accumulated execution data.
353+
// Still update #lastSeenRunTimestamp so that once activeRerunSuiteUid is
354+
// cleared the final suite update isn't mistakenly treated as a new run.
355+
if (rerunState.activeRerunSuiteUid) {
356+
const payloads = Array.isArray(data)
357+
? (data as Record<string, SuiteStatsFragment>[])
358+
: ([data] as Record<string, SuiteStatsFragment>[])
359+
for (const chunk of payloads) {
360+
if (!chunk) {
361+
continue
362+
}
363+
for (const suite of Object.values(chunk)) {
364+
if (!suite?.start) {
365+
continue
366+
}
367+
const t = getTimestamp(
368+
suite.start as Date | number | string | undefined
369+
)
370+
if (t > this.#lastSeenRunTimestamp) {
371+
this.#lastSeenRunTimestamp = t
372+
}
373+
}
374+
}
375+
return false
376+
}
377+
332378
const payloads = Array.isArray(data)
333379
? (data as Record<string, SuiteStatsFragment>[])
334380
: ([data] as Record<string, SuiteStatsFragment>[])
@@ -399,6 +445,7 @@ export class DataManagerController implements ReactiveController {
399445

400446
#handleTestStopped() {
401447
this.#activeRerunTestUid = undefined
448+
rerunState.activeRerunSuiteUid = undefined
402449

403450
// Mark all running tests as failed when test execution is stopped
404451
const suites = this.suitesContextProvider.value || []
@@ -563,6 +610,15 @@ export class DataManagerController implements ReactiveController {
563610
this.suitesContextProvider.setValue(
564611
Array.from(suiteMap.entries()).map(([uid, suite]) => ({ [uid]: suite }))
565612
)
613+
614+
// Once the active rerun suite reaches a terminal state, clear the tracking
615+
// flag so subsequent CLI-triggered runs can be detected normally.
616+
if (rerunState.activeRerunSuiteUid) {
617+
const activeSuite = suiteMap.get(rerunState.activeRerunSuiteUid)
618+
if (activeSuite?.end) {
619+
rerunState.activeRerunSuiteUid = undefined
620+
}
621+
}
566622
}
567623

568624
#handleLogsUpdate(data: string[]) {
@@ -649,8 +705,14 @@ export class DataManagerController implements ReactiveController {
649705
// #mergeChildSuites preserves stale child suites from the previous run,
650706
// but they must not keep their terminal states — mark them 'pending' so
651707
// they render as a spinner instead of a stale checkmark/cross.
708+
// Exception: when only a specific child scenario is being rerun
709+
// (activeRerunSuiteUid differs from the incoming feature suite's uid),
710+
// sibling scenarios must keep their existing terminal states.
711+
const isChildRerun =
712+
!!rerunState.activeRerunSuiteUid &&
713+
rerunState.activeRerunSuiteUid !== incoming.uid
652714
const finalSuites =
653-
incoming.state === 'pending' && mergedSuites
715+
incoming.state === 'pending' && mergedSuites && !isChildRerun
654716
? mergedSuites.map((s) =>
655717
s.state === 'passed' || s.state === 'failed'
656718
? { ...s, state: 'pending' as const, end: undefined }

packages/app/src/controller/context.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ export const suiteContext = createContext<Record<string, SuiteStatsFragment>[]>(
2727
export const hasConnectionContext = createContext<boolean>(
2828
Symbol('hasConnection')
2929
)
30+
export const activeRerunContext = createContext<string | undefined>(
31+
Symbol('activeRerunContext')
32+
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const rerunState = {
2+
activeRerunSuiteUid: undefined as string | undefined,
3+
isTopLevelRerun: false
4+
}

packages/nightwatch-devtools/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"license": "MIT",
2525
"dependencies": {
2626
"@wdio/devtools-backend": "workspace:*",
27+
"@wdio/devtools-script": "workspace:*",
2728
"@wdio/logger": "^9.6.0",
2829
"import-meta-resolve": "^4.2.0",
2930
"stacktrace-parser": "^0.1.10",

packages/nightwatch-devtools/src/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ export const NAVIGATION_COMMANDS = ['url', 'navigate', 'navigateTo'] as const
9292
/** Spinner progress frames — suppress from UI Console output. */
9393
export const SPINNER_RE = /^[]/u
9494

95-
/** Matches a path segment that indicates a test/spec directory (e.g. /tests/ or /spec/). */
96-
export const TEST_PATH_PATTERN = /\/(test|spec|tests)\//i
95+
/** Matches a path segment that indicates a test/spec directory (e.g. /tests/, /spec/, /nightwatch/examples/, /features/). */
96+
export const TEST_PATH_PATTERN = /\/(test|spec|tests|nightwatch|examples?|features|step-definitions)\//i
9797

9898
/** Matches file names that follow the *.test.ts / *.spec.js naming convention. */
9999
export const TEST_FILE_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/i

packages/nightwatch-devtools/src/helpers/utils.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,20 @@ export function extractTestMetadata(filePath: string): TestFileMetadata {
150150
result.suiteLine = lineNum
151151
}
152152
}
153-
const m = line.match(/(?:it|test|specify)\s*\(\s*['"`]([^'"`]+)['"`]/)
154-
if (m) {
155-
result.testNames.push(m[1])
153+
// describe/it style: it('name', ...) / test('name', ...) / specify('name', ...)
154+
const itMatch = line.match(/(?:it|test|specify)\s*\(\s*['"`]([^'"`]+)['"`]/)
155+
if (itMatch) {
156+
result.testNames.push(itMatch[1])
157+
result.testLines.push(lineNum)
158+
continue
159+
}
160+
// Object-export style (NightwatchTests): 'Test name': () => { / 'Test name': function() {
161+
// This is the default format generated by `npx nightwatch --yes`
162+
const objMatch = line.match(
163+
/^\s*['"`]([^'"`]+)['"`]\s*:\s*(?:async\s+)?(?:\([^)]*\)\s*=>|function\s*\()/
164+
)
165+
if (objMatch) {
166+
result.testNames.push(objMatch[1])
156167
result.testLines.push(lineNum)
157168
}
158169
}

packages/nightwatch-devtools/src/index.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class NightwatchDevToolsPlugin {
6262
#failCount = 0
6363
#skipCount = 0
6464
#configPath: string | undefined
65+
#srcFolders: string[] = []
6566

6667
#getRerunLabel() {
6768
return process.env.DEVTOOLS_RERUN_ENTRY_TYPE === 'test'
@@ -240,6 +241,13 @@ class NightwatchDevToolsPlugin {
240241
const desiredCapabilities = browser.desiredCapabilities || {}
241242
const sessionId = browser.sessionId
242243
const opts = browser.options || {}
244+
245+
// Capture src_folders once so beforeEach can resolve test file paths
246+
if (this.#srcFolders.length === 0) {
247+
const sf = (opts as any).src_folders
248+
this.#srcFolders = Array.isArray(sf) ? sf : sf ? [sf] : []
249+
}
250+
243251
this.sessionCapturer.sendUpstream('metadata', {
244252
type: TraceType.Testrunner,
245253
capabilities,
@@ -418,7 +426,13 @@ class NightwatchDevToolsPlugin {
418426
const isRetry = existingIdx !== -1
419427
if (isRetry) {
420428
featureSuite.suites[existingIdx] = scenarioSuite
421-
this.sessionCapturer.sendUpstream('clearExecutionData', {})
429+
// Pass the specific scenario uid so only this scenario's execution data
430+
// is reset — a uid-less clearExecutionData would mark ALL suites as
431+
// running, destroying the previous terminal states of sibling scenarios.
432+
this.sessionCapturer.sendUpstream('clearExecutionData', {
433+
uid: scenarioUid,
434+
entryType: 'suite'
435+
})
422436
} else {
423437
featureSuite.suites.push(scenarioSuite)
424438
}
@@ -584,7 +598,29 @@ class NightwatchDevToolsPlugin {
584598

585599
if (!fullPath && testFile) {
586600
const workspaceRoot = process.cwd()
601+
// currentTest.module is the path relative to a src_folder, e.g. "basic/ecosia"
602+
// So we must try: path.join(cwd, srcFolder, module + '.js') for each src_folder
603+
const modulePath = (currentTest.module || '').replace(/\\/g, '/')
604+
const srcFolderPaths = this.#srcFolders.flatMap((sf) =>
605+
modulePath
606+
? [
607+
path.join(workspaceRoot, sf, modulePath + '.js'),
608+
path.join(workspaceRoot, sf, modulePath + '.ts'),
609+
path.join(workspaceRoot, sf, modulePath + '.cjs'),
610+
path.join(workspaceRoot, sf, modulePath),
611+
]
612+
: []
613+
)
587614
const possiblePaths = [
615+
// Highest priority: expand module path via each configured src_folder
616+
...srcFolderPaths,
617+
// Fallback: treat module path as relative to cwd (works when src_folders isn't nested)
618+
...(modulePath ? [
619+
path.join(workspaceRoot, modulePath + '.js'),
620+
path.join(workspaceRoot, modulePath + '.ts'),
621+
path.join(workspaceRoot, modulePath + '.cjs'),
622+
path.join(workspaceRoot, modulePath),
623+
] : []),
588624
path.join(workspaceRoot, 'example/tests', testFile + '.js'),
589625
path.join(workspaceRoot, 'example/tests', testFile),
590626
path.join(workspaceRoot, 'tests', testFile + '.js'),

packages/nightwatch-devtools/src/session.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -569,12 +569,17 @@ export class SessionCapturer {
569569
`
570570

571571
await browser.execute(injectionScript, [scriptContent])
572-
await browser.pause(300)
573572

574-
const checkResult = await browser.execute(
575-
'return typeof window.wdioTraceCollector !== "undefined"'
576-
)
577-
const hasCollector = ((checkResult as any)?.value ?? checkResult) === true
573+
// Poll for collector — the async IIFE may take a moment to initialise
574+
let hasCollector = false
575+
for (let attempt = 0; attempt < 5; attempt++) {
576+
await browser.pause(200)
577+
const checkResult = await browser.execute(
578+
'return typeof window.wdioTraceCollector !== "undefined"'
579+
)
580+
hasCollector = ((checkResult as any)?.value ?? checkResult) === true
581+
if (hasCollector) break
582+
}
578583

579584
if (hasCollector) {
580585
log.info('✓ Script injected and collector ready')

0 commit comments

Comments
 (0)