Skip to content

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

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

Winify wants to merge 12 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 so the devtools service's browser.getSnapshot(), now render their output with @wdio/snapshot. Before, they kept a second renderer that had drifted from the WebdriverIO CLI's. The result is byte-identical to wdio session snapshot --selectors, so agents reading the CLI and agents calling this function see one format.

@wdio/snapshot pins webdriverio exactly to a v10 release. That makes @wdio/elements 2 and @wdio/devtools-service WebdriverIO-10-only, and both get a major changeset. The rest of the workspace's move to WDIO 10 already landed in #434. This PR is rebuilt on top of it and carries only the snapshot change.

Type of change

  • Breaking change (existing behavior changes for users)
  • Bugfix (the show-trace bin each adapter declares is now shipped)

Packages touched

  • core: the old web snapshot builder is removed, and serializeMobileSnapshot is kept for trace capture
  • elements: getSnapshot delegates to @wdio/snapshot
  • service: browser.getSnapshot() returns { text, refs }, and its peers are narrowed to ^10.0.0
  • shared: xpathLocatorTag removed (no consumer)
  • conformance: the fake browser answers GET /timeouts like a real one
  • nightwatch-devtools, selenium-devtools, backend: the show-trace bin file is added to the two adapters, and the WebdriverIO 10.1.0 runtime ranges are bumped (patch changeset). More than one adapter is touched because each package ships its own bin; there is no logic to share in core.

Notes for reviewers

The branch was checked for regressions against upstream/main in a real runtime: 3 runs per tree for e2e and Android, 2 for snapshot fidelity, and 4 for the bench. The runtime phases measured the head before the unrelated assertion-fold commit was split out into its own PR. The bench measured the final code.

  • Unit tests: base's test suite was run against head's source.
  • WDIO e2e: mocha and cucumber, live and trace.
  • Nightwatch and Selenium: e2e as a guard.
  • Android: UiAutomator2 on an emulator, against webdriverio/native-demo-app.
  • Snapshot fidelity: 15 pages.

Per-action trace capture and live streaming are unchanged in every adapter, apart from the direct-getSnapshot Actions row described below. Cucumber step keywords are intact. getSnapshot is 42–57% faster, with smaller output.

Known limitations, all in @wdio/snapshot and to be fixed there rather than worked around here, so the output stays identical to the CLI:

Behavior changes reviewers might trip over:

  • Default scope differs. getSnapshot from @wdio/elements defaults to the full page; browser.getSnapshot() defaults to the viewport.
  • Mobile browsers use the page collector. A mobile browser session (Chrome on Android, Safari on iOS) is snapshotted from the page instead of Appium's page source.
  • Native getElements lost _sourceXML. It no longer attaches tree.attributes._sourceXML.
  • A stray Actions row appears when getSnapshot is called directly. Calling @wdio/elements' getSnapshot(browser) directly inside a devtools-service run shows the collector's executeScript call as an Actions row. Use browser.getSnapshot() under the service, which stays hidden. Routing the collector through browser.execute instead would change the transport and break parity with the CLI.

The workspace also moves to WebdriverIO 10.1.0 here, with @wdio/snapshot 10.1.0, whose build is byte-identical to 10.0.2. The bump first broke packages/conformance ("follows every rule", "adapters agree" under BiDi). The cause was the suite's fake browser, not devtools. In 10.1.0, BiDi browser.execute reads GET /timeouts first (webdriverio#16031), and the fake answered that with null, so every execute failed and no document was anchored. The fake now answers a GET of /timeouts with the WebDriver defaults. Real Chrome traces are the same on 10.0.2 and 10.1.0.

Stays in draft until #16131 and the Android resource-id fix (webdriverio/webdriverio#16149) are released in @wdio/snapshot.

Screenshots / recordings

n/a (no UI change)

🤖 Generated with Claude Code

Winify and others added 7 commits October 19, 2026 18:24
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>
@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>
@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 Outdated
Comment on lines +29 to +32
const platformName = String(
caps.platformName ?? caps['appium:platformName'] ?? ''
).toLowerCase()
return NATIVE_PLATFORMS.has(platformName) && !caps.browserName

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Native cloud snapshots fail

nativePlatformName ignores capabilities inside vendor options. The repository already supports native cloud sessions whose platformName exists only inside bstack:options. Those sessions now fall through to collectWebTree, which tries to run page JavaScript against a native app. browser.getSnapshot() then returns [Snapshot unavailable] instead of the app tree.

Read nested capabilities before choosing the collector, and pass the resolved platform to the native parser.

Knowledge Base Used: Guard capture paths for native and mobile-browser sessions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and fixed in 9fca30c. Base routed on WDIO's isAndroid/isIOS, which read vendor options, so this was a regression from the move to @wdio/snapshot. nativePlatformName now reads platformName and browserName through shared's deepCapString, the same one-level vendor-bag reader the adapters already use for device detection. The resolved platform is passed on to the native parser. The tests cover a bstack:options-only Android session, which now takes the native path, and a nested browserName: 'Safari' session, which takes the web path.

Winify and others added 5 commits October 20, 2026 08:27
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>
@wdio/snapshot pins webdriverio exactly, so adopting the release that
carries webdriverio#16131 needs the workspace on 10.1.0. @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>

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

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