190 lines
8.6 KiB
JavaScript
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);
|
|
}
|
|
})();
|