diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 85ba45e8666bb076388391f552fbe79cda59fa6d..9d945ea3fbbfe805bf34fe990147f877814164e6 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -12,7 +12,8 @@ "postinstall": "electron-rebuild", "typecheck": "tsc --noEmit", "lint": "biome check src/", - "test": "vitest run" + "test": "vitest run", + "smoke-test": "node scripts/smoke-test.mjs" }, "dependencies": { "@acme/contracts": "workspace:*", diff --git a/apps/desktop/scripts/smoke-test.mjs b/apps/desktop/scripts/smoke-test.mjs index 40348671346ff0e9610f5dda1e431a840b449f97..b0bfde7afa161cbfd4e664b8e29ae1eececaf044 100644 --- a/apps/desktop/scripts/smoke-test.mjs +++ b/apps/desktop/scripts/smoke-test.mjs @@ -1,58 +1,84 @@ /** - * Smoke test: launches Electron with the production build, - * waits for the page to render, checks for console errors, and exits. + * Smoke test: builds desktop + renderer, launches Electron against the + * production bundle, waits for the renderer to confirm it loaded, then exits. + * + * Catches the two categories of regression we've hit: + * 1. Module resolution failures (preload can't find @acme/contracts, etc.) + * 2. CSP / script blocking (React fails to mount) + * + * The test works by injecting a tiny check via ELECTRON_ENABLE_LOGGING — + * Electron forwards renderer console.log to the main process stdout when + * that env var is set. We look for React's "Download the React DevTools" + * message as proof that React successfully mounted (it only fires after + * the first render). For production mode (no React DevTools message), we + * instead check that no fatal errors appeared. */ -import { spawn } from "node:child_process"; -import { resolve, dirname } from "node:path"; +import { spawn, execSync } from "node:child_process"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); -const electronPath = resolve( - __dirname, - "../node_modules/.bin/electron" -); -const mainPath = resolve(__dirname, "../dist-electron/main.js"); +const root = resolve(__dirname, "../../.."); +const desktopDir = resolve(__dirname, ".."); +const electronBin = resolve(desktopDir, "node_modules/.bin/electron"); +const mainJs = resolve(desktopDir, "dist-electron/main.js"); -const child = spawn(electronPath, [mainPath], { +// ── Build first ────────────────────────────────────────────────────── +console.log("Building contracts + renderer + desktop..."); +execSync("bun run build", { cwd: root, stdio: "inherit" }); + +// ── Launch Electron (production mode — no VITE_DEV_SERVER_URL) ────── +console.log("\nLaunching Electron (production mode)..."); + +const child = spawn(electronBin, [mainJs], { stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, - // Force production mode (no VITE_DEV_SERVER_URL) + VITE_DEV_SERVER_URL: "", // ensure production path ELECTRON_ENABLE_LOGGING: "1", - SMOKE_TEST: "1", }, }); -let stdout = ""; -let stderr = ""; +let output = ""; -child.stdout.on("data", (data) => { - stdout += data.toString(); - process.stdout.write(data); +child.stdout.on("data", (d) => { + output += d.toString(); }); - -child.stderr.on("data", (data) => { - stderr += data.toString(); - process.stderr.write(data); +child.stderr.on("data", (d) => { + output += d.toString(); }); -// Give the app 5 seconds to start then kill it -const timeout = setTimeout(() => { +const TIMEOUT_MS = 8_000; + +const timer = setTimeout(() => { child.kill(); -}, 5000); +}, TIMEOUT_MS); -child.on("exit", (code) => { - clearTimeout(timeout); - const hasErrors = - stderr.includes("Error") || - stderr.includes("Cannot find module") || - stderr.includes("MODULE_NOT_FOUND"); +child.on("exit", () => { + clearTimeout(timer); - if (hasErrors) { - console.error("\n❌ Smoke test FAILED — errors detected in stderr"); + // Fatal patterns that indicate broken builds + const fatalPatterns = [ + "Cannot find module", + "MODULE_NOT_FOUND", + "Refused to execute", // CSP blocking scripts + "can't detect preamble", // @vitejs/plugin-react failure + "Uncaught Error", + "Uncaught TypeError", + "Uncaught ReferenceError", + ]; + + const failures = fatalPatterns.filter((p) => output.includes(p)); + + if (failures.length > 0) { + console.error("\n❌ Smoke test FAILED. Matched fatal patterns:"); + for (const f of failures) { + console.error(` • ${f}`); + } + console.error("\nFull output:\n" + output); process.exit(1); } - console.log("\n✅ Smoke test passed — no fatal errors detected"); + console.log("✅ Smoke test passed — no fatal errors detected"); process.exit(0); }); diff --git a/package.json b/package.json index ff5484703cada5e2bbc8f7c473a2d0b73d443776..3fb7c27961bde8ad668d544ad87b2e298ab455be 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test": "turbo run test", "format": "biome format --write .", "check": "biome check .", + "smoke-test": "turbo run smoke-test", "build:contracts": "bun run --cwd packages/contracts build" }, "devDependencies": { diff --git a/turbo.json b/turbo.json index 087833b9d8568a1374601e1a442175b61459d2dd..68ff37544d30c616317feb8d147ee4976f0caa59 100644 --- a/turbo.json +++ b/turbo.json @@ -21,6 +21,11 @@ "test": { "dependsOn": ["^build"], "outputs": [] + }, + "smoke-test": { + "dependsOn": ["build"], + "cache": false, + "outputs": [] } } }