/** * Drives the console's first-run experience in a real Chrome, for * scripts/e2e-setup-browser.sh: a brand-new user with nothing signs in, creates * a farm, gets gateway settings, connects a (stand-in) gateway with them, adds * a printer through the form, sees an alert when it fails, dismisses it, and * removes the printer. * * Results are `oklabel` / `FAILEDlabel`. The password, the gateway * token and the printer's access code never reach a log line. * * Environment: APP_URL, API_BASE, WS_BASE, SUPABASE_E2E_EMAIL, * SUPABASE_E2E_PASSWORD, FRAMES_FILE, ACCESS_CODE, CHROME_BIN. */ import { readFileSync } from "node:fs"; import puppeteer from "puppeteer-core"; const { APP_URL, API_BASE, WS_BASE, FRAMES_FILE, ACCESS_CODE } = process.env as Record; 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, args: ["--no-sandbox", "--disable-dev-shm-usage"], }); let gateway: ReturnType | undefined; try { const page = await browser.newPage(); const pageErrors: string[] = []; page.on("pageerror", (err) => pageErrors.push(String((err as Error).message ?? err).slice(0, 160))); const waitText = (selector: string, text: string, timeout = 30_000) => page .waitForFunction( (sel: string, want: string) => document.querySelector(sel)?.textContent?.includes(want) ?? false, { timeout }, selector, text, ) .then(() => true) .catch(() => false); await page.goto(`${APP_URL}/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, "a brand-new user signs in", signInError ?? ""); if (signInError) process.exit(1); // 1. nothing yet -> pointed at Setup await page.goto(`${APP_URL}/`, { waitUntil: "domcontentloaded", timeout: 60_000 }); say(await waitText(".fleet", "You have no farm yet"), "the empty fleet page points a new user to Setup"); // 2. onboarding await page.goto(`${APP_URL}/setup`, { waitUntil: "domcontentloaded", timeout: 60_000 }); await page.waitForSelector(".onboarding input", { timeout: 30_000 }); await page.type(".onboarding input", "Browser Farm"); await page.click(".onboarding button[type=submit]"); say(await waitText(".setup__farm h2", "Browser Farm"), "creating a farm from nothing works (org and farm)"); // 3. gateway settings await page.waitForSelector(".gateway-setup button", { timeout: 15_000 }); say( await page.$eval(".gateway-setup", (el) => el.textContent?.includes("No gateway is connected") ?? false), "setup says no gateway is connected yet", ); await page.click(".gateway-setup button"); await page.waitForSelector(".gateway-setup__settings", { timeout: 15_000 }); const settings = await page.$eval(".gateway-setup__settings", (el) => el.textContent ?? ""); const farmId = /farm_id = "([^"]+)"/.exec(settings)?.[1] ?? ""; const proxyToken = /gateway_token = "([^"]+)"/.exec(settings)?.[1] ?? ""; const uplink = /uplink_url = "([^"]+)"/.exec(settings)?.[1] ?? ""; say(Boolean(farmId && proxyToken && uplink.endsWith("/ws/edge/v1")), "the page shows complete gateway settings"); // 4. a gateway configured from exactly what the page showed const exchanged = await fetch(`${API_BASE}/auth/edge-token`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ farmId, edgeProxyToken: proxyToken }), }); say(exchanged.ok, "the token the page showed is accepted by the cloud"); const jwt = ((await exchanged.json()) as { token: string }).token; gateway = Bun.spawn(["bun", "scripts/fake-gateway-serve.ts", `${WS_BASE}/ws/edge/v1`, jwt, "120", FRAMES_FILE], { env: { ...process.env, FARM_ID: farmId, TELEMETRY_AFTER_PROVISION: "1" }, stdout: "ignore", stderr: "ignore", }); say(await waitText(".gateway-setup", "A gateway is connected", 30_000), "the page notices the gateway connecting"); await page.click(".gateway-setup button:last-of-type"); // Done: the token leaves the screen say( !(await page.$eval("body", (el, token) => el.textContent?.includes(token) ?? false, proxyToken)), "dismissing the settings removes the token from the page", ); // 5. add a printer const fields = await page.$$(".add-printer form input"); await fields[0]!.type("Bench P1P"); await fields[1]!.type("192.168.0.77"); await fields[2]!.type("01P00A000000007"); await fields[3]!.type(ACCESS_CODE); await page.click(".add-printer button[type=submit]"); say(await waitText(".add-printer", "Added Bench P1P", 45_000), "adding a printer through the form succeeds"); say(await waitText(".managed", "Bench P1P"), "the printer appears in the list"); const leftover = await page.$eval(".add-printer input[type=password]", (el) => (el as HTMLInputElement).value); say(leftover === "", "the access code is cleared from the form after sending"); const frames = readFileSync(FRAMES_FILE, "utf8"); say(frames.includes(ACCESS_CODE), "the gateway received the access code"); // 6. the printer fails -> an alert on the fleet page await page.goto(`${APP_URL}/`, { waitUntil: "domcontentloaded", timeout: 60_000 }); say(await waitText(".alert-list", "the print failed", 60_000), "a failed print raises an alert on the fleet page"); await page.click(".alert-list button"); const gone = await page .waitForFunction(() => document.querySelector(".alert-list") === null, { timeout: 20_000 }) .then(() => true) .catch(() => false); say(gone, "dismissing the alert removes it"); // 7. remove await page.goto(`${APP_URL}/setup`, { waitUntil: "domcontentloaded", timeout: 60_000 }); await page.waitForSelector(".managed button", { timeout: 20_000 }); await page.click(".managed button"); say(await waitText(".managed", "forget its access code"), "removing asks first, and says what it deletes"); const buttons = await page.$$(".managed button"); await buttons[0]!.click(); // "Yes, remove" const removed = await page .waitForFunction(() => document.querySelector(".managed") === null, { timeout: 30_000 }) .then(() => true) .catch(() => false); say(removed, "the printer is removed"); say(readFileSync(FRAMES_FILE, "utf8").includes("remove_printer"), "the gateway was told to forget it"); say(pageErrors.length === 0, "no uncaught page errors", pageErrors.slice(0, 2).join(" | ")); } finally { gateway?.kill(); await browser.close(); } process.exit(failed ? 1 : 0);