Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6b9f2bb
feat: Add Nightwatch DevTools plugin
vishnuv688 Feb 16, 2026
3b93f15
Enhanced command capture
vishnuv688 Feb 17, 2026
878579d
Command capture enhancement
vishnuv688 Feb 19, 2026
69225b1
Eslint fix
vishnuv688 Feb 25, 2026
84b60c0
Code refactor
vishnuv688 Mar 2, 2026
9558949
Console and network logs
vishnuv688 Mar 3, 2026
556ab34
Command capture enhancement for Cucumber test runner
vishnuv688 Mar 9, 2026
c494b89
Cucumber testrunner command capture fix
vishnuv688 Mar 10, 2026
b12bd8f
Eslint fix
vishnuv688 Mar 11, 2026
ada54fc
Test fix
vishnuv688 Mar 12, 2026
1c615a9
Code refactor
vishnuv688 Mar 13, 2026
f4a0812
TestLens - Nightwatch devtools
vishnuv688 Mar 17, 2026
b5af1bc
Snapshot feature
vishnuv688 Mar 18, 2026
09c38aa
Snapshot feature fix
vishnuv688 Mar 19, 2026
4e66bc5
Eslint fix
vishnuv688 Mar 24, 2026
0d90c8a
Eslint fix
vishnuv688 Mar 24, 2026
86fd363
Test rerun feature intial commit
vishnuv688 Mar 26, 2026
bdc5e42
Test rerun feature enhancement
vishnuv688 Mar 27, 2026
92555fe
Test rerun feature
vishnuv688 Mar 30, 2026
7959eb9
Test rerun feature enhancement
vishnuv688 Mar 31, 2026
7a87dc0
Eslint fix
vishnuv688 Apr 1, 2026
4a04808
Test failure fix
vishnuv688 Apr 2, 2026
eecaa3f
Code refactor
vishnuv688 Apr 3, 2026
638fce3
Test rerun test suite/case state fix
vishnuv688 Apr 6, 2026
0c786e6
Eslint fix
vishnuv688 Apr 7, 2026
7f6e252
Nightwatch devtools readme
vishnuv688 Apr 7, 2026
086ab5e
Main Readme update
vishnuv688 Apr 7, 2026
58862c9
Update pnpm-lock.yaml
vishnuv688 Apr 7, 2026
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
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# WebdriverIO DevTools

A powerful browser devtools extension for debugging, visualizing, and controlling WebdriverIO test executions in real-time.
A powerful browser devtools extension for debugging, visualizing, and controlling test executions in real-time.

Works with **WebdriverIO** and **[Nightwatch.js](./packages/nightwatch-devtools/README.md)** — same backend, same UI, same capture infrastructure.

## Features

