Skip to content

Commit c122e74

Browse files
committed
feat: Add Nightwatch DevTools plugin
- Complete Nightwatch adapter for WebdriverIO DevTools - Auto-opening browser UI with visual test debugging - Command tracking with retry deduplication - Performance data capture (80% solution without CDP) - Suite title extraction from describe() blocks - All tests displayed correctly in UI - Supporting changes to backend, service, and app packages
1 parent 6708695 commit c122e74

27 files changed

Lines changed: 4729 additions & 25 deletions

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: '144.0.7559.60', // specify chromium browser version for testing
66+
browserVersion: '144.0.7559.133', // specify chromium browser version for testing
6767
'goog:chromeOptions': {
6868
args: [
6969
'--headless',

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"scripts": {
55
"build": "pnpm --parallel build",
66
"demo": "wdio run ./example/wdio.conf.ts",
7+
"demo:nightwatch": "pnpm --filter @wdio/nightwatch-devtools example",
78
"dev": "pnpm --parallel dev",
89
"preview": "pnpm --parallel preview",
910
"test": "vitest run",
@@ -17,7 +18,10 @@
1718
"pnpm": {
1819
"overrides": {
1920
"vite": "^7.3.0"
20-
}
21+
},
22+
"ignoredBuiltDependencies": [
23+
"chromedriver"
24+
]
2125
},
2226
"devDependencies": {
2327
"@types/node": "^25.0.3",

packages/app/src/components/sidebar/explorer.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Element } from '@core/element'
22
import { html, css, nothing, type TemplateResult } from 'lit'
3-
import { customElement } from 'lit/decorators.js'
3+
import { customElement, property } from 'lit/decorators.js'
44
import { consume } from '@lit/context'
55
import type { TestStats, SuiteStats } from '@wdio/reporter'
66
import type { Metadata } from '@wdio/devtools-service/types'
@@ -63,6 +63,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
6363
]
6464

6565
@consume({ context: suiteContext, subscribe: true })
66+
@property({ type: Array })
6667
suites: Record<string, SuiteStats>[] | undefined = undefined
6768

6869
@consume({ context: metadataContext, subscribe: true })
@@ -71,6 +72,10 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
7172
@consume({ context: isTestRunningContext, subscribe: true })
7273
isTestRunning = false
7374

75+
updated(changedProperties: Map<string | number | symbol, unknown>) {
76+
super.updated(changedProperties)
77+
}
78+
7479
connectedCallback(): void {
7580
super.connectedCallback()
7681
window.addEventListener('app-test-filter', this.#filterListener)
@@ -285,6 +290,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
285290
feature-file="${entry.featureFile || ''}"
286291
feature-line="${entry.featureLine ?? ''}"
287292
suite-type="${entry.suiteType || ''}"
293+
?has-children="${entry.children && entry.children.length > 0}"
288294
.runDisabled=${this.#isRunDisabled(entry)}
289295
.runDisabledReason=${this.#getRunDisabledReason(entry)}
290296
>
@@ -326,16 +332,48 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
326332
)
327333
}
328334

335+
#isRunning(entry: TestStats | SuiteStats): boolean {
336+
if ('tests' in entry) {
337+
// Check if any immediate test is running
338+
if (entry.tests.some((t) => !t.end)) {
339+
return true
340+
}
341+
// Check if any nested suite is running
342+
if (entry.suites.some((s) => this.#isRunning(s))) {
343+
return true
344+
}
345+
return false
346+
}
347+
// For individual tests, check if end is not set
348+
return !entry.end
349+
}
350+
351+
#hasFailed(entry: TestStats | SuiteStats): boolean {
352+
if ('tests' in entry) {
353+
// Check if any immediate test failed
354+
if (entry.tests.find((t) => t.state === 'failed')) {
355+
return true
356+
}
357+
// Check if any nested suite has failures
358+
if (entry.suites.some((s) => this.#hasFailed(s))) {
359+
return true
360+
}
361+
return false
362+
}
363+
// For individual tests
364+
return entry.state === 'failed'
365+
}
366+
329367
#getTestEntry(entry: TestStats | SuiteStats): TestEntry {
330368
if ('tests' in entry) {
331369
const entries = [...entry.tests, ...entry.suites]
332370
return {
333371
uid: entry.uid,
334372
label: entry.title,
335373
type: 'suite',
336-
state: entry.tests.some((t) => !t.end)
374+
state: this.#isRunning(entry)
337375
? TestState.RUNNING
338-
: entry.tests.find((t) => t.state === 'failed')
376+
: this.#hasFailed(entry)
339377
? TestState.FAILED
340378
: TestState.PASSED,
341379
callSource: (entry as any).callSource,
@@ -421,9 +459,14 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
421459
(suite) => suite.uid,
422460
(suite) => this.#renderEntry(suite)
423461
)
424-
: html`<p class="text-disabledForeground text-sm px-4 py-2">
425-
No tests found
426-
</p>`}
462+
: html`<div class="text-sm px-4 py-2">
463+
<p class="text-disabledForeground">No tests to display</p>
464+
<p class="text-xs text-disabledForeground mt-2">
465+
Debug: suites=${this.suites?.length || 0},
466+
rootSuites=${uniqueSuites.length},
467+
filtered=${suites.length}
468+
</p>
469+
</div>`}
427470
</wdio-test-suite>
428471
`
429472
}

packages/app/src/components/sidebar/test-suite.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ export class ExplorerTestEntry extends CollapseableEntry {
8080
@property({ type: String, attribute: 'suite-type' })
8181
suiteType?: string
8282

83+
@property({ type: Boolean, attribute: 'has-children' })
84+
hasChildren = false
85+
8386
static styles = [
8487
...Element.styles,
8588
css`
@@ -206,8 +209,7 @@ export class ExplorerTestEntry extends CollapseableEntry {
206209
}
207210

208211
render() {
209-
const hasNoChildren =
210-
this.querySelectorAll('[slot="children"]').length === 0
212+
const hasNoChildren = !this.hasChildren
211213
const isCollapsed = this.isCollapsed === 'true'
212214
const runTooltip = this.runDisabled
213215
? this.runDisabledReason ||

packages/app/src/controller/DataManager.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ export const isTestRunningContext = createContext<boolean>(
4949
)
5050

5151
interface SocketMessage<
52-
T extends keyof TraceLog | 'testStopped' = keyof TraceLog | 'testStopped'
52+
T extends keyof TraceLog | 'testStopped' | 'clearExecutionData' = keyof TraceLog | 'testStopped' | 'clearExecutionData'
5353
> {
5454
scope: T
55-
data: T extends keyof TraceLog ? TraceLog[T] : unknown
55+
data: T extends keyof TraceLog ? TraceLog[T] : T extends 'clearExecutionData' ? { uid?: string } : unknown
5656
}
5757

5858
export class DataManagerController implements ReactiveController {
@@ -270,6 +270,14 @@ export class DataManagerController implements ReactiveController {
270270
return
271271
}
272272

273+
// Handle clear execution data event (when tests change)
274+
if (scope === 'clearExecutionData') {
275+
const clearData = data as { uid?: string }
276+
this.clearExecutionData(clearData.uid)
277+
this.#host.requestUpdate()
278+
return
279+
}
280+
273281
// Check for new run BEFORE processing suites data
274282
if (scope === 'suites') {
275283
const shouldReset = this.#shouldResetForNewRun(data)

packages/backend/src/index.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,15 @@ export function broadcastToClients(message: string) {
3232

3333
export async function start(opts: DevtoolsBackendOptions = {}) {
3434
const host = opts.hostname || 'localhost'
35-
const port = opts.port || (await getPort({ port: DEFAULT_PORT }))
35+
// Use getPort to find an available port, starting with the preferred port
36+
const preferredPort = opts.port || DEFAULT_PORT
37+
const port = await getPort({ port: preferredPort })
38+
39+
// Log if we had to use a different port
40+
if (opts.port && port !== opts.port) {
41+
log.warn(`Port ${opts.port} is already in use, using port ${port} instead`)
42+
}
43+
3644
const appPath = await getDevtoolsApp()
3745

3846
server = Fastify({ logger: true })
@@ -91,6 +99,34 @@ export async function start(opts: DevtoolsBackendOptions = {}) {
9199
log.info(
92100
`received ${message.length} byte message from worker to ${clients.size} client${clients.size > 1 ? 's' : ''}`
93101
)
102+
103+
// Parse message to check if it's a clearCommands message
104+
try {
105+
const parsed = JSON.parse(message.toString())
106+
107+
// If this is a clearCommands message, transform it to clear-execution-data format
108+
if (parsed.scope === 'clearCommands') {
109+
const testUid = parsed.data?.testUid
110+
log.info(`Clearing commands for test: ${testUid || 'all'}`)
111+
112+
// Create a synthetic message that DataManager will understand
113+
const clearMessage = JSON.stringify({
114+
scope: 'clearExecutionData',
115+
data: { uid: testUid }
116+
})
117+
118+
clients.forEach((client) => {
119+
if (client.readyState === WebSocket.OPEN) {
120+
client.send(clearMessage)
121+
}
122+
})
123+
return
124+
}
125+
} catch (e) {
126+
// Not JSON or parsing failed, forward as-is
127+
}
128+
129+
// Forward all other messages as-is
94130
clients.forEach((client) => {
95131
if (client.readyState === WebSocket.OPEN) {
96132
client.send(message.toString())
@@ -111,8 +147,16 @@ export async function stop() {
111147
}
112148

113149
log.info('Shutting down WebdriverIO Devtools application')
114-
await server.close()
150+
151+
// Close all WebSocket connections first
152+
clients.forEach((client) => {
153+
if (client.readyState === WebSocket.OPEN || client.readyState === WebSocket.CONNECTING) {
154+
client.terminate()
155+
}
156+
})
115157
clients.clear()
158+
159+
await server.close()
116160
}
117161

118162
/**
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Build output
2+
dist/
3+
*.tsbuildinfo
4+
5+
# Dependencies
6+
node_modules/
7+
8+
# Test outputs
9+
tests_output/
10+
logs/
11+
example/logs/
12+
13+
# Trace files
14+
*-trace-*.json
15+
nightwatch-trace-*.json
16+
17+
# Log files
18+
*.log
19+
npm-debug.log*
20+
pnpm-debug.log*
21+
22+
# IDE
23+
.idea/
24+
.vscode/
25+
*.swp
26+
*.swo
27+
*~
28+
29+
# OS
30+
.DS_Store
31+
Thumbs.db
32+
33+
# Temporary files
34+
*.tmp
35+
*.temp
36+
*.bak

0 commit comments

Comments
 (0)