loupfeedDemos

Product walkthroughs that look human.

Screen recordings go stale the day after you make them, and automated ones look like a robot filling in a form. loupfeed plays a scenario file the way a person would use your app — the cursor travels, the page scrolls, the typing hesitates — and records the take. Re-run it every release instead of recording it again.

Playwright on the web, Maestro on mobile · npm i @loupfeed/demo-core

The invariant

The engine guarantees it looks real.

This is the part you cannot get from a normal test runner. Every interaction goes through a humanizer, and the rules are not optional — a scenario cannot opt out of looking human.

travel

The cursor visibly moves to its target on an eased, bowed, slightly jittery curve — with a dwell before the press, and overshoot-and-settle on long reaches.

visibility

Nothing is clicked unless it is genuinely in the viewport. There are no invisible, teleported clicks anywhere in a take.

scroll

Off-screen targets are brought into view with wheel-burst scrolling — flick, coast, pause — never a programmatic jump.

typing

Per-key cadence with hesitation after words and punctuation, instead of text appearing in a field all at once.

seed

All randomness draws from one seeded stream, so the same scenario and the same seed produce byte-for-byte the same take.

The scenario

A file you can read, review and diff.

A demo is JSON in your repo, not a binary you re-record. It goes through code review like anything else, and when the UI changes you edit a line instead of booking an afternoon.

  • Selectors starting with fb: target the same opaque element ids the build plugins stamp for feedback — they survive refactors
  • Anything else is passed to Playwright untouched, so the engine works on apps that never installed loupfeed
  • ${VAR} placeholders resolve from the environment at play time, so credentials never enter the file
  • read steps hold a beat on something worth seeing; expectVisible asserts the transition actually happened
{ "name": "login-walkthrough", "baseUrl": "https://app.example.com", "humanize": { "seed": 7, "speed": 0.9 }, "steps": [ { "kind": "goto", "path": "/sign-in" }, { "kind": "type", "selector": "input[name=email]", "text": "ada@example.com" }, { "kind": "click", "selector": "fb:a1b2c3d4e5f6", "note": "the Continue button" }, { "kind": "expectVisible", "selector": "text=Dashboard" } ] }
With your own AI

Your agent writes the scenario. The engine keeps it honest.

That division of labour is the whole design. An agent is good at deciding what a demo should show and terrible at making playback look human — so it never drives the browser directly. It discovers your app, writes a scenario grounded in what it found, and the engine plays it back under the invariant.

demo_discover crawls the navigable surface link by link, collapses id-bearing URLs onto route templates, and emits an app map: every route with its headings, its interactive elements — each carrying a scenario-ready selector — and its outgoing links.

Version one follows links and records buttons without pressing them, so screens reachable only through a button land in unvisited rather than being silently missed.

# claude, with @loupfeed/demo-mcp connected demo_discover → 14 routes mapped demo_get_map → ground the scenario in it demo_validate_scenario → 2 issues, fixed demo_save_scenario → scenarios/checkout.json demo_play → .loupfeed-demo/checkout.webm # a step failed? read the screenshot beside the take, # adjust, re-play — same seed, same conditions.
demo_discoverdemo_get_mapdemo_validate_scenariodemo_save_scenariodemo_playdemo_list_recordings
Mobile

The same scenario, compiled for a phone.

On a phone there is no visible cursor, so looking human reduces to pacing, flick-style scroll physics and settle beats. @loupfeed/demo-native compiles the same scenario schema into a Maestro flow and records on-device, rather than making you maintain a second script.

  • fb:<id> targets the element id the React Native SDK exposes as the view's testID
  • scroll becomes a burst of decaying percentage swipes — flick, then coast
  • Maestro has no sleep, so read and pause become an animation wait plus a held beat
  • Structural CSS selectors are refused with a clear error instead of failing strangely on a device
Scenario stepMaestro
gotoopenLink via deep link, or launchApp
clicktapOn by id or text
typetapOn + inputText
scrolldecaying swipe burst
scrollToscrollUntilVisible
expectVisibleextendedWaitUntil
recordingstartRecording / stopRecording

Status, plainly: the compiler and runner are unit-tested against Maestro's documented command set. The end-to-end proof on a real simulator is the next step, tracked in the open issue — so treat mobile playback as newer than the web player.

Why bother

A demo you can re-run is a different object.

Never re-record

The launch video, the release note clip and the onboarding walkthrough all regenerate from the same file after a UI change.

It notices breakage

Every expectVisible is an assertion. A demo that stops playing is telling you a real flow stopped working.

Same take, every time

Seeded playback means a re-run is comparable to the last one — no “it looked fine when I recorded it”.

The rest of the platform

Demos are one of four.

Record it once. Play it every release.

Free to self-host, forever. Playwright is a peer dependency — install it where the player runs and you are going.