@reticlehq/test is an ordinary library: you write a script, it drives your running app, and it tells you whether what you expected actually happened.
That makes this page two things at once, and both are the same code:
- You decide what “verified” means for your app, in a file you own, in code you can read and keep in version control.
- Those same checks run in CI, so the thing you wrote by hand is the thing that guards the branch.
@reticlehq/test installed, and your app already running. Write checks with reticleTest(name, async (t) => …), open a headless session with bootSession({ driveUrl, headless: true }), and run them with runSpecs. Exit non-zero when summary.failed is not 0.
Checks bind to signals and testids, never DOM structure, so they survive the refactors that break selector-based suites.
Driving Reticle interactively is reconnaissance. This page is how you make it repeatable.
The test context t
A thin, typed façade over Reticle’s tools. It resolves testids → refs for you, so specs never touch refs or DOM:
Any failed matcher throws with the structured evidence (near-miss, failure reason) so the runner reports why.
Deterministic + honest
t.clockbakesreticle_clockinto the spec, so time-gated UI (a 5s auto-dismiss, a 500ms hover dwell) is tested deterministically instead of racing real timers.t.expectInputModeReal(): a hover/drag spec asserts native input is active; if it’s running synthetic (no CDP), the spec is skipped with a reason, never silently passing on a no-op. Enable real input headless withreticle drive(see usage §18).
Run a suite (headless, the same path CI uses)
bootSession launches a headless real-input browser at your app and gives the runner a programmatic tool invoker (no MCP/stdio):
pass | fail (with evidence) | skip (with reason). For CI, emit JUnit:
Flows become specs
.reticle/ flows (see Flows) can be executed directly as specs, replayed with their expect/success predicates and skipping dynamic (LLM-output) regions, so the recorded map and the suite can’t drift apart:
Authoring tip: record → prune → commit
You don’t have to hand-write steps. Drive the flow once (or record it via the panel), let Reticle emit the program, trim it, and commit it as a spec. The regression test is a byproduct of testing, not separate work.FAQ
Do I need vitest or jest to run these?
No.runSpecs is its own runner: it takes the specs registered by reticleTest, an invoke function, and a print callback, and returns { results, summary }. You can call it from a plain node script.mjs. If you already have vitest, nothing stops you calling runSpecs from inside a test, but the runner does not depend on one.
How do I fail the CI job?
runSpecs never throws on a failing spec, so you have to read the summary and exit yourself:
summary carries { total, passed, failed, skipped, ok }. Note that skipped is not failed: a spec that skipped with a reason (see t.expectInputModeReal()) leaves failed at 0.
Why did my hover or drag spec skip instead of running?
Because native input was not active.t.expectInputModeReal() deliberately skips with a reason rather than passing on a synthetic no-op, since a synthetic hover cannot trigger a CSS :hover or a pointer-library drag. If you want those specs to actually run, drive the app with real input: npx @reticlehq/server drive http://localhost:4310, or point the server at a CDP endpoint with RETICLE_CDP_URL.
Does the app need to be running already?
Yes. The spec runner is attach-only and never starts your dev server (that is the interactive agent’s job, not CI’s). Boot your app first, then pass its URL asdriveUrl. If nothing is listening there, bootSession opens a headless tab against a URL that serves nothing and no session ever connects.
Can I emit JUnit for my CI’s test reporter?
Yes,toJUnitXml and writeJUnit are both exported from @reticlehq/test. Pass them the results array from runSpecs.
Do I have to write specs by hand if I already have flows?
No.flowsAsSpecs registers one reticleTest per flow under .reticle/flows/, replayed with that flow’s own expect and success predicates and skipping its dynamic regions. That keeps the recorded map and the CI suite from drifting apart, because they are the same artifact.