#!/usr/bin/env node
import { createRequire } from "node:module";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = join(import.meta.dirname, "..");
const PROJECT_ID = "smoke-test";
export const SMOKE_COMPOSITION_HTML =
'
';
function json(body, status = 200) {
return { status, contentType: "application/json", body: JSON.stringify(body) };
}
function text(body, contentType = "text/plain") {
return { status: 200, contentType, body };
}
const PROJECT_PATH = `/api/projects/${PROJECT_ID}`;
const GET_RESPONSES = new Map([
[
"/api/projects",
json({ projects: [{ id: PROJECT_ID, dir: "/tmp/smoke-test", title: "Smoke test" }] }),
],
[
PROJECT_PATH,
json({
id: PROJECT_ID,
dir: "/tmp/smoke-test",
title: "Smoke test",
files: ["index.html"],
compositions: ["index.html"],
}),
],
[`${PROJECT_PATH}/preview`, text(SMOKE_COMPOSITION_HTML, "text/html")],
[`${PROJECT_PATH}/renders`, json({ renders: [] })],
[`${PROJECT_PATH}/lint`, json({ findings: [] })],
[
`${PROJECT_PATH}/storyboard`,
json({
exists: false,
path: "STORYBOARD.md",
globals: { extra: {} },
frames: [],
warnings: [],
script: { exists: false, path: "SCRIPT.md", content: "" },
}),
],
[`${PROJECT_PATH}/selection`, json({ selection: null, updatedAt: null })],
["/api/registry/blocks", json([])],
["/api/fonts", json({ fonts: [] })],
["/api/fonts/google", json({ fonts: [] })],
["/api/assets/global", json({ assets: [] })],
]);
const MUTATION_RESPONSES = new Map([
[`${PROJECT_PATH}/selection`, json({ ok: true, selection: null, updatedAt: null })],
]);
function projectFileResponse(pathname) {
if (!pathname.startsWith(`${PROJECT_PATH}/files/`)) return undefined;
const filename = decodeURIComponent(pathname.split("/files/")[1] ?? "");
return json({
filename,
content: filename === "index.html" ? SMOKE_COMPOSITION_HTML : "",
});
}
function projectPreviewResponse(pathname) {
if (!pathname.startsWith(`${PROJECT_PATH}/preview/`)) return undefined;
return pathname.endsWith("/.media/manifest.jsonl")
? text("", "application/x-ndjson")
: text(SMOKE_COMPOSITION_HTML, "text/html");
}
function gsapAnimationsResponse(pathname) {
if (!pathname.startsWith(`${PROJECT_PATH}/gsap-animations/`)) return undefined;
return json({ animations: [], timelineVar: "tl", preamble: "", postamble: "" });
}
function getStudioSmokeResponse(pathname) {
return (
GET_RESPONSES.get(pathname) ??
projectFileResponse(pathname) ??
projectPreviewResponse(pathname) ??
gsapAnimationsResponse(pathname)
);
}
function studioSmokeApiPathResponse(method, pathname) {
return method === "GET"
? (getStudioSmokeResponse(pathname) ?? null)
: (MUTATION_RESPONSES.get(pathname) ?? null);
}
export function studioSmokeApiResponse(method, requestUrl) {
const { pathname } = new URL(requestUrl);
return pathname.startsWith("/api/") ? studioSmokeApiPathResponse(method, pathname) : undefined;
}
export function isExpectedStudioSmokeError(message) {
return message.includes("favicon.ico");
}
async function loadPuppeteer() {
const requireFromProducer = createRequire(join(ROOT, "packages", "producer", "package.json"));
return requireFromProducer("puppeteer");
}
export async function runStudioRuntimeSmoke(targetUrl) {
const puppeteer = await loadPuppeteer();
const browser = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
const errors = [];
const unmockedApiRequests = [];
page.on("pageerror", (error) => errors.push(error.message));
page.on("console", (message) => {
if (message.type() === "error") errors.push(message.text());
});
await page.setRequestInterception(true);
page.on("request", (request) => {
const response = studioSmokeApiResponse(request.method(), request.url());
if (response === undefined) {
void request.continue();
return;
}
if (response === null) {
unmockedApiRequests.push(`${request.method()} ${new URL(request.url()).pathname}`);
void request.respond(json({ error: "unmocked smoke endpoint" }, 501));
return;
}
void request.respond(response);
});
try {
await page.goto(targetUrl, { waitUntil: "networkidle0", timeout: 30_000 });
await new Promise((resolve) => setTimeout(resolve, 3_000));
const errorBoundary = await page.evaluate(() => {
const textContent = document.body.innerText;
return textContent.includes("Something went wrong") ? textContent : null;
});
if (errorBoundary) errors.push(`React error boundary triggered: ${errorBoundary}`);
} finally {
await browser.close();
}
const fatal = errors.filter((error) => !isExpectedStudioSmokeError(error));
const failures = [
...new Set(unmockedApiRequests.map((request) => `unmocked API request: ${request}`)),
...fatal,
];
if (failures.length > 0) {
throw new Error(
`Studio runtime smoke failed:\n${failures.map((error) => `- ${error}`).join("\n")}`,
);
}
}
async function main() {
const targetUrl = process.argv[2] ?? "http://localhost:5199/#project=smoke-test";
await runStudioRuntimeSmoke(targetUrl);
console.log("PASS: studio loaded with schema-valid API fixtures and no runtime errors");
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
}