Skip to content

refactor(elements): produce the element snapshot with @wdio/snapshot from v10 - #429

Open
Winify wants to merge 23 commits into
webdriverio:mainfrom
Winify:feat/elements-snapshot-v10
Open

Winify wants to merge 23 commits into
webdriverio:mainfrom
Winify:feat/elements-snapshot-v10

Conversation

@Winify

@Winify Winify commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

What & why

@wdio/elements' getSnapshot and the devtools service's browser.getSnapshot() can now render with @wdio/snapshot, the renderer behind wdio session snapshot --selectors. Agents that read the CLI and agents that call this function then see the same format. The renderer is chosen at runtime, so no WebdriverIO version is dropped:

  • WebdriverIO 9, 10.0, 10.1: the current renderer, with output byte-identical to main. @wdio/snapshot is never loaded.
  • WebdriverIO 10.2 and later (prereleases included), with @wdio/snapshot 10.2 or later installed: @wdio/snapshot. It is an optional peer, so it is opt-in: npm i -D @wdio/snapshot.
  • WebdriverIO 10.2 or later without it: the current renderer, silently.

The result is additive: { format: 'legacy' | 'snapshot', text, elements, refs? }. elements is filled on both paths, refs only on the snapshot path. inViewportOnly keeps working, and viewport is accepted as an alias. Both renderers default to the viewport. Every export main has is still there, so existing code compiles unchanged. Both packages therefore ship as a minor. A major comes only when the legacy renderer is dropped.

On the snapshot path, a classic executeScript that fails once is not retried on that browser; later snapshots go straight to execute, as @wdio/session's collectTop does.

Type of change

  • Bugfix (non-breaking change which fixes an issue) (the show-trace bin each adapter declares is now shipped)
  • New feature (non-breaking change which adds functionality)
  • Polish (an improvement to an existing feature)
  • Breaking change (existing behavior changes for users)
  • Documentation
  • Internal (build, CI, dependencies, tooling) (the workspace builds and tests against WebdriverIO 10.1.0)

Packages touched

  • shared (types and contracts): deepCapString exported for native-session detection.
  • core (framework-agnostic capture/reporting): unchanged public API; the snapshot builder stays for the legacy renderer.
  • elements (published element/snapshot API — @wdio/elements): runtime renderer choice, snapshotRenderer(), additive result, optional @wdio/snapshot peer.
  • service (WebdriverIO adapter): browser.getSnapshot() returns the additive result; peers unchanged from main.
  • nightwatch-devtools (Nightwatch adapter): ships its show-trace bin; WebdriverIO 10.1.0 runtime ranges.
  • selenium-devtools (Selenium adapter): ships its show-trace bin; WebdriverIO 10.1.0 runtime ranges.
  • selenium-devtools-py (Selenium Python adapter)
  • backend (server): WebdriverIO 10.1.0 runtime range.
  • app (UI)
  • script (page-injected runtime)
  • trace (Trace mode)

conformance (not in the list): the fake browser answers GET /timeouts the way a real one does. .github/scripts/wdio-peer-smoke asserts the legacy renderer on both legs.

Notes for reviewers

Waiting on WebdriverIO 10.2.0. The fixes @wdio/snapshot needs are released only as 10.2.0-alpha.100.sha-b38abd894 (npm next): webdriverio/webdriverio#16131, #16150 and #16152. The top commit pins the workspace to that alpha for verification and is dropped before merge. webdriverio/webdriverio#16344, which makes webdriverio a peer of @wdio/snapshot, should land in the same release. Results on the alpha are in the comment below.

Behavior to know about:

  • The text differs from wdio session snapshot --selectors on pages where the CLI session holds state this function lacks. The CLI has a page recorder that gives refs to elements with only a click listener, appends a Requests failed: note, and numbers refs across its session.
  • On the snapshot path, a ref can carry no selector when none resolves to its element under BiDi.
  • With npm, a prerelease WebdriverIO install needs --legacy-peer-deps, because the peer ranges do not admit prereleases.
  • Calling @wdio/elements' getSnapshot(browser) directly inside a devtools-service run shows the collector's script call as an Actions row. browser.getSnapshot() stays hidden.

