You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-**Recording against a drifted app state is now refused, not silently mis-recorded.** Re-running a roster after the app moved on — a submitted form's button now `disabled`, a spent one-time action, a control now sitting under an overlay — used to slip the live safeguard: it drove the target with a DOM `el.click()`, which punches through a disabled/covered element without erroring, so the real coordinate-click then hit nothing or the overlay (reading as random clicks). The safeguard now asserts the target is actionable the way the real take clicks it and **refuses** (`[not-actionable]`, exit 2) on a `disabled` / `aria-disabled` / `pointer-events:none` / covered target, routing you to reset the app to a fresh state (occlusion is viewport-guarded, so a below-fold target is not false-flagged).
16
+
-**Audit driving now matches real typing.** The safeguard's `fill` sets the value through the native input setter and dispatches `input` + `change` + `blur`, so a submit gated on `change`/blur/controlled-state enables during the audit too — a valid fill→enable-submit flow (the `form-flow` preset) is no longer false-refused.
fill: (sel,t)=>page.evaluate(({ q, t })=>{constel=document.querySelector(q);if(el){el.value=t;el.dispatchEvent(newEvent('input',{bubbles: true}));}},{q: sel, t }),
763
-
select: (sel,o)=>page.evaluate(({ q, o })=>{constel=document.querySelector(q);if(el){constx=[...el.options].find((p)=>p.text.trim()===o||p.value===o);if(x){el.value=x.value;el.dispatchEvent(newEvent('change',{bubbles: true}));}}},{q: sel, o }),
762
+
// Type/select the way the REAL take does (keyboard + coordinate change), so
763
+
// the audit's post-action DOM — and the actionability verdict riding it —
764
+
// matches what the recording produces. A raw `el.value=` fires no `change`
765
+
// and is invisible to a controlled (React) input (its value setter is
766
+
// overridden on the instance): use the prototype's native setter and
767
+
// dispatch input + change (+ blur) so a submit gated on change/blur/state
768
+
// actually enables here too. Without this the gate FALSE-REFUSES a valid
769
+
// fill->enable-submit flow (the shipped form-flow preset).
770
+
fill: (sel,t)=>page.evaluate(({ q, t })=>{
771
+
constel=document.querySelector(q);if(!el)return;
772
+
// native value setter ONLY on a real input/textarea — calling the input
773
+
// prototype's setter on any other element throws "Illegal invocation".
select: (sel,o)=>page.evaluate(({ q, o })=>{constel=document.querySelector(q);if(el){constx=[...el.options].find((p)=>p.text.trim()===o||p.value===o);if(x){el.value=x.value;el.dispatchEvent(newEvent('input',{bubbles: true}));el.dispatchEvent(newEvent('change',{bubbles: true}));}}},{q: sel, o }),
// Human message for a not-actionable interaction target. Routes the operator to
829
+
// the REAL cause — the app drifted from the state the roster was authored for —
830
+
// because the recorder cannot reset server-side state (a fresh browser context
831
+
// won't un-submit a form). Pure; unit-tested through auditRosterLive.
832
+
exportfunctionactionabilityMessage(sel,act){
833
+
constreason=act&&act.reason;
834
+
if(reason==='covered')
835
+
return`"${sel}" is covered by ${(act&&act.by)||'another element'} at its click point — the real mouse click would hit that overlay, not the target (reads as a random click). Dismiss/hide the overlay first (an earlier click to close it, or {"hide":"<overlay>"}).`;
836
+
if(reason==='pointer-events')
837
+
return`"${sel}" has pointer-events:none — the real mouse click can't actuate it. Reveal/enable it first, or target the actionable element.`;
838
+
if(reason==='missing')
839
+
return`"${sel}" matches no element — the step fires on nothing.`;
840
+
return`"${sel}" is disabled — the app is in a state this roster wasn't authored for (a form already submitted, a one-time action already spent). The recorder can't reset server-side state; reset the demo/app to a fresh state before recording (or pass --no-safeguards to override).`;
841
+
}
842
+
779
843
// Live audit (needs a page): walks the roster against the REAL rendered DOM and
780
844
// returns hard ERRORS for the two failures static analysis can't see:
781
845
// - off-screen: an action/primitive anchored to an element whose center is
@@ -873,13 +937,35 @@ export async function auditRosterLive(steps, bridge) {
873
937
if(!(awaitmeasure(sel)))errors.push({step: i+1,kind: 'missing',message: `${what} "${sel}" matches no element — the scene never ${what==='zoom target' ? 'frames it (renders wide, reads as a random pan)' : 'scrolls (silently shows the wrong position)'}.`});
874
938
}
875
939
876
-
// drive real state so later anchors see the post-action DOM
940
+
// actionability gate: assert each interaction target is clickable the way the
941
+
// real take clicks it (by coordinate), not the way the audit drives it (DOM
942
+
// el.click(), which punches through disabled/covered). This is the app-state-
943
+
// drift failure the operator hits re-recording — a form already submitted, a
944
+
// spent one-time action, a CTA that changed and now covers/overlays.
Copy file name to clipboardExpand all lines: showreel/skills/showreel/SKILL.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -74,6 +74,8 @@ Whatever you pick: every label visibly connects to its target, and the final fra
74
74
75
75
**Discipline:**`--dry` is MANDATORY before any recording — resolves every selector ([ok]/[MISS]), no recording. A [MISS] on a glide/camera/click/fill/select target is a frozen dead beat — fix to zero MISS, then record ONCE.
76
76
77
+
**Fresh app state before every (re-)record.** A roster is authored for the app in state A; re-running it later hits state B, where the action already happened — a submitted form's button is now `disabled`, a one-time CTA is spent, a control changed and an overlay/next-page element sits where the target was. The recorder then clicks nothing (disabled) or the wrong thing (reads as random clicks). `--dry` will NOT catch this (a disabled button still resolves). The live safeguard now **refuses** (exit 2, `[not-actionable]`) when a click/fill/select target is `disabled` / `aria-disabled` / `pointer-events:none` / covered by another element. The recorder CANNOT reset server-side state (a fresh browser context won't un-submit a form) — so on a `not-actionable` refusal, **reset the demo/app to a fresh state and re-run**; never `--no-safeguards` past it (that records the broken flow).
78
+
77
79
**Render mode (decides at the command line, not in the frames):** a MOVING reel — any `glide`/`follow`/`camera`/`fill`/`select` — records realtime `--fps 30`, NO `--offline` (15fps stills → slideshow), NO `--pace fast` (collapses easing), NO `speed` (a no-op in realtime). `--pace fast` and `--offline` are ONLY for quick static/dwell-only proofs.
78
80
79
81
**Presets:** don't write step JSON from scratch. Copy a starting roster from `scripts/presets/` — `form-flow.json`, `nav-flow.json`, `dashboard.json` — and swap the selectors for the page under test.
0 commit comments