/** * Drives the real Continuum web console in a real Chrome, for * scripts/e2e-camera.sh (BROWSER=1). * * Everything before this proved the camera path with a stand-in for the * browser. This is the browser: the actual Nuxt app, the actual * `WebRTCStreamView`, a real RTCPeerConnection pinned to relay-only, and Chrome's * own H.264 decoder. The question it answers is whether a person looking at * the fleet page would see a picture. * * Signing in: the login page only offers a magic link by email, which a * script cannot follow, so this signs in with the app's own Supabase client * (`$supabase.client`) using a password the caller just set on a throwaway * user. The app then behaves exactly as it does for a real session: cookie, * redirect, token on every API call, the telemetry socket. * * What it reads from the browser rather than trusting the UI: the page wraps * RTCPeerConnection so the selected ICE candidate pair can be inspected, and * the decoded frame is sampled on a canvas. It deliberately saves no * screenshot - the frame is the inside of someone's workshop, and pixel * statistics answer "is there a picture" without keeping one. * * Results are `oklabel` / `FAILEDlabel`. The password and token * never reach a log line. * * Environment: APP_URL, SUPABASE_E2E_EMAIL, SUPABASE_E2E_PASSWORD, CHROME_BIN. */ import puppeteer from "puppeteer-core"; const appUrl = process.env.APP_URL!; const email = process.env.SUPABASE_E2E_EMAIL!; const password = process.env.SUPABASE_E2E_PASSWORD!; const chromeBin = process.env.CHROME_BIN ?? "/usr/bin/google-chrome"; let failed = false; const say = (good: boolean, label: string, detail = "") => { if (!good) failed = true; console.log(`${good ? "ok" : "FAILED"}\t${label}${detail ? ` (${detail})` : ""}`); }; const browser = await puppeteer.launch({ executablePath: chromeBin, headless: true, // No sandbox because this container cannot create the namespaces Chrome's // sandbox needs (see continuum-local-env-limits); the page it loads is our // own dev server. args: ["--no-sandbox", "--disable-dev-shm-usage", "--autoplay-policy=no-user-gesture-required"], }); try { const page = await browser.newPage(); const pageErrors: string[] = []; page.on("pageerror", (err) => pageErrors.push(String((err as Error).message ?? err).slice(0, 160))); // Keeps every RTCPeerConnection the app creates, so its stats can be read. await page.evaluateOnNewDocument(() => { const Native = window.RTCPeerConnection; (window as any).__pcs = []; (window as any).RTCPeerConnection = function (this: unknown, ...args: any[]) { const pc = new Native(...(args as [RTCConfiguration?])); (window as any).__pcs.push(pc); return pc; } as unknown as typeof RTCPeerConnection; (window as any).RTCPeerConnection.prototype = Native.prototype; }); // The first load compiles the app, so it is slow. await page.goto(`${appUrl}/login`, { waitUntil: "domcontentloaded", timeout: 120_000 }); await page.waitForFunction( () => typeof (window as any).useNuxtApp === "function" && Boolean((window as any).useNuxtApp().$supabase), { timeout: 120_000 }, ); const signInError = await page.evaluate( async (e: string, p: string) => { const { client } = (window as any).useNuxtApp().$supabase; const { error } = await client.auth.signInWithPassword({ email: e, password: p }); return error ? String(error.message) : null; }, email, password, ); say(signInError === null, "the app's own Supabase client signs the user in", signInError ?? ""); if (signInError) process.exit(1); await page.goto(`${appUrl}/`, { waitUntil: "domcontentloaded", timeout: 60_000 }); const stayed = await page .waitForSelector(".fleet", { timeout: 60_000 }) .then(() => true) .catch(() => false); say(stayed && !page.url().includes("/login"), "the fleet page loads and does not bounce back to /login", page.url()); const live = await page .waitForFunction(() => document.querySelector(".app-shell__status.is-online") !== null, { timeout: 30_000 }) .then(() => true) .catch(() => false); say(live, "the header reports Live: the telemetry socket authenticated with the real token"); const connected = await page .waitForFunction(() => document.querySelector(".webrtc-view__status.is-connected") !== null, { timeout: 60_000, }) .then(() => true) .catch(() => false); const status = await page.evaluate( () => document.querySelector(".webrtc-view__status")?.textContent?.trim() ?? "no player rendered", ); say(connected, "the camera player reports connected", `status: ${status}`); if (connected) { // Waits for a decoded frame, then samples it twice a second apart. const decoded = await page .waitForFunction( () => { const v = document.querySelector(".webrtc-view video"); return Boolean(v && v.readyState >= 2 && v.videoWidth > 0); }, { timeout: 30_000 }, ) .then(() => true) .catch(() => false); const frame = decoded ? await page.evaluate(async () => { const v = document.querySelector(".webrtc-view video")!; const t0 = v.currentTime; await new Promise((r) => setTimeout(r, 1500)); const canvas = document.createElement("canvas"); canvas.width = 64; canvas.height = 36; const ctx = canvas.getContext("2d")!; ctx.drawImage(v, 0, 0, canvas.width, canvas.height); const px = ctx.getImageData(0, 0, canvas.width, canvas.height).data; let sum = 0; let sumSq = 0; const n = px.length / 4; for (let i = 0; i < px.length; i += 4) { const y = 0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2]; sum += y; sumSq += y * y; } const mean = sum / n; return { width: v.videoWidth, height: v.videoHeight, advanced: v.currentTime - t0, mean, stdev: Math.sqrt(Math.max(0, sumSq / n - mean * mean)), }; }) : null; say(decoded && frame !== null, "Chrome decoded a video frame", frame ? `${frame.width}x${frame.height}` : ""); if (frame) { say(frame.advanced > 0.5, "playback time is advancing, so the stream is live", `+${frame.advanced.toFixed(1)}s`); say(frame.stdev > 2, "the frame has picture content, not black or one flat colour", `mean luma ${frame.mean.toFixed(0)}, stdev ${frame.stdev.toFixed(1)}`); } // Read from the browser's own ICE stats, so "relay-only" is observed // rather than inferred from the configuration. const selected = await page.evaluate(async () => { for (const pc of (window as any).__pcs as RTCPeerConnection[]) { const stats = await pc.getStats(); const byId = new Map(); stats.forEach((s) => byId.set(s.id, s)); for (const s of byId.values()) { if (s.type === "transport" && s.selectedCandidatePairId) { const pair = byId.get(s.selectedCandidatePairId); const local = pair && byId.get(pair.localCandidateId); if (local) return local.candidateType as string; } } } return null; }); say(selected === "relay", "the browser's selected local candidate is a TURN relay", `type: ${selected ?? "none"}`); } say(pageErrors.length === 0, "the page raised no uncaught errors", pageErrors.slice(0, 2).join(" | ")); } finally { await browser.close(); } process.exit(failed ? 1 : 0);