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
15 changes: 14 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,20 @@ dist-ssr
/*.ts
/*.json
*.tgz
example/wdio-*.json
examples/wdio/wdio-*.json
examples/wdio/wdio-*.webm
examples/nightwatch/logs/

# Adapter-encoded screencasts (written next to the project root by default)
selenium-video-*.webm
nightwatch-video-*.webm
packages/nightwatch-devtools/nightwatch-video-*.webm

# trace.zip output (mode: 'trace')
trace-*.zip

# vitest --coverage output
coverage/

# pnpm state, cache, logs, and debug files
/packages/**/*.mjs
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,27 @@ Real-time capture of browser-side events through the WebDriver BiDi protocol —

When BiDi is active in Selenium or Nightwatch, the per-command Chrome performance-log network-capture path is gated off so requests don't appear twice in the dashboard. The attach + sink logic lives in `@wdio/devtools-core`'s `bidi.ts` — same module both adapters consume.

### 📦 Trace mode (trace.zip)

Headless capture path — no DevTools UI window opens. At session end the adapter writes a `trace-<sessionId>.zip` next to the user's spec / config file, suitable for offline replay, AI-agent diffing, or any consumer that prefers a portable artifact over a live UI.

| Adapter | How to enable |
|---|---|
| **WebdriverIO** | `services: [['devtools', { mode: 'trace' }]]` |
| **Selenium** | `DevTools.configure({ mode: 'trace' })` (before importing `selenium-webdriver`) |
| **Nightwatch** | `globals: nightwatchDevtools({ mode: 'trace' })` |

The zip contains:
- `trace.trace` — NDJSON `context-options` + `before`/`after` action events
- `trace.network` — HAR-style network entries derived from the existing capture
- `resources/page@<id>-<ts>.jpeg` — screenshot per user-facing action
- `resources/elements-page@<id>-<ts>.json` — flat interactable element list from `@wdio/elements`
- `resources/snapshot-page@<id>-<ts>.txt` — depth-indented accessibility-tree snapshot (AI-friendly)

What counts as a user-facing action is filtered through an allow-list in `@wdio/devtools-core/action-mapping.ts` (`url`, `click`, `setValue`, `sendKeys`, `get`, etc.). Internal commands like `findElement`/`waitUntil`/`executeScript` don't produce trace entries.

Trace mode and live mode are **mutually exclusive** — `screencast` options are ignored in trace mode (live-mode feature). Live and trace serve different audiences (humans debugging vs. agents diffing), and stacking them only costs perf.

