Files

190 lines
8.6 KiB
JavaScript

// 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);
}
})();