Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/wdio.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const config: Options.Testrunner = {
capabilities: [
{
browserName: 'chrome',
browserVersion: '144.0.7559.60', // specify chromium browser version for testing
browserVersion: '144.0.7559.133', // specify chromium browser version for testing
'goog:chromeOptions': {
args: [
'--headless',
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"scripts": {
"build": "pnpm --parallel build",
"demo": "wdio run ./example/wdio.conf.ts",
"demo:nightwatch": "pnpm --filter @wdio/nightwatch-devtools example",
"dev": "pnpm --parallel dev",
"preview": "pnpm --parallel preview",
"test": "vitest run",
Expand All @@ -17,7 +18,10 @@
"pnpm": {
"overrides": {
"vite": "^7.3.0"
}
},
"ignoredBuiltDependencies": [
"chromedriver"
]
},
"devDependencies": {
"@types/node": "^25.0.3",
Expand Down
88 changes: 74 additions & 14 deletions packages/app/src/components/sidebar/explorer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Element } from '@core/element'
import { html, css, nothing, type TemplateResult } from 'lit'
import { customElement } from 'lit/decorators.js'
import { customElement, property } from 'lit/decorators.js'
import { consume } from '@lit/context'
import type { TestStats, SuiteStats } from '@wdio/reporter'
import type { Metadata } from '@wdio/devtools-service/types'
Expand Down Expand Up @@ -31,6 +31,13 @@ import type { DevtoolsSidebarFilter } from './filter.js'

const EXPLORER = 'wdio-devtools-sidebar-explorer'

const STATE_MAP: Record<string, TestState> = {
'running': TestState.RUNNING,
'failed': TestState.FAILED,
'passed': TestState.PASSED,
'skipped': TestState.SKIPPED
}

@customElement(EXPLORER)
export class DevtoolsSidebarExplorer extends CollapseableEntry {
#testFilter: DevtoolsSidebarFilter | undefined
Expand Down Expand Up @@ -63,6 +70,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
]

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

@consume({ context: metadataContext, subscribe: true })
Expand All @@ -71,6 +79,10 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
@consume({ context: isTestRunningContext, subscribe: true })
isTestRunning = false

updated(changedProperties: Map<string | number | symbol, unknown>) {
super.updated(changedProperties)
}

connectedCallback(): void {
super.connectedCallback()
window.addEventListener('app-test-filter', this.#filterListener)
Expand Down Expand Up @@ -285,6 +297,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
feature-file="${entry.featureFile || ''}"
feature-line="${entry.featureLine ?? ''}"
suite-type="${entry.suiteType || ''}"
?has-children="${entry.children && entry.children.length > 0}"
.runDisabled=${this.#isRunDisabled(entry)}
.runDisabledReason=${this.#getRunDisabledReason(entry)}
>
Expand Down Expand Up @@ -326,18 +339,64 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
)
}

#isRunning(entry: TestStats | SuiteStats): boolean {
if ('tests' in entry) {
// Check if any immediate test is running
if (entry.tests.some((t) => !t.end)) {
return true
}
// Check if any nested suite is running
if (entry.suites.some((s) => this.#isRunning(s))) {
return true
}
return false
}
// For individual tests, check if end is not set
return !entry.end
}

#hasFailed(entry: TestStats | SuiteStats): boolean {
if ('tests' in entry) {
// Check if any immediate test failed
if (entry.tests.find((t) => t.state === 'failed')) {
return true
}
// Check if any nested suite has failures
if (entry.suites.some((s) => this.#hasFailed(s))) {
return true
}
return false
}
// For individual tests
return entry.state === 'failed'
}

#computeEntryState(entry: TestStats | SuiteStats): TestState {
const state = (entry as any).state

// Check explicit state first
const mappedState = STATE_MAP[state]
if (mappedState) return mappedState

// For suites, compute state from children
if ('tests' in entry) {
if (this.#isRunning(entry)) return TestState.RUNNING
if (this.#hasFailed(entry)) return TestState.FAILED
return TestState.PASSED
}

// For individual tests, check if still running
return !entry.end ? TestState.RUNNING : TestState.PASSED
}

#getTestEntry(entry: TestStats | SuiteStats): TestEntry {
if ('tests' in entry) {
const entries = [...entry.tests, ...entry.suites]
return {
uid: entry.uid,
label: entry.title,
type: 'suite',
state: entry.tests.some((t) => !t.end)
? TestState.RUNNING
: entry.tests.find((t) => t.state === 'failed')
? TestState.FAILED
: TestState.PASSED,
state: this.#computeEntryState(entry),
callSource: (entry as any).callSource,
specFile: (entry as any).file,
fullTitle: entry.title,
Expand All @@ -353,11 +412,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
uid: entry.uid,
label: entry.title,
type: 'test',
state: !entry.end
? TestState.RUNNING
: entry.state === 'failed'
? TestState.FAILED
: TestState.PASSED,
state: this.#computeEntryState(entry),
callSource: (entry as any).callSource,
specFile: (entry as any).file,
fullTitle: (entry as any).fullTitle || entry.title,
Expand Down Expand Up @@ -421,9 +476,14 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry {
(suite) => suite.uid,
(suite) => this.#renderEntry(suite)
)
: html`<p class="text-disabledForeground text-sm px-4 py-2">
No tests found
</p>`}
: html`<div class="text-sm px-4 py-2">
<p class="text-disabledForeground">No tests to display</p>
<p class="text-xs text-disabledForeground mt-2">
Debug: suites=${this.suites?.length || 0},
rootSuites=${uniqueSuites.length},
filtered=${suites.length}
</p>
</div>`}
</wdio-test-suite>
`
}
Expand Down
6 changes: 4 additions & 2 deletions packages/app/src/components/sidebar/test-suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ export class ExplorerTestEntry extends CollapseableEntry {
@property({ type: String, attribute: 'suite-type' })
suiteType?: string

@property({ type: Boolean, attribute: 'has-children' })
hasChildren = false

static styles = [
...Element.styles,
css`
Expand Down Expand Up @@ -206,8 +209,7 @@ export class ExplorerTestEntry extends CollapseableEntry {
}

render() {
const hasNoChildren =
this.querySelectorAll('[slot="children"]').length === 0
const hasNoChildren = !this.hasChildren
const isCollapsed = this.isCollapsed === 'true'
const runTooltip = this.runDisabled
? this.runDisabledReason ||
Expand Down
12 changes: 10 additions & 2 deletions packages/app/src/controller/DataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ export const isTestRunningContext = createContext<boolean>(
)

interface SocketMessage<
T extends keyof TraceLog | 'testStopped' = keyof TraceLog | 'testStopped'
T extends keyof TraceLog | 'testStopped' | 'clearExecutionData' = keyof TraceLog | 'testStopped' | 'clearExecutionData'
> {
scope: T
data: T extends keyof TraceLog ? TraceLog[T] : unknown
data: T extends keyof TraceLog ? TraceLog[T] : T extends 'clearExecutionData' ? { uid?: string } : unknown
}

export class DataManagerController implements ReactiveController {
Expand Down Expand Up @@ -270,6 +270,14 @@ export class DataManagerController implements ReactiveController {
return
}

// Handle clear execution data event (when tests change)
if (scope === 'clearExecutionData') {
const clearData = data as { uid?: string }
this.clearExecutionData(clearData.uid)
this.#host.requestUpdate()
return
}

// Check for new run BEFORE processing suites data
if (scope === 'suites') {
const shouldReset = this.#shouldResetForNewRun(data)
Expand Down
52 changes: 48 additions & 4 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,17 @@ export function broadcastToClients(message: string) {
})
}

export async function start(opts: DevtoolsBackendOptions = {}) {
export async function start(opts: DevtoolsBackendOptions = {}): Promise<{ server: FastifyInstance; port: number }> {
const host = opts.hostname || 'localhost'
const port = opts.port || (await getPort({ port: DEFAULT_PORT }))
// Use getPort to find an available port, starting with the preferred port
const preferredPort = opts.port || DEFAULT_PORT
const port = await getPort({ port: preferredPort })

// Log if we had to use a different port
if (opts.port && port !== opts.port) {
log.warn(`Port ${opts.port} is already in use, using port ${port} instead`)
}

const appPath = await getDevtoolsApp()

server = Fastify({ logger: true })
Expand Down Expand Up @@ -91,6 +99,34 @@ export async function start(opts: DevtoolsBackendOptions = {}) {
log.info(
`received ${message.length} byte message from worker to ${clients.size} client${clients.size > 1 ? 's' : ''}`
)

// Parse message to check if it's a clearCommands message
try {
const parsed = JSON.parse(message.toString())

// If this is a clearCommands message, transform it to clear-execution-data format
if (parsed.scope === 'clearCommands') {
const testUid = parsed.data?.testUid
log.info(`Clearing commands for test: ${testUid || 'all'}`)

// Create a synthetic message that DataManager will understand
const clearMessage = JSON.stringify({
scope: 'clearExecutionData',
data: { uid: testUid }
})

clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(clearMessage)
}
})
return
}
} catch (e) {
// Not JSON or parsing failed, forward as-is
}

// Forward all other messages as-is
clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message.toString())
Expand All @@ -102,7 +138,7 @@ export async function start(opts: DevtoolsBackendOptions = {}) {

log.info(`Starting WebdriverIO Devtools application on port ${port}`)
await server.listen({ port, host })
return server
return { server, port }
}

export async function stop() {
Expand All @@ -111,8 +147,16 @@ export async function stop() {
}

log.info('Shutting down WebdriverIO Devtools application')
await server.close()

// Close all WebSocket connections first
clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN || client.readyState === WebSocket.CONNECTING) {
client.terminate()
}
})
clients.clear()

await server.close()
}

/**
Expand Down
36 changes: 36 additions & 0 deletions packages/nightwatch-devtools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Build output
dist/
*.tsbuildinfo

# Dependencies
node_modules/

# Test outputs
tests_output/
logs/
example/logs/

# Trace files
*-trace-*.json
nightwatch-trace-*.json

# Log files
*.log
npm-debug.log*
pnpm-debug.log*

# IDE
.idea/
.vscode/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Temporary files
*.tmp
*.temp
*.bak
Loading
Loading