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
7 changes: 7 additions & 0 deletions .changeset/show-trace-bin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@wdio/devtools-service': patch
'@wdio/selenium-devtools': patch
'@wdio/nightwatch-devtools': patch
---

Ship the `show-trace` bin the package has always declared. The file was never committed, so `npx show-trace <trace.zip>` failed in a project that installed only this adapter.
10 changes: 10 additions & 0 deletions .changeset/snapshot-matches-the-wdio-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@wdio/elements": major
"@wdio/devtools-service": major
---

`getSnapshot` now produces the same text as `wdio session snapshot --selectors`, by delegating to `@wdio/snapshot` instead of keeping a second renderer that drifted from the CLI's. Agents that read the CLI's output and agents that call this function now see one format.

Breaking: it returns `{ text, refs }` rather than `{ text, elements }`, takes `{ viewport }` (default `false`, the full page) in place of `{ inViewportOnly }` (default `true`), no longer prefixes a `[Page: …]` header, and throws on failure instead of returning a placeholder. `serializeWebSnapshot`, `serializeMobileSnapshot` and their option and result types are removed. Requires `webdriverio` ^10, and declares `engines.node` `>=22.19.0`. A mobile browser session (Chrome on Android, Safari on iOS) is now snapshotted from the page rather than from Appium's page source, and Mac and Windows native sessions take the native path. `getElements` on a native session no longer attaches the raw page source as `tree.attributes._sourceXML`.

Breaking for `@wdio/devtools-service`: it now requires WebdriverIO 10. It consumes `@wdio/elements` 2, which delegates to `@wdio/snapshot`, and that package pins `webdriverio` exactly to a v10 release, so the service's peer ranges for `webdriverio`, `@wdio/types`, `@wdio/reporter`, `@wdio/logger`, `@wdio/protocols` and `@wdio/allure-reporter` are narrowed to `^10.0.0`. Its `browser.getSnapshot()` returns the same `{ text, refs }` instead of `{ text, elements }`, and the `inViewportOnly` option is replaced by `viewport`. The service still defaults to the viewport (`viewport: true`) so repeated calls from an agent loop stay small, whereas calling `getSnapshot` from `@wdio/elements` directly defaults to the full page.
7 changes: 7 additions & 0 deletions .changeset/wdio-10-1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@wdio/devtools-backend": patch
"@wdio/selenium-devtools": patch
"@wdio/nightwatch-devtools": patch
---

The `@wdio/logger` each package depends on, and the `webdriverio` the Selenium and Nightwatch adapters depend on, move to their 10.1.0 lines (`^10.1.0`), alongside the rest of the WebdriverIO toolchain the repository now builds and tests against. An install that pinned either to 10.0.x resolves 10.1.x from now on.
6 changes: 3 additions & 3 deletions .github/scripts/wdio-peer-smoke/smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import DevToolsHookService, {
setupForDevtools,
type ServiceOptions
} from '@wdio/devtools-service'
import { getElements, getSnapshot, type SnapshotResult } from '@wdio/elements'
import { getElements, getSnapshot, type GetSnapshotResult } from '@wdio/elements'
import { xmlToJSON } from '@wdio/elements/locators'