Expand Down Expand Up @@ -106,14 +108,21 @@ pnpm build
pnpm demo
```

## Nightwatch Integration

Using [Nightwatch.js](https://nightwatchjs.org/)? A dedicated adapter package brings the same DevTools UI to your Nightwatch test suite with zero test code changes.

→ **[`@wdio/nightwatch-devtools`](./packages/nightwatch-devtools/README.md)** — installation, configuration, and Cucumber/BDD setup.

## Project Structure

```
packages/
├── app/ # Frontend Lit-based UI application
├── backend/ # Fastify server with test runner management
├── service/ # WebdriverIO service and reporter
└── script/ # Browser-injected trace collection script
├── app/ # Frontend Lit-based UI application
├── backend/ # Fastify server with test runner management
├── service/ # WebdriverIO service and reporter
├── script/ # Browser-injected trace collection script
└── nightwatch-devtools/ # Nightwatch adapter plugin
```

## Contributing
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ module.exports = [
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_' }
{ argsIgnorePattern: '^_', ignoreRestSiblings: true }
],
'@typescript-eslint/consistent-type-imports': 'error',
'no-unused-vars': 'off',
Expand Down
4 changes: 2 additions & 2 deletions example/wdio.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@ export const config: Options.Testrunner = {
capabilities: [
{
browserName: 'chrome',
browserVersion: '144.0.7559.60', // specify chromium browser version for testing
browserVersion: '146.0.7680.178', // specify chromium browser version for testing
'goog:chromeOptions': {
args: [
'--headless',
'--disable-gpu',
'--remote-allow-origins=*',
'--window-size=1280,800'
'--window-size=1600,1200'
]
}
// }, {
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
11 changes: 7 additions & 4 deletions packages/app/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ import { TraceType, type TraceLog } from '@wdio/devtools-service/types'
import { Element } from '@core/element'
import { DataManagerController } from './controller/DataManager.js'
import { DragController, Direction } from './utils/DragController.js'
import { SIDEBAR_MIN_WIDTH } from './controller/constants.js'

import './components/header.js'
import './components/sidebar.js'
import './components/workbench.js'
import './components/onboarding/start.js'

const SIDEBAR_MIN_WIDTH = 250

@customElement('wdio-devtools')
export class WebdriverIODevtoolsApplication extends Element {
dataManager = new DataManagerController(this)
Expand Down Expand Up @@ -71,8 +70,12 @@ export class WebdriverIODevtoolsApplication extends Element {
this.requestUpdate()
}

#clearExecutionData({ detail }: { detail?: { uid?: string } }) {
this.dataManager.clearExecutionData(detail?.uid)
#clearExecutionData({
detail
}: {
detail?: { uid?: string; entryType?: 'suite' | 'test' }
}) {
this.dataManager.clearExecutionData(detail?.uid, detail?.entryType)
}

#mainContent() {
Expand Down
127 changes: 96 additions & 31 deletions packages/app/src/components/browser/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,27 @@ import type { CommandLog } from '@wdio/devtools-service/types'

import {
mutationContext,
type TraceMutation,
metadataContext,
type Metadata
} from '../../controller/DataManager.js'
commandContext
} from '../../controller/context.js'
import type { Metadata } from '@wdio/devtools-service/types'

import '~icons/mdi/world.js'
import '../placeholder.js'

const MUTATION_SELECTOR = '__mutation-highlight__'

function transform(node: any): VNode<{}> {
if (typeof node !== 'object') {
if (typeof node !== 'object' || node === null) {
// Plain string/number text node — return as-is for Preact to render as text.
return node as VNode<{}>
}

const { children, ...props } = node.props
const { children, ...props } = node.props ?? {}
/**
* ToDo(Christian): fix way we collect data on added nodes in script
*/
if (!node.type && children.type) {
if (!node.type && children?.type) {
return transform(children)
}

Expand All @@ -44,13 +45,18 @@ const COMPONENT = 'wdio-devtools-browser'
export class DevtoolsBrowser extends Element {
#vdom = document.createDocumentFragment()
#activeUrl?: string
/** Base64 PNG of the screenshot for the currently selected command, or null. */
#screenshotData: string | null = null

@consume({ context: metadataContext, subscribe: true })
metadata: Metadata | undefined = undefined

@consume({ context: mutationContext, subscribe: true })
mutations: TraceMutation[] = []

@consume({ context: commandContext, subscribe: true })
commands: CommandLog[] = []

static styles = [
...Element.styles,
css`
Expand Down Expand Up @@ -112,6 +118,31 @@ export class DevtoolsBrowser extends Element {
border-radius: 0 0 0.5rem 0.5rem;
min-height: 0;
}

.screenshot-overlay {
position: absolute;
inset: 0;
background: #111;
display: flex;
align-items: flex-start;
justify-content: center;
border-radius: 0 0 0.5rem 0.5rem;
overflow: hidden;
}

.screenshot-overlay img {
max-width: 100%;
height: auto;
display: block;
}

