// E2E browser tests using playwright-core + system chromium. // This is a fallback because the Playwright MCP server is configured for `chrome` // (not present in this env). It produces equivalent screenshots and assertions. const path = require("path"); const fs = require("fs"); const { chromium } = require("/home/claude/.npm/_npx/e41f203b7505f1fb/node_modules/playwright-core"); const URL = "http://localhost:3000"; const OUT = path.join(__dirname, "..", "test-results"); if (!fs.existsSync(OUT)) fs.mkdirSync(OUT, { recursive: true }); const flows = []; function pushResult(flow, status, screenshots, notes) { flows.push({ flow, status, screenshots, notes }); console.log(`${status === "pass" ? "PASS" : "FAIL"}: E2E ${flow} - ${notes}`); } async function shot(page, name) { const p = path.join(OUT, name); await page.screenshot({ path: p, fullPage: false }); return name; } (async () => { let browser; try { browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/chromium", args: ["--no-sandbox", "--disable-dev-shm-usage"], }); console.log("PASS: chromium launched"); // -------------------- E2E A: Skribbl flow -------------------- { const screenshots = []; let notes = []; try { const ctxA = await browser.newContext({ viewport: { width: 1280, height: 800 } }); const pageA = await ctxA.newPage(); await pageA.goto(URL + "/", { waitUntil: "networkidle" }); const title = await pageA.title(); const html = await pageA.content(); const hasBrand = /DrawTogether/i.test(html); if (!hasBrand) throw new Error("brand 'DrawTogether' not found on landing"); notes.push("landing has DrawTogether"); screenshots.push(await shot(pageA, "e2e-skribbl-01-landing.png")); // Click create-room CTA await pageA.click('[data-testid=create-room-cta]'); await pageA.waitForURL("**/create", { timeout: 5000 }); notes.push("navigated to /create"); // Fill nickname, click skribbl mode await pageA.click('[data-testid=mode-skribbl]'); await pageA.fill('[data-testid=nickname-input]', "Alice"); screenshots.push(await shot(pageA, "e2e-skribbl-02-create-form.png")); await pageA.click('[data-testid=create-submit]'); // wait for /room/XXXXXX await pageA.waitForURL(/\/room\/[A-Z0-9]{6}$/, { timeout: 8000 }); const lobbyUrl = pageA.url(); const code = lobbyUrl.match(/\/room\/([A-Z0-9]{6})/)[1]; notes.push("created room, code=" + code); // Wait for room-code element, ensure it shows the code await pageA.waitForSelector('[data-testid=room-code]', { timeout: 5000 }); const onPageCode = (await pageA.textContent('[data-testid=room-code]')).trim(); if (onPageCode !== code) throw new Error("room-code mismatch on page: " + onPageCode + " vs URL " + code); notes.push("room-code visible on page: " + onPageCode); screenshots.push(await shot(pageA, "e2e-skribbl-03-lobby-alice.png")); // Open second context for Bob const ctxB = await browser.newContext({ viewport: { width: 1280, height: 800 } }); const pageB = await ctxB.newPage(); await pageB.goto(URL + "/join", { waitUntil: "networkidle" }); await pageB.fill('[data-testid=room-code-input]', code); await pageB.fill('[data-testid=nickname-input]', "Bob"); await pageB.click('[data-testid=join-submit]'); await pageB.waitForURL(/\/room\/[A-Z0-9]{6}/, { timeout: 8000 }); notes.push("Bob joined the room"); // ensure player-list shows both await pageB.waitForSelector('[data-testid=player-list]', { timeout: 5000 }); screenshots.push(await shot(pageB, "e2e-skribbl-04-lobby-bob.png")); // Back to Alice — start the game await pageA.waitForSelector('[data-testid=start-game]', { timeout: 5000 }); // small wait to ensure both connected await pageA.waitForTimeout(500); await pageA.click('[data-testid=start-game]'); // play screen has [data-testid=room-pill] await pageA.waitForURL(/\/room\/[A-Z0-9]{6}\/play/, { timeout: 8000 }); await pageA.waitForSelector('[data-testid=room-pill]', { timeout: 5000 }); notes.push("Alice landed on /play"); screenshots.push(await shot(pageA, "e2e-skribbl-05-play-alice.png")); // Bob should also navigate to play try { await pageB.waitForURL(/\/room\/[A-Z0-9]{6}\/play/, { timeout: 8000 }); screenshots.push(await shot(pageB, "e2e-skribbl-06-play-bob.png")); notes.push("Bob landed on /play"); } catch (e) { notes.push("Bob did NOT auto-navigate to /play within timeout (minor): " + e.message); } await ctxA.close(); await ctxB.close(); pushResult("A", "pass", screenshots, notes.join("; ")); } catch (e) { pushResult("A", "fail", screenshots, "error: " + e.message + "; trace: " + (notes.join("; "))); } } // -------------------- E2E B: Join non-existent room -------------------- { const screenshots = []; let notes = []; try { const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } }); const page = await ctx.newPage(); await page.goto(URL + "/join", { waitUntil: "networkidle" }); await page.fill('[data-testid=room-code-input]', "ZZZZZZ"); await page.fill('[data-testid=nickname-input]', "Ghost"); await page.click('[data-testid=join-submit]'); // Should NOT navigate to a /room/ URL successfully — wait briefly to see await page.waitForTimeout(2000); const finalUrl = page.url(); const successful = /\/room\/[A-Z0-9]{6}$/.test(finalUrl); if (successful) throw new Error("incorrectly redirected to a room: " + finalUrl); // look for an error message const text = await page.textContent("body"); const hasError = /room not found|couldn[']t join|not found|error/i.test(text); notes.push("stayed on /join, hasErrorMessage=" + hasError); screenshots.push(await shot(page, "e2e-join-bad-01.png")); if (!hasError) { // not strictly fail — but flag pushResult("B", "fail", screenshots, "no error message visible after invalid code; final URL=" + finalUrl); } else { pushResult("B", "pass", screenshots, notes.join("; ") + "; finalUrl=" + finalUrl); } await ctx.close(); } catch (e) { pushResult("B", "fail", screenshots, "error: " + e.message); } } // -------------------- E2E C: Color mode lobby start -------------------- { const screenshots = []; let notes = []; try { const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } }); const page = await ctx.newPage(); await page.goto(URL + "/create", { waitUntil: "networkidle" }); await page.click('[data-testid=mode-color]'); await page.fill('[data-testid=nickname-input]', "Painter"); screenshots.push(await shot(page, "e2e-color-01-create.png")); await page.click('[data-testid=create-submit]'); await page.waitForURL(/\/room\/[A-Z0-9]{6}$/, { timeout: 8000 }); await page.waitForSelector('[data-testid=room-code]', { timeout: 5000 }); notes.push("color room created"); screenshots.push(await shot(page, "e2e-color-02-lobby.png")); // start game (color allows single player per server logic) await page.click('[data-testid=start-game]'); await page.waitForURL(/\/room\/[A-Z0-9]{6}\/play/, { timeout: 8000 }); // wait for color-canvas await page.waitForSelector('[data-testid=color-canvas]', { timeout: 8000 }); // also color palette const paletteCount = await page.locator('[data-testid^="color-#"]').count(); notes.push("color-canvas + palette swatches=" + paletteCount); screenshots.push(await shot(page, "e2e-color-03-play.png")); if (paletteCount < 1) throw new Error("no color palette swatches found"); pushResult("C", "pass", screenshots, notes.join("; ")); await ctx.close(); } catch (e) { pushResult("C", "fail", screenshots, "error: " + e.message + "; trace: " + notes.join("; ")); } } } catch (e) { console.log("FAIL: e2e bootstrap - " + e.message); } finally { if (browser) try { await browser.close(); } catch (_) {} console.log("__E2E_RESULTS__ " + JSON.stringify(flows)); process.exit(flows.every((f) => f.status === "pass") ? 0 : 1); } })();