feat(pace): command pacing for recorded runs - #316
Merged
Conversation
Splits the twd-js half out of the earlier combined recording spec, which is superseded by twd-cli's video capture design. Records the constraints found while checking feasibility so they do not have to be rediscovered: should() is a synchronous chainable and cannot await a pace delay without a breaking change, log() is synchronous, overlays cannot be plain <body> children because of the `body > div:not(#twd-sidebar-root)` scoping, runner events are not awaited, and onFail fires after afterEach. Direction only. Blocked on twd-cli video capture shipping first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pacing is what makes a recorded run watchable. twd-cli's record.speed is an ffmpeg setpts filter applied after the fact: measured, it stretches the timeline without adding frames, so 0.25 speed gives a 7.7fps video and slows the dead air as much as the interesting moments. TWD owns the in-page command loop, so it can space execution itself at full frame rate. Design: a src/pace.ts module holding one number, reachable only through a window.__twdSetPace tooling global rather than public API, so a stray call cannot slow CI. pace() is added only where the code is already async, so no public API becomes asynchronous and log() stays synchronous. Typing delay is derived from the same number and applied through a cached user-event setup instance, since user-event applies delay at config level after every API method. Overlays are dropped for v1 and the earlier draft is marked superseded, keeping its overlay research and correcting two things it got wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lets a recorded twd-cli run space out its own commands, so the video is watchable instead of a blur.
This is the twd-js half. It does nothing on its own until twd-cli drives it, which is a separate PR in that repo.
Why not just slow the video down
twd-cli already has
record.speed, an ffmpegsetptsfilter applied after recording. It stretches the same frames over a longer timeline, so the effective frame rate falls in proportion. Measured on identical page activity:10.50.25It also slows the dead air exactly as much as the interesting moments. TWD owns its own in-page command loop, so it can pace the execution instead and keep full frame rate.
How it is enabled
window.__twdSetPace(ms), registered alongside the existing__TWD_STATE__and__testRunnerglobals.Deliberately not a public
twd.setPace(). A documented API method can be left behind in a spec file, and every future CI run would then be slower with no obvious cause. There is a test asserting it never leaks out ofsrc/index.tsor onto thetwdobject.One number, two scales
The caller passes one number. The keystroke delay is derived from it and clamped at 60ms, because user-event applies
delayuniformly and reusing the pace itself would put half a second between every character.setPace(ms)2005002000What is paced, and what is not
Paced:
userEvent.*interactions andtwd.visit. Both are already async, so nothing became async that was not.Not paced, on purpose:
should()andtwd.setInputValue()are synchronous chainables and pacing them would be a breaking change; thescreenDomproxy is a pass-through with no uniform hook; queries do not change the page, so a pause after one is dead air with no highlight to show.Keystroke spacing
user-event applies
delayat config level, inwrapAndBindImplafter every API method as well as between keystrokes, pointer actions and option selections. So when paced the proxy routes through a cachedsetup({ delay })instance rather than merging options per call. That covers every method uniformly and sidestepsclear(element)being the one direct API method that takes no options.The cache is keyed on a pace generation counter, not on the delay, so setting the same value twice still invalidates.
Behavior note: the direct API builds a fresh
Systemper call while a setup instance shares pointer state across calls. That is the library's recommended usage and arguably more realistic, and it only applies when pacing is on, which only happens inside a recorded run.Normal runs are untouched
pace()returnsundefinedsynchronously when disabled: no promise allocated, no timer scheduled, no setup instance built. There is a test pinning that it returnsundefinedand not a Promise, which fails loudly if anyone later makes it anasyncfunction.Verification
439 tests passing, up from 406.
tscunchanged at its 4 pre-existing errors, none in the touched files.Driven end to end against
examples/twd-test-appwith Puppeteer:120 + 600 = 720 against a measured 718, so the hold lands exactly once per action and
setPace(0)restores full speed. Manually confirmed at 200 and 500, which both read well.Included docs
specs/2026-07-28-twd-js-command-pacing-design.mdis the design andspecs/2026-07-28-twd-js-command-pacing-plan.mdis the plan. The earlier overlay draft is kept but marked superseded: overlays are dropped for v1, since the only reason to pause after a query is to highlight what it found.No public documentation is added, because
__twdSetPaceis deliberately not public API.Next
twd-cli gains a
record.pacekey and a--record-pace <ms>flag that calls this. That needs a twd-js release first, since the global does not exist until then.