Skip to content

Click-driven routines

A routine is a script: a list of steps that run top to bottom, loop after loop. Each step does exactly one thing — click, wait, move the cursor, or run your code. One clock. Anything that changes your mock uses { click } (your handlers) or { run } — not a second schedule on the routine.

typescript
// Your click handlers and resetApp() — busker does not wire these.
busk(root, {
start: [0.55, 0.25],
steps: [
{wait: 900},
{click: '[data-nav-item="alerts"]'},
{wait: 1500},
{click: '[data-row="p0"]'},
{wait: 2600},
{click: '[data-close]'},
{wait: 900},
{move: [0.55, 0.25]},
{run: () => resetApp(root)},
],
clickTargets: ['[data-nav-item="home"]', '[data-nav-item="alerts"]', '[data-row="p0"]'],
});
StepWhat it does
{ click: 'selector' }Glide to the element, dwell, press, and really .click() it.
{ wait: ms }Pause — reading time after whatever just opened or happened.
{ move: selector | [x, y] }Glide somewhere without pressing (park the cursor before the loop ends).
{ run: () => void }Your code once at this beat (toggle a class, append a row, update copy — whatever your mock needs).

That is the whole vocabulary. Pauses are never folded into click steps, so the script reads like a storyboard and nothing drifts when you edit a { wait }.

Glide speed is not on each step. Pass motion once on the routine (defaults are tuned for product mocks):

typescript
busk(root, {
motion: {
pxPerSecond: 580,
easing: [0.4, 0, 0.2, 1],
dwellMs: 250,
},
steps: [/* … */],
});

Busker measures distance in pixels when each glide starts. Duration = distance ÷ pxPerSecond. Progress along the line uses easing, a CSS-style cubic-bezier (default [0.4, 0, 0.2, 1]).

Each loop re-compiles the script from fresh layout. Hidden step targets are remeasured when the playhead reaches them so glides stay at the same speed.

clickTargets is the list of selectors that should look clickable and count as hits when a visitor clicks (dead clicks still get the miss hint):

typescript
clickTargets: ['[data-nav-item="alerts"]', '[data-filter]'],

Busker does not change your UI. Wire click handlers (or rely on scripted { click } steps) for navigation, filters, and modals. To reset the mock before the next lap, add a final { run: () => … } step (often after a { move } that parks the cursor).

start is where the cursor rests before the first step, as a fraction of the root: [0.55, 0.25] is a bit right of centre, a quarter of the way down. It defaults to the middle.

delay waits after the mock is eligible to play (visibility + canPlay) before the first beat. During that wait the cursor stays hidden and play() / pause() do nothing — unlike a leading { wait }, which keeps the cursor visible at start. If the mock scrolls off screen or canPlay() goes false mid-wait, the timer resets and starts again when eligibility returns.

typescript
busk(root, {
delay: 900,
steps: [
{click: '[data-nav-item="alerts"]'},
// …
],
});
v1v2
TimedRoutine (duration + moves[])Removed. Use steps only.
{ click: '…', wait: 900, moveFor: 550 }{ wait: 900 }, { click: '…' }
{ to: [0.5, 0.5], wait: 900 }{ wait: 900 }, { move: [0.5, 0.5] }
{ run: fn, wait: 400 }{ wait: 400 }, { run: fn }
tasks, toggles, typing, countdowns on the routine{ run } steps in order (or click handlers)
onLoopfinal { run: () => reset() } step in steps
ScriptRoutine / assertScriptRoutineRoutine / assertRoutine
compile() → { moves, tasks, … }{ glides, runs, duration } — see compile() helper
per-step moveFor / dwelloptional routine motion

Loop length for a test? Use compile().

← Markup · Next: compile() helper