const serviceOptions: ServiceOptions = {
Expand Down Expand Up @@ -35,8 +35,8 @@ export async function standalone(opts: Options.WebdriverIO): Promise<void> {

export async function elements(
browser: WebdriverIO.Browser
): Promise<SnapshotResult> {
): Promise<GetSnapshotResult> {
await getElements(browser, { inViewportOnly: false })
xmlToJSON('<hierarchy />')
return getSnapshot(browser, { inViewportOnly: false })
return getSnapshot(browser, { viewport: false })
}
11 changes: 5 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,17 @@ jobs:
- name: 🧩 Component Tests
run: pnpm test:ui --maxInstances 2

# The workspace develops against one WebdriverIO major, but the service and
# @wdio/elements declare peers on two. Each published major is installed from
# npm into a scratch project beside the packed tarballs, so a peer conflict, a
# broken entry point or a type mismatch with that major fails here first. 9
# runs too so a red 10 can be told apart from a broken harness.
# The service and @wdio/elements are WebdriverIO-10-only (@wdio/snapshot pins
# webdriverio exactly). The published major is installed from npm into a
# scratch project beside the packed tarballs, so a peer conflict, a broken
# entry point or a type mismatch with that major fails here first.
wdio-peer:
name: wdio-peer (webdriverio ${{ matrix.wdio }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
wdio: [9, 10]
wdio: [10]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Use Node.js
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ coverage/

# pnpm state, cache, logs, and debug files
/packages/**/*.mjs
!/packages/*/bin/*.mjs

.claude

Expand All @@ -72,3 +73,7 @@ packages/**/allure-results*/
packages/**/allure-report*/
examples/wdio/cucumber/wdio.allure.conf.ts

# Written on purpose by examples/wdio/mocha/specs/wdio.e2e.ts
examples/wdio/screenshot.jpg
examples/wdio/snapshot-full.txt
examples/wdio/snapshot-viewport.txt
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ The execution environment is the browser, not Node, so this package cannot impor

The WDIO-typed element-detection API (`@wdio/elements`): `getSnapshot`, `getElements`, the accessibility-tree and interactable-element readers. Unlike `shared` and `core` this package **is** published, so its exported names are public API.

Imports from: `core`. The script bodies, serializers, types, and locator generation all live in `core` (`element-scripts.ts`, `element-snapshot.ts`, `element-types.ts`, `locators/`) because they're framework-agnostic; this package holds only the wrappers that take a `WebdriverIO.Browser` — which `core` can't type against. The arrow never reverses: `core` does not import `elements`, and the adapters' per-action capture (`captureActionSnapshot`) calls core's script bodies directly rather than going through this package. `elements/snapshot.ts` and `elements/locators/index.ts` are re-export shims kept for API stability.
Imports from: `core`. The script bodies, serializers, types, and locator generation all live in `core` (`element-scripts.ts`, `element-snapshot.ts`, `element-types.ts`, `locators/`) because they're framework-agnostic; this package holds only the wrappers that take a `WebdriverIO.Browser` — which `core` can't type against. The arrow never reverses: `core` does not import `elements`, and the adapters' per-action capture (`captureActionSnapshot`) calls core's script bodies directly rather than going through this package. `elements/locators/index.ts` is a re-export shim kept for API stability.

The DOM-walking scripts run in the page via `browser.execute`, so — like `script` — they avoid Node-only APIs.

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ Documented divergences from the conventions above. They exist today as debt to b
- Relatedly, cucumber's **per-step** `resetCommandTracking()` was wiping the native-assert buffer, so each scenario kept only its *last* step's assertions (measured 1 of 2). The buffer is per **test unit** — `resetTestTracking()` at `wrapBrowserOnce` now clears it.
- **DOM `mutations` ARE captured**, and the old `ECONNRESET` / "collector not found" attribution is **wrong** — neither appears any more. The gap closed itself with document-start injection (`core/bidi-preload.ts`), confirmed on a *baseline* build so the credit belongs to that change and not to anything here. Measured per scenario with `traceGranularity:'test'` + `webSocketUrl: true`: 39 and 19 mutation entries, 2 DOM anchors each, **0 of 19 rows on the wrong document**. What remains is noise, not a gap: the screencast poller issues `/screenshot` every 200 ms into the session Nightwatch quits per scenario, logging 9–26 `WARN webdriverHttp: … socket hang up` per run. Fix is to stop the recorder before the per-scenario quit, or suppress the warn for a session in teardown.
- **A tsup entry that another entry imports is not a leaf, so an `import.meta.url === process.argv[1]` self-start check inside it is dead code; a CLI has to be its own leaf entry.** tsup hoists a module body shared by two entries into `dist/chunk-*.js`, and there `import.meta.url` is the chunk's path, which can never equal `process.argv[1]`. `packages/backend/src/show-trace.ts` imports `start` from `index.ts`, which is what made index shared, so index's old "start if run directly" guard was dead in **every** build: `node dist/index.js` exited 0 without ever serving, while `dist/show-trace.js` self-started correctly for exactly the same reason inverted, being a leaf whose body stays in its own output file (it also compares realpaths, because the invoked path is the `node_modules/.bin` symlink). The live dashboard server is therefore its own leaf entry, `packages/backend/src/server.ts` (shebang, built to an executable `dist/server.js`, shipped as the `devtools-backend` bin, accepting `--port`, `--hostname`, `-h`/`--help`), and `index.ts` stays library-only for the three adapters' in-process `start`/`stop`. Same family as the `cucumberHooks.cts` entry above: the tsup entry list is part of the contract, and in both cases the symptom was silence rather than an error.
- **A captured text locator is generated in the recording runner's dialect; every other branch is portable CSS.** `shared/locator-dialect.ts` `locatorDialect(runner)` is the one fact table — WDIO runners (`mocha`/`jasmine`/`cucumber`) get `a*=Logout`, `nightwatch*`/`selenium-webdriver` and an **unidentified recorder** get `//a[contains(., "Logout")]`. The id reaches the page script as `CaptureActionSnapshotInput.runner` and the zip as an extension field on `context-options`, read back through `isTestRunnerId` onto `Metadata.runner`; absent in older and foreign zips, where the player shows no hint. Under WDIO a text carrying a `"` still emits XPath — WDIO compiles `tag*=` to XPath with `"` quoting and would build a broken expression — and WDIO resolves `//` itself. `locatorsMatch` decomposes **both** sides from either dialect, so `after.point` survives whichever way round the two grammars fall; the `concat()`-stitched literal is still left to exact comparison. `@wdio/elements`' standalone `getSnapshot` deliberately keeps the portable XPath default (its output is pasted into arbitrary tools), so a WDIO run's `browser.getSnapshot()` and its trace A11y tab disagree on that one branch.
- **A captured text locator is generated in the recording runner's dialect; every other branch is portable CSS.** `shared/locator-dialect.ts` `locatorDialect(runner)` is the one fact table — WDIO runners (`mocha`/`jasmine`/`cucumber`) get `a*=Logout`, `nightwatch*`/`selenium-webdriver` and an **unidentified recorder** get `//a[contains(., "Logout")]`. The id reaches the page script as `CaptureActionSnapshotInput.runner` and the zip as an extension field on `context-options`, read back through `isTestRunnerId` onto `Metadata.runner`; absent in older and foreign zips, where the player shows no hint. Under WDIO a text carrying a `"` still emits XPath — WDIO compiles `tag*=` to XPath with `"` quoting and would build a broken expression — and WDIO resolves `//` itself. `locatorsMatch` decomposes **both** sides from either dialect, so `after.point` survives whichever way round the two grammars fall; the `concat()`-stitched literal is still left to exact comparison. `@wdio/elements` 2.x `getSnapshot` is not on this table at all: it renders `@wdio/snapshot`'s `attachSelectors` choice so its text matches `wdio session snapshot --selectors`, which can be a WebdriverIO-v10-only strategy (`role/…`, `aria/…`), so a WDIO run's `browser.getSnapshot()` and its trace A11y tab can differ on that branch.
- `Metadata.runner` (typed `TestRunnerId`) and `metadata.options.framework` (untyped `string`, read by the sidebar's `getFramework`) are two carriers of the same fact. All three adapters now set both; the next change to either should collapse `getFramework` onto `Metadata.runner`.
- Selenium's `ctx.runner` (the *detected JS runner* — mocha/jest/cucumber, from `detectRunner()`) shadows the new `Metadata.runner` concept. Renamed to `detectedRunner` at the `driverMetadata` boundary only, where the two met inside one function and a swap would have been a silent dialect bug; the repo-wide rename in `plugin-internals.ts`/`session-lifecycle.ts` is still open.
- **A node:assert's target comes from VALUE PROVENANCE, not from the assert call.** node:assert takes values, so `assert.equal(await el.getText(), …)` names no element. `core/read-value-locators.ts` owns the value→locator registry (`rememberReadValue`/`selectorForReadValue`/`resolveAssertTargetFromArgs`; most-recent-producer-wins, 200 keys, ≤256 chars, primitives only), reached through `patchNodeAssert`'s optional `resolveAssertTarget` hook, which walks the **raw** args (sanitizing destroys handle identity) onto the shared `CommandLog.selector`. It lives in core because two adapters feed it. **Selenium** feeds it from `driverPatcher`'s settle path and composes it with its handle WeakMap (`helpers/element-locators.ts`, adapter-local — it keys on `WebElement` identity, which no other framework has). **Nightwatch** feeds it from `browserProxy`'s capture callback via `assertTarget.commandTargetSelector`, an allowlist of element-read commands; every other command — every driver-level read (`title`, `getCurrentUrl`, `source`) included — records the **null sentinel**, without which a page-title assertion inherits the box of an element that reads the same text (measured on selenium's mocha example, `h1` vs `document.title` both "Example Domain"; reproduced and fixed for Nightwatch on a local `#hero`/`<title>` collision page). Measured: Nightwatch node:assert rows with a locator **0/7 → 4/7** (the 3 without are a title read, the title/`#hero` collision, and a literal); Selenium unchanged at 2/4 mocha and 2/3 cucumber. Residuals: boolean reads are weakly discriminating, so a value collision with no intervening producer can mis-attribute; Nightwatch's `element()/.find()` API and its ignored commands (`execute`, `executeAsync`, `perform`) are never wrapped, so their values neither claim nor clear. **`after.point` is deliberately not extended to assert rows** — `resolveActionPoint` gates on `class:'Element'` + `POINTABLE_METHODS`, so WDIO's folded assertion rows get none either; a point draws a click marker an assertion never earned.
Expand Down
14 changes: 7 additions & 7 deletions examples/wdio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
"private": true,
"type": "module",
"devDependencies": {
"@wdio/cli": "10.0.2",
"@wdio/cucumber-framework": "10.0.1",
"@wdio/cli": "10.1.0",
"@wdio/cucumber-framework": "10.1.0",
"@wdio/devtools-service": "workspace:*",
"@wdio/globals": "10.0.2",
"@wdio/local-runner": "10.0.2",
"@wdio/mocha-framework": "10.0.1",
"@wdio/spec-reporter": "10.0.1",
"@wdio/types": "10.0.1",
"@wdio/globals": "10.1.0",
"@wdio/local-runner": "10.1.0",
"@wdio/mocha-framework": "10.1.0",
"@wdio/spec-reporter": "10.1.0",
"@wdio/types": "10.1.0",
"expect-webdriverio": "^8.0.0",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"@typescript-eslint/parser": "^8.71.0",
"@typescript-eslint/utils": "^8.71.0",
"@vitest/coverage-v8": "^5.0.2",
"@wdio/allure-reporter": "^10.0.1",
"@wdio/allure-reporter": "^10.1.0",
"autoprefixer": "^10.6.1",
"eslint": "^10.11.0",
"eslint-config-prettier": "^10.1.8",
Expand All @@ -63,10 +63,10 @@
"unplugin-icons": "^24.0.0",
"vite": "^8.3.2",
"vitest": "^5.0.2",
"webdriverio": "^10.0.2",
"webdriverio": "^10.1.0",
"ws": "^8.22.0"
},
"dependencies": {
"@wdio/cli": "10.0.2"
"@wdio/cli": "10.1.0"
}
}
8 changes: 4 additions & 4 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@
"@iconify-json/mdi": "^1.2.3",
"@lit/context": "^1.1.6",
"@tailwindcss/postcss": "^4.3.3",
"@wdio/browser-runner": "^10.0.2",
"@wdio/browser-runner": "^10.1.0",
"@wdio/devtools-shared": "workspace:^",
"@wdio/globals": "^10.0.2",
"@wdio/mocha-framework": "^10.0.1",
"@wdio/globals": "^10.1.0",
"@wdio/mocha-framework": "^10.1.0",
"@wdio/protocols": "10.0.2",
"@wdio/reporter": "10.0.1",
"@wdio/reporter": "10.1.0",
"autoprefixer": "^10.6.1",
"codemirror": "^6.0.2",
"expect-webdriverio": "^8.0.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@fastify/websocket": "^11.3.1",
"@wdio/devtools-app": "workspace:^",
"@wdio/devtools-script": "workspace:^",
"@wdio/logger": "10.0.0",
"@wdio/logger": "10.1.0",
"fastify": "^5.12.5",
"fflate": "^0.8.2",
"get-port": "^7.2.0",
Expand All @@ -57,7 +57,7 @@
"devDependencies": {
"@types/shell-quote": "^1.7.5",
"@types/ws": "^8.18.2",
"@wdio/cli": "^10.0.2",
"@wdio/cli": "^10.1.0",
"@wdio/devtools-shared": "workspace:^",
"@wdio/devtools-trace": "workspace:^",
"nodemon": "^3.1.14",
Expand Down
12 changes: 6 additions & 6 deletions packages/conformance/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@
"@types/node": "26.6.4",
"@types/selenium-webdriver": "^4.35.6",
"@types/ws": "^8.18.2",
"@wdio/cli": "10.0.2",
"@wdio/cli": "10.1.0",
"@wdio/devtools-service": "workspace:^",
"@wdio/devtools-shared": "workspace:^",
"@wdio/globals": "10.0.2",
"@wdio/local-runner": "10.0.2",
"@wdio/mocha-framework": "10.0.1",
"@wdio/globals": "10.1.0",
"@wdio/local-runner": "10.1.0",
"@wdio/mocha-framework": "10.1.0",
"@wdio/nightwatch-devtools": "workspace:^",
"@wdio/selenium-devtools": "workspace:^",
"@wdio/spec-reporter": "10.0.1",
"@wdio/spec-reporter": "10.1.0",
"expect-webdriverio": "^8.0.0",
"nightwatch": "^3.16.0",
"selenium-webdriver": "^4.50.0",
"webdriverio": "^10.0.2",
"webdriverio": "^10.1.0",
"ws": "^8.22.0"
}
}
7 changes: 7 additions & 0 deletions packages/conformance/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ export const PAGES: Record<string, Record<string, string | null>> = {

export const BROKEN_PATH = '/broken'

/** W3C's session defaults: a real browser always answers `GET /timeouts` with all three, and WebdriverIO 10.1 reads `script` from it before every BiDi `execute`. */
export const DEFAULT_TIMEOUTS = {
script: 30_000,
pageLoad: 300_000,
implicit: 0
}

export const BIDI_PATH = '/session/conformance/bidi'

/** BiDi acknowledgements whose callers read a field back; commands the fake browser models are answered from its state. */
Expand Down
25 changes: 12 additions & 13 deletions packages/conformance/src/fake-webdriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
BLANK_PNG,
BROKEN_PATH,
COLLECTOR_SIGNATURE,
DEFAULT_TIMEOUTS,
ELEMENT_KEY,
NAVIGATION_HALF_MS,
PAGES,
Expand Down Expand Up @@ -52,6 +53,13 @@ type Reply = {

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))

const FIXED_REPLIES: Record<string, unknown> = {
'/screenshot': BLANK_PNG,
'/window/rect': { x: 0, y: 0, width: 1280, height: 720 },
'/window': WINDOW_HANDLE,
'/window/handles': [WINDOW_HANDLE]
}

const anchorOf = (url: string, bornAt: number): TraceMutation => ({
type: 'childList',
url,
Expand Down Expand Up @@ -206,6 +214,9 @@ export class FakeWebDriver {
if (method === 'POST' && (path === '/element' || path === '/elements')) {
return this.#find(String(params.value), path === '/elements')
}
if (method === 'GET' && path === '/timeouts') {
return { value: DEFAULT_TIMEOUTS }
}
const element = path.match(/^\/element\/([^/]+)\/(value|click|clear|text)$/)
if (element) {
return this.#onElement(element[1], element[2], params)
Expand All @@ -218,19 +229,7 @@ export class FakeWebDriver {
)
}
}
if (path === '/screenshot') {
return { value: BLANK_PNG }
}
if (path === '/window/rect') {
return { value: { x: 0, y: 0, width: 1280, height: 720 } }
}
if (path === '/window') {
return { value: WINDOW_HANDLE }
}
if (path === '/window/handles') {
return { value: [WINDOW_HANDLE] }
}
return { value: null }
return { value: FIXED_REPLIES[path] ?? null }
}

async #navigate(path: string): Promise<Reply> {
Expand Down
Loading
Loading