Skip to content

Name a node:assert target from value provenance in the WDIO service - #484

Merged
vishnuv688 merged 5 commits into
mainfrom
fix/481-service-assert-target
Oct 28, 2026
Merged

vishnuv688 merged 5 commits into
mainfrom
fix/481-service-assert-target

Conversation

@vishnuv688

Copy link
Copy Markdown
Member

What & why

Closes #481.

In the WebdriverIO service, a node:assert row took the last element any locator had resolved. So assert.notEqual(await browser.getTitle(), 'Secure Area') after $('h2').getText() boxed h2. The user's own browser.getTitle() also had no row, because getTitle was in INTERNAL_COMMANDS.

  • The service now feeds core's value registry (rememberReadValue) from every row SessionCapturer.afterCommand logs. A command that ACTION_MAP classes Element records the selector the row resolved. Every other read, such as getTitle, records the "no element" sentinel. wireAssertCapture passes resolveAssertTargetFromArgs to patchNodeAssert, as Selenium and Nightwatch already do. A node:assert row with no known producer now names nothing instead of the last resolved element.
  • The last-resolved fallback is kept for synthesized expect-webdriverio rows only (captureMatcherCommand). Those rows resolve their own element inside the matcher. Folding is unchanged.
  • getTitle was internal because the snapshot probe calls it from inside a hook, with the spec frame on the stack. That made the probe look like a user call, and listing it as internal hid the user's call too. action-snapshot.ts now counts its own title reads while they are in flight, and both hooks skip a getTitle that arrives during one (isServiceProbe). Without that guard, the probe's afterCommand takes the user's stack frame and logs the probe's result as the user's row; a unit test covers this.
  • Removes the two devtools-service known gaps for WebdriverIO service: an assertion on the page title boxes the last element touched #481 from packages/conformance/src/allowlist.ts, along with their KNOWN_DEBT entry and the CLAUDE.md bullet. The value-provenance, handle-selector and matcher-folding Known debt entries now describe the service's current behaviour.

Type of change

  • Bugfix (non-breaking change which fixes an issue)
  • 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)

Packages touched

  • shared (types and contracts)
  • core (framework-agnostic capture/reporting)
  • elements (published element/snapshot API — @wdio/elements)
  • service (WebdriverIO adapter)
  • nightwatch-devtools (Nightwatch adapter)
  • selenium-devtools (Selenium adapter)
  • selenium-devtools-py (Selenium Python adapter)
  • backend (server)
  • app (UI)
  • script (page-injected runtime)
  • trace (Trace mode)

Capability matrix

  • packages/conformance/src/features.ts updated, and tables regenerated with pnpm docs:capabilities, if this adds or changes user-facing behaviour

No feature cell changes. pnpm docs:capabilities --check passes.

Notes for reviewers

Stacked on #483. The base is feat/479-conformance-timings-targets. Retarget this PR to main after #483 merges.

Proof:

  • Conformance passed 3 runs in a row with the gaps removed (15/15 each: 4 adapters x 3 BiDi modes, plus agreement).
  • I then reverted only the packages/service/src change and rebuilt, keeping the gaps removed. Conformance failed 6 tests: page-level-assertion-names-no-element ("assert.notEqual on the page title names h2") in all three modes, and the agreement check ("devtools-service shows [Assert.notEqual h2], selenium-devtools shows [Page.getTitle, Assert.notEqual]").
  • Two new regression tests in packages/service/tests/assert-target.test.ts fail without the fix and pass with it: the title-vs-heading collision, and an assert on an h2 read that still names h2 after a later #logout click. A third test in trace-action-capture.test.ts covers the snapshot probe re-entering the hooks during the user's getTitle.

Behaviour changes worth knowing:

  • expect(browser).toHaveTitle() now folds into the visible getTitle read, like toHaveText does with getText. Before, it produced a fresh row. getUrl is still internal, so toHaveUrl still produces a fresh row.
  • A value read through interleaved element handles is claimed for whatever selector the row resolved. This is the same handle-selector limitation already listed in Known debt.

Checks: pnpm build, full pnpm test (3049 passed), pnpm lint (0 errors), pnpm typecheck, pnpm docs:capabilities --check, and the dist leak grep for packages/service (no matches).