### 🔍︎ TestLens
- **Code Intelligence**: View test definitions directly in your editor
- **Run/Debug Actions**: Execute individual tests or suites with inline CodeLens actions
Expand Down
1 change: 1 addition & 0 deletions examples/nightwatch/nightwatch.conf.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ module.exports = {
// off to avoid duplicate entries.
globals: nightwatchDevtools({
port: 3000,
mode: 'trace',
screencast: { enabled: true, pollIntervalMs: 200 },
bidi: true
})
Expand Down
2 changes: 1 addition & 1 deletion examples/selenium/jest-test/test/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const VALID_USERNAME = 'tomsmith'
const VALID_PASSWORD = 'SuperSecretPassword!'

DevTools.configure({
screencast: { enabled: true, quality: 70, maxWidth: 1280, maxHeight: 720 },
mode: 'trace',
headless: true
})

Expand Down
16 changes: 1 addition & 15 deletions examples/wdio/wdio.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,21 +127,7 @@ export const config: Options.Testrunner = {
// Services take over a specific job you don't want to take care of. They enhance
// your test setup with almost no effort. Unlike plugins, they don't add new
// commands. Instead, they hook themselves up into the test process.
services: [
'devtools'
// [
// 'devtools',
// {
// screencast: {
// enabled: true,
// captureFormat: 'jpeg', // 'jpeg' or 'png' — frame format sent by Chrome over CDP
// quality: 70, // JPEG quality 0–100
// maxWidth: 1280, // max frame width in px
// maxHeight: 720 // max frame height in px
// }
// }
// ]
],
services: [['devtools', { mode: 'trace' as const }]],
//
// Framework you want to run your specs with.
// The following are supported: Mocha, Jasmine, and Cucumber
Expand Down
4 changes: 3 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
"license": "MIT",
"devDependencies": {
"@types/ws": "^8.18.1",
"@types/yazl": "^2.4.6",
"@wdio/devtools-script": "workspace:*",
"@wdio/devtools-shared": "workspace:^",
"stacktrace-parser": "^0.1.11",
"ws": "^8.21.0"
"ws": "^8.21.0",
"yazl": "^2.5.1"
}
}
65 changes: 65 additions & 0 deletions packages/core/src/action-mapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Allow-list mapping from runner-native command names to trace
// vocabulary. Ported from Vince Graics' PR #209 (`@wdio/tracing-service`); the
// existing devtools UI uses its own denylist (`INTERNAL_COMMANDS`) — this map
// is for the trace.zip exporter to filter + rename in one step.

export interface TraceAction {
class: string
method: string
}

const ACTION_MAP: Record<string, TraceAction> = {
// WDIO browser-level
url: { class: 'Page', method: 'navigate' },
navigateTo: { class: 'Page', method: 'navigate' },
back: { class: 'Page', method: 'goBack' },
forward: { class: 'Page', method: 'goForward' },
refresh: { class: 'Page', method: 'reload' },
newWindow: { class: 'Page', method: 'goto' },
// Selenium WebDriver navigation (driver.get, driver.navigate().to/back/forward/refresh)
get: { class: 'Page', method: 'navigate' },
to: { class: 'Page', method: 'navigate' },
// WDIO element-level
click: { class: 'Element', method: 'click' },
doubleClick: { class: 'Element', method: 'dblclick' },
setValue: { class: 'Element', method: 'fill' },
selectByVisibleText: { class: 'Element', method: 'selectOption' },
moveTo: { class: 'Element', method: 'hover' },
scrollIntoView: { class: 'Element', method: 'scrollIntoViewIfNeeded' },
dragAndDrop: { class: 'Element', method: 'dragTo' },
// Selenium WebElement actions
sendKeys: { class: 'Element', method: 'fill' },
clear: { class: 'Element', method: 'clear' },
submit: { class: 'Element', method: 'submit' },
// Cross-runner
keys: { class: 'Keyboard', method: 'press' },
execute: { class: 'Page', method: 'evaluate' },
executeAsync: { class: 'Page', method: 'evaluate' },
switchToFrame: { class: 'Frame', method: 'goto' },
touchAction: { class: 'Element', method: 'tap' }
}

// Excluded by design:
// clearValue / addValue — WDIO fires these inside setValue (duplicate events).
// executeScript — Selenium's `until` polling fires it ~50ms; also recurses
// because @wdio/elements uses executeScript inside captureActionSnapshot.
// WDIO's user-facing `execute`/`executeAsync` are still captured.

export function mapCommandToAction(command: string): TraceAction | null {
return ACTION_MAP[command] ?? null
}

export function formatActionTitle(
action: TraceAction,
args: unknown[],
params?: Record<string, unknown>
): string {
const firstArg = args[0] ?? params?.selector
if (firstArg === undefined) {
return `${action.class}.${action.method}()`
}
const label = (
typeof firstArg === 'object' ? JSON.stringify(firstArg) : String(firstArg)
).slice(0, 80)
return `${action.class}.${action.method}("${label}")`
}
4 changes: 4 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Framework-agnostic capture/reporter logic shared by @wdio/devtools-*
// adapters. See ARCHITECTURE.md §2 and CLAUDE.md §2.2.

export * from './action-mapping.js'
export * from './assert-patcher.js'
export * from './trace-exporter.js'
export * from './trace-har.js'
export * from './trace-zip-writer.js'
export * from './bidi.js'
export * from './console.js'
export * from './uid.js'
Expand Down
Loading
Loading