WebdriverIO 10.1.0 and conformance. In 10.1.0, BiDi browser.execute reads GET /timeouts first (webdriverio#16031). The conformance fake answered with null, so every execute failed. It now answers with the WebDriver defaults. Real Chrome traces are the same on 10.0.2 and 10.1.0.

Screenshots / recordings

n/a (no UI change)

🤖 Generated with Claude Code

@Winify
Winify force-pushed the feat/elements-snapshot-v10 branch from 207aa27 to 7cd00ab Compare October 20, 2026 05:51
@Winify Winify changed the title Feat/elements snapshot v10 refactor(elements): produce the element snapshot with @wdio/snapshot from v10 Oct 20, 2026
@Winify
Winify requested a review from vishnuv688 October 20, 2026 06:17
@Winify
Winify marked this pull request as ready for review October 20, 2026 06:18
@greptile-apps

greptile-apps Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The reviewed changes appear safe to merge, with no new blocking findings.

Summary

getSnapshot now uses @wdio/snapshot and returns { text, refs }. The service keeps its viewport default and failure placeholder.

  • Snapshot calls now return the CLI’s selector-ready text and refs.
  • Installed adapters can launch the show-trace command.
  • The conformance fake returns default timeouts to WebdriverIO 10.1.
  • The workspace and adapter builds now use WebdriverIO 10.1.0.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Service["browser.getSnapshot(): viewport by default"] --> API["getSnapshot"]
  Direct["Direct call: full page by default"] --> API
  API --> Native{"Native app capabilities?"}
  Native -->|Yes| XML["Read and parse page source"]
  Native -->|No| Web["Collect page tree"]
  XML --> Render["@wdio/snapshot: attach selectors and format"]
  Web --> Render
  Render --> Result["Return text and refs"]
Loading

Reviews (3) · Last reviewed commit: "chore(changeset): note the WebdriverIO 1..." · Reviewed by Greptile

Comment thread packages/elements/src/get-snapshot.ts Resolved

Winify commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Review

The refactor is the right direction: one renderer, with byte-identical output to wdio session snapshot --selectors as the contract. The routing tests (vendor-bag Android, nested browserName: 'Safari', Mac and Windows native) cover the cases that matter. My findings follow, with the PR now in draft and webdriverio/webdriverio#16131 acknowledged as still open.

Must fix before merge

1. component-tests is red because of the 10.1.0 bump. The fix is upstream, not in this repo.
mutation-item.test.ts and workbench.test.ts fail on 12dd905. Every test in them passes, then each worker dies with session ended: WebDriver Bidi command "script.callFunction" … the connection was closed. That error is thrown from @wdio/runner@10.1.0's #checkForTestError → browser.execute. Head 9fca30c, before the bump, was green, and main is still on @wdio/browser-runner 10.0.2. webdriverio/webdriverio#16152 fixes this and is merged after v10.1.0. It clears once the workspace is on the release that carries it, so a re-run or a workaround here won't help.

2. Commit 1e88a9b's message is wrong. It says 10.1.0 is "the release that carries webdriverio#16131". #16131 is still open, and neither #16150 nor #16152 is in 10.1.0. The PR body also says 10.1.0's @wdio/snapshot build is byte-identical to 10.0.2. So the bump buys nothing today. I suggest one bump, straight to the release that carries #16131, #16150 and #16152, with a commit message that names them.

3. @wdio/snapshot still nests a second webdriverio, and 09187ac doesn't fix that.
09187ac assumes a caret range "lets the consumer's install pick the @wdio/snapshot release that matches its webdriverio". npm and pnpm pick the highest version in ^10.0.2, whatever webdriverio version is installed next to it. A project on webdriverio@10.0.x gets @wdio/snapshot@10.1.0, and with it a nested webdriverio@10.1.0. In the webdriverio repo, packages/wdio-snapshot/package.json declares "webdriverio": "workspace:*" as a hard dependency, but src/ only imports its types. Making it a peerDependency upstream, ideally in the same release as #16131, removes the duplicate whatever range this PR uses.

Should fix

4. classicUnavailable is dropped (packages/elements/src/get-snapshot.ts, collectWebTree). @wdio/session's collectTop caches it so it stops trying the classic path after the first failure. Here every call on a BiDi driver without classic executeScript pays a failed round-trip first, and agent loops call this repeatedly. A WeakMap<WebdriverIO.Browser, boolean> would match the CLI.

5. Scope. The description says the branch "carries only the snapshot change", but it also carries the workspace move to 10.1.0, the show-trace bins in three adapters and the conformance /timeouts fake. The bin fix is real and stands on its own, so it could merge now rather than wait on upstream releases.

Nits

  • The transport comment reads backwards: "bidi is the path for a classic session". The logic matches collectTop, but the comment is easy to misread. Something like "a non-BiDi session runs the collector through execute" would be clearer.
  • Lost zero-size-viewport guard. The old mobile path treated a 0×0 window (minimized app, broken ADB connection) as unknown. Now inViewport(tree, 0, 0) ?? parsed.tree silently returns the full tree. That's probably acceptable, but it isn't tested.
  • Viewport defaults differ. getSnapshot() defaults to the full page, browser.getSnapshot() to the viewport. The PR documents it, but users will still trip over it. Matching the library default and letting agents opt in to viewport: true would avoid that.
  • The Greptile thread on vendor-bag capabilities is fixed in 9fca30c and tested, but still unresolved.

I didn't run the unit tests locally. Apart from component-tests, CI is green on 12dd905.


Generated by Claude Code

@Winify
Winify force-pushed the feat/elements-snapshot-v10 branch from 12dd905 to 2be4d5e Compare October 27, 2026 08:45
Winify and others added 2 commits November 2, 2026 14:26
The hand-rolled snapshot builder drifted from what the wdio CLI prints for the same page. Delegating to @wdio/snapshot makes getSnapshot() output match `wdio session snapshot`. @wdio/snapshot requires webdriverio 10, so the peer range narrows to ^10.0.0; the service's getSnapshot command follows the new options/result shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/packages/**/*.mjs is gitignored, so bin/show-trace.mjs was never
committed and every published adapter declared a bin it did not ship.
Also ignore the snapshot files the wdio example spec writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Winify
Winify force-pushed the feat/elements-snapshot-v10 branch from 2be4d5e to dd834a4 Compare November 2, 2026 13:39
Winify and others added 17 commits November 2, 2026 15:40
…driverIO 9 again")

@wdio/devtools-service consumes @wdio/elements 2, which renders through
@wdio/snapshot, and that package pins webdriverio exactly to a v10 release.
A 9.x peer range could therefore never resolve, so the service's peers
(webdriverio, @wdio/types, @wdio/reporter, @wdio/logger, @wdio/protocols,
@wdio/allure-reporter) narrow to ^10.0.0.

- changeset: mark the service major and spell out the break
  (getSnapshot returns refs, viewport replaces inViewportOnly, Node >=22.19.0)
- CI: the wdio-peer smoke job drops the 9 leg; it only exercises the service
  and @wdio/elements
- smoke.ts: GetSnapshotResult and the viewport option
- CLAUDE.md, elements README: stop claiming getSnapshot shares the trace
  capture's scripts or keeps the portable XPath default

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page source is stamped on the root by action-snapshot.ts; the helper the
comment named no longer exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wdio/snapshot pins webdriverio exactly, so an exact @wdio/snapshot pin
nested a second webdriverio under every consumer whose ^10 resolved a
newer minor. A caret range lets the consumer's install pick the
@wdio/snapshot release that matches its webdriverio, leaving one copy;
the peer smoke asserts exactly that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The elements snapshot shim and its serializer surface were removed, so the docs no longer describe them as live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nges

The audit found two consumer-visible changes the changeset did not
mention: mobile browser sessions move from Appium's page source to the
page collector, and native getElements stops attaching _sourceXML.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Device clouds state platformName (and browserName) only inside a vendor
bag such as bstack:options. nativePlatformName read top-level keys only,
so those native sessions fell through to the web collector and
getSnapshot returned "[Snapshot unavailable]". The base got this from
WDIO's isAndroid/isIOS, which read vendor options too.

Reuse shared's one-level deepCapString (now exported) rather than a
second reader, and cover it with unit tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WebdriverIO 10.1.0's BiDi execute reads the session's script timeout
before every call (scriptTimeoutOf, `getTimeouts().then(({ script }) =>
script, ...)`). The fake answered every unlisted endpoint with null, so
that destructuring threw inside the fulfilment handler, which its own
rejection handler does not catch: every browser.execute rejected before
script.callFunction was sent. The service never drained the collector
with its forced anchor, and the bidi:true scenario reported every
document unanchored and every row replaying the wrong page.

W3C requires GET /timeouts to return the timeouts object, and real
Chrome does: the mocha login example traces identically on 10.0.2 and
10.1.0 (4 of 4 documents anchored, 13 actions). The gap was the fake's.
The fixed-reply endpoints move into one table so the route stays under
the function-size cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The service now requires WebdriverIO 10, so the workspace builds and
tests against the newest 10.x release. 10.1.0 does not carry
webdriverio#16131, #16150 or #16152 yet; the move to the release that
does follows once it ships. @wdio/protocols stays at 10.0.2; no 10.1.0
was published.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wdio/session's collectTop remembers that a BiDi session has no classic
executeScript and goes straight to execute afterwards; getSnapshot tried the
classic path first on every call and paid a failed round trip each time an
agent loop asked for a snapshot. The docstring also no longer promises a
selector on every ref: @wdio/snapshot attaches only selectors that resolve to
their element under BiDi.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…10.2

getSnapshot picks its renderer from the installed WebdriverIO. From 10.2
(prereleases included) it renders with @wdio/snapshot; below that it keeps
the renderer from upstream/main, so WebdriverIO 9 and 10.0-10.1 output does
not change.

The result is one additive type: format 'legacy' | 'snapshot', text, elements
on both paths, and refs on the snapshot path. viewport is accepted next to
inViewportOnly. The serializer and SnapshotResult exports return. SnapshotError
is local, and @wdio/snapshot is loaded only through a dynamic import, so
importing @wdio/elements on WebdriverIO 9 never loads it.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
… version

browser.getSnapshot() defaults to the viewport on both renderers and reports
the renderer's format in its failure value.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@wdio/elements takes the webdriverio peer back to ^9.0.0 || ^10.0.0, and the
service peers return to upstream/main. @wdio/snapshot is ^10.2.0-0, so
prereleases satisfy it. The wdio-peer matrix is [9, 10] again. Both packages
get a minor changeset, because the result only gains fields.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
The renderer is chosen at runtime from the installed webdriverio, so the
README and the getSnapshot JSDoc now state the 10.2 gate, the result fields
per renderer, the per-renderer viewport default, and the ref without a
selector case, instead of claiming one renderer's output for every install.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Winify and others added 3 commits November 2, 2026 16:03
As a regular dependency, @wdio/snapshot nested a second, v10 webdriverio
under itself in every npm project on WebdriverIO 9 or 10.1, which the
wdio-peer smoke rejects. The new renderer now runs only when WebdriverIO
is 10.2 or later and the project installed @wdio/snapshot itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…keep 1.x types

@wdio/cli pulls @wdio/snapshot transitively through @wdio/session at its
own exact version, so a ^10.2.0-0 optional peer made npm install a second
@wdio/snapshot and webdriverio on 10.1. The peer now accepts any 10.x and
the renderer gate checks the resolved @wdio/snapshot version instead.
Both renderers default to the viewport, a failed load reports legacy, the
native window size is read safely, and the published declarations no
longer reference @wdio/snapshot, an optional peer most installs lack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The alpha carries webdriverio#16131, #16150 and #16152; this commit is for verification only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Winify
Winify marked this pull request as ready for review November 2, 2026 16:38
@Winify
Winify force-pushed the feat/elements-snapshot-v10 branch from dd834a4 to 3d4c594 Compare November 2, 2026 16:39
@Winify

Winify commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Snapshot renderer picked at runtime, no WebdriverIO 10 requirement

The renderer is opt-in:

  • below 10.2 (9.x, 10.0, 10.1): the current renderer, same output as before. Do not add @wdio/snapshot on these versions: npm refuses it or nests a WebdriverIO 10 under WebdriverIO 9.
  • 10.2 and later, prereleases included, with @wdio/snapshot 10.2 or later resolvable (npm i -D @wdio/snapshot, a 10.2 or later release): @wdio/snapshot
  • 10.2 and later without a resolvable @wdio/snapshot 10.2 or later: the current renderer, silently. On both renderers the default is viewport-only; { viewport: false } gives the full page.

The peer range is webdriverio ^9.0.0 || ^10.0.0, so nothing here narrows either package to WebdriverIO 10. Both packages ship as a minor. Every type change is additive: getSnapshot returns { format: 'legacy' | 'snapshot', text, elements, refs? }, and SnapshotResult plus every export this PR had removed are back. A major comes only when the legacy renderer is dropped.

Legacy path unchanged

On WebdriverIO 9.32.0 and 10.1.0, getSnapshot returns format: 'legacy', with text and elements byte-identical to main on the fixture site, the-internet /tables and webdriver.io, both with the default options and with inViewportOnly: false. A resolve hook logged no attempt to load @wdio/snapshot. The wdio-peer smoke passes on both legs (9 and 10) and asserts that snapshotRenderer() returns 'legacy'. An npm project on @wdio/cli 10.1 installs one webdriverio and one @wdio/snapshot.

Verified against WebdriverIO 10.2.0-alpha

WebdriverIO 10.2.0-alpha.100.sha-b38abd894 (npm next) ships the three upstream fixes this PR was waiting on: webdriverio/webdriverio#16131, #16150 and #16152. The branch is pinned to that alpha through pnpm-workspace.yaml overrides in a separate commit (3d4c5941), which is dropped before merge. The peer ranges (webdriverio and @wdio/snapshot ^10.0.0) do not admit prereleases, so with npm a prerelease (next) WebdriverIO install needs --legacy-peer-deps. @wdio/snapshot is an optional peer of @wdio/elements, so it is not installed for WebdriverIO 9 or 10.1 projects. The renderer choice is a runtime check of the installed WebdriverIO version and of whether @wdio/snapshot resolves.

Gate on the alpha: pnpm typecheck clean, pnpm build passes, pnpm test 3590 passed / 4 skipped, pnpm lint 0 errors, no workspace-package leak in dist/. pnpm test:ui (component-tests) passes 31/31; it was red before #16152. Conformance passes, 303 passed / 3 skipped.

Selectors (#16131): I checked 15 pages twice each: the local fixture site, static probe pages, webdriver.io, the-internet and GitHub. Out of 1640 selectors, none matched more than one element. All 356 positional CSS paths resolved to the right element, and no role/ name had leading whitespace. One failure remains, and it is not one of the cases #16131 fixed. 8 docusaurus footer links fail per page, on webdriver.io and its API docs: <a>Discord<svg aria-label="(opens in new tab)">. The snapshot names that link Discord, and role/link[name="Discord"] resolves to nothing under BiDi. The same pages have an aria/WebdriverIO AI Copilot selector that resolves to a different element. Both come from @wdio/snapshot and will be reported upstream.

Android (#16150): UiAutomator2 on an emulator against native-demo-app, Home/Login/Forms/Swipe, 3 runs. Every one of the 39 selectors resolves to exactly one element. The resource id text_input, which has no package prefix, is now emitted as android=new UiSelector().resourceId("text_input") instead of id=text_input. After the fork, the snapshot path still gives identical text on 14 of the 15 fidelity pages (GitHub differs only in live content) and on all 4 Android screens. Trace mode on the native example has the same rows, screenshots and native snapshots as main (17/17/17).

Regression check against main (run before the renderer fork, which does not touch trace capture): 3 trace runs plus 1 live run per tree for WDIO mocha, WDIO cucumber, Nightwatch BDD and Selenium mocha. Exit codes, action rows, error rows, DOM anchors and cucumber step keywords are identical, and smoke:assert passes on every run. Wall time and zip sizes are within run-to-run noise.

Correction: the output is not byte-identical to the CLI

getSnapshot matches wdio session snapshot --selectors on 6 of the 15 pages, not on all of them. The rest differ in three ways:

  • The CLI daemon installs a page-side recorder (Symbol.for("wdio.page")) that marks elements with JS click listeners as clickable. A plain-session getSnapshot has no recorder, so it gives no ref to a control that has only a listener: the-internet's sortable table headers, and two React containers on GitHub.
  • The CLI appends a Requests failed: note.
  • The CLI's ref numbers continue across earlier actions in its session.

The renderer and selector logic are shared. What differs comes from what the CLI session recorded.

@Winify

Winify commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai review

- Omit refs with no resolved selector from `elements`; their candidates stay in `refs`.
- Require `@wdio/snapshot` ^10.2.0-0 as the optional peer, matching the runtime gate.
- Return `[No elements found]` with a warning when a native page-source read fails, as the legacy renderer does, instead of rejecting.
- Warn when the renderer checks fail or resolve an `@wdio/snapshot` below 10.2; stay silent only when it is not installed.
- Fall back to the BiDi transport after two consecutive classic failures rather than one, so a transient script error does not latch it for the browser's lifetime.
- Trim multi-line comments to one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant