Repository navigation
fix(studio): an offline preview says GSAP could not load instead of an unhandled Event - #5111
Conversation
…n unhandled Event
Edit accuracy: accurate 2055 (base branch 2055), smooth 1493 of thoseThe gate passes. Quarantined, measured but not gated (0) Unstable (1)
|
terencecho
left a comment
There was a problem hiding this comment.
Reviewed #5111 at bf50effd9c6ad5475a7800dc8c2d6e52768d9276 against base/merge-base 6308727b85726433e6a1e148b608d8cb11085114. Approved on the six-file effective diff; no blocking issue found.
The preview fallback excludes its own injected script from the capture listener, catches the failed retry, and reports a contextual Error instead of leaving a bare unhandled Event. Non-capture Studio previews buffer errors before authored scripts execute; the Studio hook resets on attach, starts the live listener, and replays early entries, including on load and repeated attachment. The existing modal supplies dialog labelling and focus behavior. The actual Before/After captures show the previously lone timeline TypeError and, after this patch, the explicit GSAP failure above that TypeError; close and Copy to Agent remain visible.
Focused isolated suites passed 113/113 with NODE_ENV=test after building workspace dependencies. All 11 required GitHub checks, including Windows render, passed at this head. I did not run a separate live offline Studio/Electron E2E; the author’s real-Chrome offline repro and screenshots supply that evidence. Retrying GSAP from the same CDN host cannot recover a host outage, but that pre-existing limitation is disclosed and does not invalidate this error-reporting fix. No merge or deployment action from this review.
— Review by tai (pr-review)
What broke
When a project's GSAP script can't load from its CDN (offline, blocked, or a TLS-intercepting proxy), Studio's preview tries a fallback copy. When that also failed, the fallback's promise rejected with nobody listening. The page got an unhandled rejection whose reason is a bare
Event(embedders such as Playwright log it aspageerror: Event), and the user was never told GSAP was missing. The only thing on screen was the knock-onUncaught TypeError: Cannot read properties of undefined (reading 'timeline')in "Console errors in preview".Repro: open a project made from the CLI blank template (GSAP from cdn.jsdelivr.net) with that host blocked.
Root cause
Two gaps on the same path:
studio-server/src/routes/preview.tsnever handled the case where both loads fail. Also, the fallback's own failed<script>reached the same capture listener, which would have reported the failure a second time.useConsoleErrorCapture) attaches to the preview window on the iframe'sload. Both GSAP loads fail beforeload, so anything the preview raised earlier never reached the modal. That includes the GSAP failure, and also any script error raised while the page loads.Fix
reportError(new Error("GSAP could not load from <authored URL> or <fallback URL>, so this preview's animations will not play.")). It skips its own fallback scripts, so each failure is reported once.window.__hfPreviewErrors(the name lives in@hyperframes/core/studio-preview-mark). When Studio's capture attaches, right after its existing reset atload, it adds that list, then listens live as before. The list is read, not drained. The capture owns every reset: each attach starts from an empty findings list, adds its live listener, then reads the window's whole list (one array, no argument spread). So a capture that attaches mid-load, attaches again atload, or attaches twice to the same window (React StrictMode) shows each error once, and a preview that goes away clears its findings. App no longer resets the list itself, which removes a hidden dependence on the order of twoloadlisteners.Proof
Red first, on unchanged main (devbox):
preview.test.ts"reports a GSAP script that fails from the CDN and from the fallback, never rejecting unhandled": fails with vitest'sUnhandled Rejection(the rawEvent) and no report.preview.test.ts"keeps every error a Studio preview raises from its first script, and leaves captures without it": fails, no list.useConsoleErrorCapture.test.tsx"shows the errors a preview raised before its load, once each": fails,consoleErrorsstaysundefined.useConsoleErrorCapture.test.tsx"shows a loaded preview's errors once when the capture attaches to it twice" (StrictMode): fails at the first head of this PR with the error shown twice; passes after the reset moved into attach.useConsoleErrorCapture.test.tsx"keeps listening after reading a preview's very long error list" (200,000 entries): failed when the list was spread into arguments (expected undefined to be 200001).useConsoleErrorCapture.test.tsx"clears a document's errors when its load replaces it, and when the preview goes away": failed on the null-preview case before the hook owned the reset.With the fix: the full
preview.test.tspasses 109/109 three runs in a row, and all four hook tests pass three runs in a row;tsc --noEmitpasses for studio, studio-server and core; oxfmt, oxlint, comment ratchet and comment citations pass.Real Studio (Vite dev server, Chrome for Testing 154, jsdelivr requests failed with
InternetDisconnected, the repo's CLI blank template as the project):Eventand thetimelineTypeError; the modal shows only the TypeError.Known behaviour change: after a save that fixes an error, the modal now closes when the reloaded preview loads, not the instant the save starts.
Not covered: the fallback still retries the same CDN host with a newer GSAP, so it can't help when that host is down. Serving a bundled GSAP is a separate change. Desktop's own surfaces were not exercised.
Before
Offline blank-template project on main: the GSAP failure is only an unhandled rejection with a bare
Event(pageerror: Event), which never reaches the modal; the modal shows only the knock-on TypeError.After
Same project at this head (bf50eff): the modal says GSAP could not load, and from where, before the TypeError.