Not verified in a real browser. All verification was unit tests plus the conformance suite against its fake WebDriver server. To check by hand, add a title assertion to the WDIO example spec, for example:

await $('h2').getText()
assert.notEqual(await browser.getTitle(), 'Secure Area')

Then run pnpm demo:wdio. The Actions list should show a Page.getTitle row, and the assert.notEqual row should box no element.

Screenshots / recordings

N/A (no UI change).

🤖 Generated with Claude Code

The service stamped every node:assert row with the last element any locator
resolved, so an assertion on the page title boxed the heading read before
it. node:assert takes values, so only provenance can say which element (if
any) a value came from: the service now feeds core's value registry from
each row it logs (element commands with their selector, driver-level reads
with none) and resolves assert targets from the raw args, as Selenium and
Nightwatch already do. The last-resolved fallback stays for expect matcher
rows, which resolve their own element.

getTitle was internal only because the snapshot probe calls it from inside
a hook with the spec on the stack, which also hid the user's own call. The
probe now marks its title reads in flight and the hooks skip those, so the
user's getTitle is a Page.getTitle row like in the other adapters.

Removes the conformance gaps for #481 now that the service passes
page-level-assertion-names-no-element.
@vishnuv688 vishnuv688 self-assigned this Oct 28, 2026
@greptile-apps

greptile-apps Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issue remains.

Summary

The service now uses remembered command results to choose targets for node:assert rows instead of borrowing the last resolved element.

  • WebdriverIO assertion rows follow the value being checked.
  • Each test's assertions use only values read during that test.

All three previous threads were rechecked and are addressed. None supplied a comment number.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Read[Captured command result] --> Registry[Remember value and target]
  Registry --> Assert[node:assert arguments]
  Assert --> Row[Assertion row target]
  Boundary[Test or scenario starts] --> Clear[Clear remembered values]
  Clear --> Registry
  Matcher[WebdriverIO matcher] --> Separate[Separate matcher capture path]
Loading

Reviews (4) · Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." · Reviewed by Greptile

Comment thread packages/service/src/session.ts Resolved
Comment thread packages/service/src/action-snapshot.ts Resolved
…t-target

The base merged main, which fixed #459 and dropped the nightwatchSkipped
gap and its Known debt bullet. This branch fixes #481 and drops the
serviceAssertTarget gap and its bullet. Both removals are kept, so
KNOWN_DEBT and the conformance debt list carry neither entry; the base's
other changes come through unchanged. With both gaps gone nothing in the
allowlist names the page-title test any more, so its index is dropped.
A node:assert row looked its operands up in a value registry that lived
for the whole run, so a later test's assert.ok(true) boxed an element an
earlier test had read true from. The registry is now cleared whenever a
test or scenario starts: core exports clearReadValues, which the service
calls from resetStack, Selenium from startTest and startScenario, and
Nightwatch from resetTestTracking. Nightwatch's plugin hook fires once per
module on the BDD interface, so its proxy also clears when the running
`it` changes, before each record and each lookup. Python clears its own
registry at pytest_runtest_logstart.

The service told its own snapshot title probe apart from the user's
getTitle by counting probes in flight. Core's probe timeout abandons a
getTitle that is still running, which left the count positive and hid a
user getTitle issued in that window. The probe now runs inside an
AsyncLocalStorage context, and WDIO awaits a command's hooks within that
command, so the hooks see the context for the probe's call and nothing
else.
The service took an element read's value as the element's only when
ACTION_MAP classed the command Element, and ACTION_MAP leaves out
getHTML, getComputedRole, getComputedLabel, isStable and isEqual, so an
assertion on their value lost its element. They are listed in the
service, every other WDIO element command is listed as returning nothing
an assertion compares, and a test fails when the installed WebdriverIO
ships an element command in neither list.
@vishnuv688
vishnuv688 changed the base branch from feat/479-conformance-timings-targets to main October 28, 2026 19:40
@vishnuv688
vishnuv688 merged commit 18052d5 into main Oct 28, 2026
14 checks passed
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.

WebdriverIO service: an assertion on the page title boxes the last element touched

1 participant