Repository navigation
Conversation
Trace mode took a per-action capture (page source + screenshot, plus DOM on web) for reads and waits as well as actions, because the capture gate was mapCommandToAction, the timeline-rendering table. 5c54ae4 added the reads to that table so they would render as rows, which also made every read, and every poll of a waiting matcher, a capture. On iOS a capture is expensive; a reported suite made 187 page-source calls for 12 interactions and went from 3:10 to 7:10. ACTION_MAP's methods are now classified in a typed METHOD_EFFECT table (an unclassified method fails to compile) and changesPage() gates the capture in all three JS adapters. Reads keep their rows and inherit the latest earlier capture through claimAfter's fallback. The first traced command of each test is still captured so a test opening with reads has a frame. The WDIO service stamps its end-of-test capture at the test's last page change, otherwise that action's row resolves to the capture from before it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The change affects trace captures across three adapter workflows. The PR appears safe to merge, with a non-blocking retry gap when the first screenshot fails. Findings
SummarySeparates timeline rows from commands that need a page capture.
Known limits acknowledged by Winify: Nightwatch BDD captures once per module; getter-only Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
Command[Traced command] --> Gate{Changes page or needs initial frame?}
Gate -->|Yes| Capture[Capture page state]
Gate -->|No| Reuse[Use latest earlier capture]
Capture --> Row[Render timeline row]
Reuse --> Row
Reviews (1) · Last reviewed commit: "perf(trace): capture page state only for..." · Reviewed by Greptile |
| if (snap) { | ||
| upsertRichestSnapshot(ctx.actionSnapshots, snap) | ||
| } else if (initialFrame && !changesPage(cmd.command)) { | ||
| capturer.awaitingInitialFrame = true |
There was a problem hiding this comment.
Failed screenshot stops later attempts
If the first screenshot fails, captureActionSnapshot can still return a truthy object with screenshot: undefined. This branch leaves awaitingInitialFrame false, so later reads never retry. FrameSnapshotIndex skips that capture, and Selenium has no final capture to recover it. A read-only test can therefore finish without frames for its rows.
Clear the initial-frame flag only after a usable screenshot arrives. Apply the same check in Nightwatch and the service.
ReviewThe direction is right and the diff is small. Splitting "does this get a timeline row" from "does this get a capture" via I'd ask for one fix before merging and have one design question. Must fix1. The initial-frame retry never fires. Greptile's finding is correct, and it's slightly worse than it describes.
Suggested fix: gate on Should address2. The service's action rows now show a later moment (not mentioned in the PR). The service captures before a command and stamps the capture with the timestamp of the last page change. Take
The capture count is the same (one per change); only when it's taken moves. Showing the settled state may well be what we want, but it is a user-visible change. The "0 rows differing" table compares page text on suites that probably have no async transitions, so it wouldn't catch this. Please either state it in the changeset or check it on a suite with a loading state. Selenium and Nightwatch capture right after the command, so the adapters now differ here as well. 3. A late result from one test can flip the next test's flag (Selenium/Nightwatch). The capture promise isn't awaited. If test N's first capture resolves after test N+1 has already taken its initial frame, the retry branch sets Nits / questions
Generated by Claude Code |
Why
In trace mode every adapter took a per-action capture (page source + screenshot, plus DOM/a11y on web) for read and wait commands as well as actions. On iOS a capture is expensive (WDA computes
visiblefor every node): a reported suite made 187getPageSourcecalls for 12 interactions and slowed from 3:10 to 7:10.The capture gate has always been
mapCommandToAction, the timeline-rendering table. 5c54ae4 (#247) added the reads (getText,is*,waitFor*,getTitle, …) toACTION_MAPso they would render as rows, and from then on each of them, including every poll of a waiting matcher, was also captured. One table was answering two questions.What
METHOD_EFFECTclassifies every trace method aschangesorreads.ACTION_MAPis typed against it, so an unclassified entry is a compile error.changesPage(command)is the capture gate.changesPage, plus the first traced command of each test, so a test that opens with reads still has a frame. Read and wait rows keep rendering and inherit the latest earlier capture throughclaimAfter's existinglatestAtOrBeforefallback.Why isn't this in
core? The gate is one call per adapter, but each adapter captures at a different point: the service before the command, Selenium and Nightwatch after it. The shared part is the classification, and that lives inshared.Verification
pnpm build,pnpm lint(0 errors),tscclean on the touched packages, conformance passes, and the dist leak grep is empty.pnpm test: 2755 passed, 1 failed. The failure isbackend/tests/run-state-continuity.test.tswithEADDRINUSE 127.0.0.1:3000, and it fails the same way onupstream/main.upstream/main) vs branch, 3 runs each,DEVTOOLS_MODE=trace. "Rows differing" compares each row's resolved snapshot page text against base.The Nightwatch wait after the submit click still resolves to
/secure: it inherits the click's own capture, which is taken after the click.iOS is not measured. A rerun of the reporting suite on this build would confirm the gain where it matters most.
Known limits
describe/itinterface fires the plugin's per-test hook once per module, so the first-command capture happens once per module there.browser.url()with no argument (a getter) still counts as a page change.🤖 Generated with Claude Code