Repository navigation
fix(selenium-devtools): give a failed driver.wait() a row, and fix the call site and first-test rows - #453
Conversation
example.com dropped its <h1>, so the mocha example's
wait(until.elementLocated(By.css('h1'))) timed out and the getText()
and assert.equal after it never ran. Read the h2 of the-internet's
login page instead, which the other examples already drive, and update
the README snippet that carried the same selector.
…ne when absent With source maps on (tsx, vitest) the adapters' own frames are named by their source paths rather than dist/, so isUserCodeFrame accepted getCallSourceFromStack's own frame and every row pointed at core's stack.ts. Locate this monorepo's packages/ dir from the nearest pnpm-workspace.yaml and treat its src/ files as library code; an installed or user-bundled adapter finds no such workspace. Return no call source instead of the 'unknown:0' placeholder, which read as a call site, and keep a malformed frame from throwing.
…der --require Under --require the plugin loads before mocha publishes its globals, and the hooks were registered by a 100 ms poll, so a first test that started sooner got no startTest and every command it ran was dropped. Export mochaHooks, mocha's root hook plugin entry, which mocha reads before loading any spec, and stop the globals poll once it is claimed.
A wait is not a captured command, so a TimeoutError had no row to attach to: nothing red in the Actions list or the Errors tab. The patched wait() now returns a promise derived from selenium's that records the failure and rethrows it (re-wrapped in selenium's WebElementPromise for an element wait), so rejection and unhandledRejection behave exactly as before, and the failure turns the wait's own poll row into a wait row named after its condition. A wait's polls now fold only into rows of that same wait, never into an earlier row with the same command and arguments.
[Medium impact] The PR appears safe to merge; no new blocking issue was found. SummaryThe PR makes failed Selenium waits visible, keeps separate calls on separate rows, fixes source locations, and registers Mocha hooks before the first test.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[driver.wait] --> B[Choose wait scope]
B --> C[Run condition]
C --> D[Match each call to its row]
D --> E[Update only that wait's rows]
C --> F{Wait fails?}
F -->|Yes| G[Replace latest poll row with failed wait]
G --> H[Rethrow original error]
Reviews (4) · Last reviewed commit: "fix(selenium-devtools): tell nested wait..." · Reviewed by Greptile |
…ut of the retry tracker Polls were keyed on command and args alone, so a condition reading a.getText() and b.getText() folded the second read over the first. Name each call at invocation as the nth call with that command, args and target within one evaluation of the condition, which every poll repeats in order; a timer poll has no call site to tell them apart. Replacing an older poll row moved only the retry tracker's lastId, leaving lastSig naming another command, so a later retry of that command overwrote the wrong row. A wait's rows now never enter the retry tracker and reset it, so no retry lands on one or folds across a wait.
…er-wait # Conflicts: # CLAUDE.md
…er polls A wait issued inside another wait's condition got a fresh scope on every evaluation of the outer condition, so its polls never folded and each outer poll added a row (and a trace snapshot). The nested wait now keeps its scope on the outer wait, keyed by its order within one evaluation. Core's toError no longer throws on a null-prototype value, so the wait tracker uses it instead of a local copy; a condition description that cannot be stringified still leaves the failed wait its row. The trace's 'unknown:0' guard goes, since nothing produces that placeholder now.
|
The new conformance rules in #466 guard this fix. With every source file this PR changes reverted, Selenium fails in all three BiDi modes: |
A wait inside another wait's condition kept its scope by position alone, so two different waits issued from different branches of the outer condition on successive polls shared one row, and the second one's failure replaced the first one's row. A nested wait is now keyed by its call site, its condition's description and its condition's source, plus its order within the evaluation: the same wait still folds across outer polls, and two different waits keep their own rows.
What & why
A Selenium mocha trace showed tests with no rows after a
driver.wait(): nogetText, noassert.equal, not even for the failing one. The trace's own stdout events had the real story,Wait timed out after 10054ms: example.com no longer serves an<h1>, sodriver.wait(until.elementLocated(By.css('h1')))timed out and nothing after it ran. Capture had not regressed, but the failure was invisible, because a failedwait()produced no row at all. Looking into it turned up three adapter bugs, fixed here alongside the example.h2of the-internet's login page, which the other examples already drive.driver.wait()gets a red row. The patchedwait()returns a promise derived from selenium's that records the failure and rethrows it, re-wrapped in selenium's ownWebElementPromisefor an element wait, so an awaited failure rejects with the same error, an unawaited one still surfaces asunhandledRejection, anddriver.wait(until.elementLocated(...)).getText()still chains. The failure turns the wait's own poll row into awaitrow named after the condition (Waiting for element to be located By(css selector, h1)), mapped to the same trace method as WDIO'swaitUntil, so it shows in the Actions list, the Errors tab and the trace.wait()call carries its own scope, and a poll folds only into a row of the same scope. Before, the fold matched any earlier row by command and arguments, so a helper's wait could overwrite the user's ownfindElementsrow. Timer polls also never folded at all, because the patcher reported core's'unknown:0'placeholder instead of no call source.src/paths, so every row'scallSourcepointed atcore/src/stack.ts. Core now locates this monorepo'spackages/directory from the nearestpnpm-workspace.yamland treats itssrc/files as library code; an adapter installed from npm or bundled into a user's build finds no such workspace, so a user's ownsrc/stays user code. A malformed frame can no longer throw, and no user frame now means no call source rather than'unknown:0', which fixes the same fold for Nightwatch.--require. The plugin loads before mocha publishes its globals, and the hooks were registered by a 100 ms poll, so a first test that started sooner lost every command. The package now exportsmochaHooks, mocha's root hook plugin entry, which mocha reads before any spec; importing the plugin from a spec still registers through the globals.Type of change
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)Notes for reviewers
core:getCallSourceFromStackis core's, and the service, Selenium and Nightwatch all inline it, so all three get a patch changeset. Nightwatch's change is test fixtures only.CommandLog.callSourcewas already optional, so the wire contract and the app are unchanged. The'unknown:0'guards intrace-sources.tsand the backend's baseline attribution stay, because stored rows and zips from older adapters still carry the placeholder.RetryTracker: with folding scoped to the wait, its bare-signature fold (isRetryIgnoringSource) had no user left and is removed.unhandledRejectionwhen unawaited, and keeps element chaining; source-mapped stacks, a host-fulfile://frame, the monorepo, an npm install and a user bundle; the first test starting before the globals poll.pnpm test2840 passed,pnpm lint0 errors,pnpm typecheckclean, and the dist leak grep is empty for selenium-devtools, nightwatch-devtools and service.pnpm build && pnpm --filter @wdio/selenium-devtools example:mocha, then open the trace; the heading tests showElement.getText()and theassert.equalrows, and every row's call site points intoexample.js. Pointing a selector at something missing shows the redwaitrow.example.com/h1snippet; a webdriverio docs PR updates it.Screenshots / recordings
n/a (no UI change)
🤖 Generated with Claude Code