.iframe-wrapper {
position: relative;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
`
]

Expand Down Expand Up @@ -148,9 +179,16 @@ export class DevtoolsBrowser extends Element {
return
}

// viewport may not be serialized yet (race between metadata message and
// first resize event), or may arrive without dimensions — fall back to
// sensible defaults so we never throw.
const viewportWidth = (metadata.viewport as any)?.width || 1280
const viewportHeight = (metadata.viewport as any)?.height || 800
if (!viewportWidth || !viewportHeight) {
return
}

this.iframe.removeAttribute('style')
const viewportWidth = metadata.viewport.width
const viewportHeight = metadata.viewport.height
const frameSize = this.getBoundingClientRect()
const headerSize = this.header.getBoundingClientRect()

Expand Down Expand Up @@ -178,23 +216,8 @@ export class DevtoolsBrowser extends Element {
)

async #renderCommandScreenshot(command?: CommandLog) {
const screenshot = command?.screenshot
if (!screenshot) {
return
}

if (!this.iframe) {
await this.updateComplete
}
if (!this.iframe) {
return
}

this.iframe.srcdoc = `
<body style="margin:0;background:#111;display:flex;justify-content:center;align-items:flex-start;">
<img src="data:image/png;base64,${screenshot}" style="max-width:100%;height:auto;display:block;" />
</body>
`
this.#screenshotData = command?.screenshot ?? null
this.requestUpdate()
}

async #renderNewDocument(doc: SimplifiedVNode, baseUrl: string) {
Expand Down Expand Up @@ -270,7 +293,12 @@ export class DevtoolsBrowser extends Element {

#handleChildListMutation(mutation: TraceMutation) {
if (mutation.addedNodes.length === 1 && !mutation.target) {
const baseUrl = this.metadata?.url || 'unknown'
// Prefer the URL embedded in the mutation itself (set by the injected script
// at capture time), then fall back to the already-resolved active URL, and
// finally to the context metadata URL. This avoids a race where metadata
// arrives after the first childList mutation fires #renderNewDocument.
const baseUrl =
mutation.url || this.#activeUrl || this.metadata?.url || 'unknown'
this.#renderNewDocument(
mutation.addedNodes[0] as SimplifiedVNode,
baseUrl
Expand Down Expand Up @@ -389,6 +417,19 @@ export class DevtoolsBrowser extends Element {
this.requestUpdate()
}

/** Latest screenshot from any command — auto-updates the preview as tests run. */
get #latestAutoScreenshot(): string | null {
if (!this.commands?.length) {
return null
}
for (let i = this.commands.length - 1; i >= 0; i--) {
if (this.commands[i].screenshot) {
return this.commands[i].screenshot!
}
}
return null
}

render() {
/**
* render a browser state if it hasn't before
Expand All @@ -398,6 +439,10 @@ export class DevtoolsBrowser extends Element {
this.#renderBrowserState()
}

const hasMutations = this.mutations && this.mutations.length
const autoScreenshot = hasMutations ? null : this.#latestAutoScreenshot
const displayScreenshot = this.#screenshotData ?? autoScreenshot

return html`
<section
class="w-full h-full bg-sideBarBackground rounded-lg border-2 border-panelBorder shadow-xl"
Expand All @@ -417,11 +462,31 @@ export class DevtoolsBrowser extends Element {
<span class="truncate">${this.#activeUrl}</span>
</div>
</header>
${this.mutations && this.mutations.length
? html`<iframe class="origin-top-left"></iframe>`
: html`<wdio-devtools-placeholder
style="height: 100%"
></wdio-devtools-placeholder>`}
${this.#screenshotData
? html` <div class="iframe-wrapper">
<div
class="screenshot-overlay"
style="position:relative;flex:1;min-height:0;"
>
<img src="data:image/png;base64,${this.#screenshotData}" />
</div>
</div>`
: hasMutations
? html` <div class="iframe-wrapper">
<iframe class="origin-top-left"></iframe>
</div>`
: displayScreenshot
? html` <div class="iframe-wrapper">
<div
class="screenshot-overlay"
style="position:relative;flex:1;min-height:0;"
>
<img src="data:image/png;base64,${displayScreenshot}" />
</div>
</div>`
: html`<wdio-devtools-placeholder
style="height: 100%"
></wdio-devtools-placeholder>`}
</section>
`
}
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import '~icons/mdi/moon-waning-crescent.js'
import '~icons/mdi/file-upload-outline.js'

import './inputs/traceLoader.js'
import { DARK_MODE_KEY } from '../controller/constants.js'

const DARK_MODE_KEY = 'darkMode'
const darkModeInitValue = localStorage.getItem(DARK_MODE_KEY)

@customElement('wdio-devtools-header')
Expand Down
5 changes: 1 addition & 4 deletions packages/app/src/components/onboarding/start.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Element } from '@core/element'
import { html, css } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { customElement } from 'lit/decorators.js'

import '../inputs/traceLoader.js'

Expand All @@ -23,9 +23,6 @@ export class DevtoolsStart extends Element {
`
]

@property()
onLoad = (content: any) => content

render() {
return html`
<div class="h-full flex-1 flex justify-center items-center bg-sideBarBackground">
Expand Down
19 changes: 17 additions & 2 deletions packages/app/src/components/sidebar/constants.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import { TestState } from './types.js'

export const STATE_MAP: Record<string, TestState> = {
running: TestState.RUNNING,
failed: TestState.FAILED,
passed: TestState.PASSED,
skipped: TestState.SKIPPED
}
import type { RunCapabilities } from './types.js'

export const DEFAULT_CAPABILITIES: RunCapabilities = {
canRunSuites: true,
canRunTests: true
canRunTests: true,
canRunAll: true
}

export const FRAMEWORK_CAPABILITIES: Record<string, RunCapabilities> = {
cucumber: { canRunSuites: true, canRunTests: false }
cucumber: { canRunSuites: true, canRunTests: false, canRunAll: true },
'nightwatch-cucumber': {
canRunSuites: true,
canRunTests: false,
canRunAll: false
},
nightwatch: { canRunSuites: true, canRunTests: true, canRunAll: false }
}
Loading
Loading