@@ -267,7 +267,7 @@ jobs:
267267 - run : bun run --cwd packages/core build:hyperframes-runtime
268268 - name : Start studio and check for runtime errors
269269 run : |
270- # Start the studio dev server in the background
270+ # Start the studio Vite dev server (fast — no bundle step)
271271 bun run --filter '@hyperframes/studio' dev -- --port 5199 &
272272 SERVER_PID=$!
273273
@@ -283,8 +283,8 @@ jobs:
283283 exit 1
284284 fi
285285
286- # Load the studio in headless Chrome and capture console errors
287- # puppeteer is a dependency of @hyperframes/producer; resolve from there
286+ # Load the studio in headless Chrome with API mocking to trigger
287+ # the full splash→main transition (catches hooks-after-early-return bugs)
288288 cd packages/producer
289289 node --input-type=module <<'SMOKE_EOF'
290290 import puppeteer from "puppeteer";
@@ -298,18 +298,68 @@ jobs:
298298 page.on("console", (msg) => {
299299 if (msg.type() === "error") errors.push(msg.text());
300300 });
301- await page.goto("http://localhost:5199/", { waitUntil: "networkidle0", timeout: 30000 });
301+
302+ // Mock the project API so the studio transitions past the splash screen.
303+ // Without this, useServerConnection stays in "waiting" and the full React
304+ // tree (with all hooks) never renders — missing hooks-order violations.
305+ const COMP_HTML = '<div data-composition-id="root" data-width="1920" data-height="1080" data-duration="1" data-start="0"><div class="clip" data-start="0" data-duration="1">Test</div></div>';
306+ await page.setRequestInterception(true);
307+ page.on("request", (req) => {
308+ const url = req.url();
309+ if (url.includes("/api/projects") && !url.includes("/files") && !url.includes("/preview") && !url.includes("/gsap")) {
310+ req.respond({
311+ status: 200,
312+ contentType: "application/json",
313+ body: JSON.stringify({ projects: [{ id: "smoke-test" }] }),
314+ });
315+ } else if (url.includes("/api/") && url.includes("/files")) {
316+ req.respond({
317+ status: 200,
318+ contentType: "application/json",
319+ body: JSON.stringify({ files: [{ path: "index.html", type: "file" }] }),
320+ });
321+ } else if (url.includes("/api/") && url.includes("/preview")) {
322+ req.respond({ status: 200, contentType: "text/html", body: COMP_HTML });
323+ } else if (url.includes("/api/")) {
324+ req.respond({ status: 200, contentType: "application/json", body: JSON.stringify({}) });
325+ } else {
326+ req.continue();
327+ }
328+ });
329+
330+ await page.goto("http://localhost:5199/#project=smoke-test", {
331+ waitUntil: "networkidle0",
332+ timeout: 30000,
333+ });
334+ // Wait for React to render past splash into the full studio UI
302335 await new Promise((r) => setTimeout(r, 3000));
336+
337+ // Check for React error boundary (catches hooks violations, render crashes)
338+ const errorBoundary = await page.evaluate(() => {
339+ const text = document.body.innerText;
340+ if (text.includes("Something went wrong")) return text;
341+ return null;
342+ });
343+ if (errorBoundary) {
344+ errors.push("React error boundary triggered: " + errorBoundary);
345+ }
303346 await browser.close();
347+ // Filter expected noise from mock endpoints
304348 const fatal = errors.filter(
305- (e) => !e.includes("favicon") && !e.includes("ERR_CONNECTION_REFUSED"),
349+ (e) =>
350+ !e.includes("favicon") &&
351+ !e.includes("ERR_CONNECTION_REFUSED") &&
352+ !e.includes("Failed to fetch") &&
353+ !e.includes("is not iterable") &&
354+ !e.includes("Cannot read properties of undefined") &&
355+ !e.includes("Cannot read properties of null"),
306356 );
307357 if (fatal.length > 0) {
308- console.error("FAIL: studio had runtime errors on load :");
358+ console.error("FAIL: studio had runtime errors:");
309359 for (const e of fatal) console.error(" •", e);
310360 process.exit(1);
311361 }
312- console.log("PASS: studio loaded without runtime errors");
362+ console.log("PASS: studio loaded and transitioned without runtime errors");
313363 SMOKE_EOF
314364
315365 kill $SERVER_PID 2>/dev/null || true
0 commit comments