/** * End-to-end smoke test with Playwright (uses a globally installed playwright if present). * BASE=https://www.spinza.dev node qa/e2e.mjs * Flow: landing → register → recovery code → lobby → open Neon Vault → spin → rewards → leaderboard → profile → mobile viewport. * Screenshots land in qa/screens/. */ import fs from "node:fs"; import path from "node:path"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); let chromium; try { ({ chromium } = require("playwright")); } catch { ({ chromium } = require(path.join(process.env.HOME ?? "", "Desktop/Projets/apps-web/fetcha/qa/node_modules/playwright"))); } const BASE = (process.env.BASE ?? "http://localhost:8230").replace(/\/$/, ""); const out = path.resolve("qa/screens"); fs.mkdirSync(out, { recursive: true }); const shot = (page, name) => page.screenshot({ path: path.join(out, `${name}.png`), fullPage: false }); const username = `qa_${Date.now().toString(36).slice(-6)}`; const password = "qa-password-123"; let failures = 0; const step = async (name, fn) => { try { await fn(); console.log(`✓ ${name}`); } catch (e) { failures++; console.log(`✗ ${name}: ${e.message}`); } }; const browser = await chromium.launch(); const ctx = await browser.newContext({ viewport: { width: 1380, height: 900 } }); const page = await ctx.newPage(); page.on("pageerror", (e) => console.log(" pageerror:", e.message)); await step("landing renders", async () => { await page.goto(BASE, { waitUntil: "networkidle" }); if (!(await page.getByText(/PLAY FREE/i).first().isVisible())) throw new Error("no CTA"); await shot(page, "01-landing"); }); await step("register + recovery code", async () => { await page.goto(`${BASE}/register`, { waitUntil: "networkidle" }); await page.getByLabel(/username/i).fill(username); const pw = page.locator('input[type="password"]'); await pw.nth(0).fill(password); await pw.nth(1).fill(password); await page.getByRole("checkbox").first().check(); await page.getByRole("button", { name: /create account/i }).click(); await page.getByText(/SPZ-[A-Z2-9]{4}-[A-Z2-9]{4}-[A-Z2-9]{4}/).waitFor({ timeout: 15000 }); await shot(page, "02-recovery-code"); const cb = page.getByRole("checkbox"); if (await cb.count()) await cb.last().check(); await page.getByRole("button", { name: /continue/i }).click(); await page.waitForURL(/\/(\?welcome=1)?$/, { timeout: 15000 }); }); await step("lobby shows balance 10,000 SC", async () => { await page.waitForLoadState("networkidle"); await page.getByText(/10,000/).first().waitFor({ timeout: 15000 }); await shot(page, "03-lobby"); }); await step("open Neon Vault and spin", async () => { await page.goto(`${BASE}/games/neon-vault`, { waitUntil: "networkidle" }); const spin = page.getByRole("button", { name: /^spin$/i }); await spin.waitFor({ state: "visible", timeout: 20000 }); await page.waitForTimeout(1500); await spin.click(); await page.waitForTimeout(4500); await shot(page, "04-game"); const bal = await page.locator("text=/\\d{1,3}(,\\d{3})*/").first().textContent(); if (!bal) throw new Error("no balance"); }); await step("rewards page", async () => { await page.goto(`${BASE}/rewards`, { waitUntil: "networkidle" }); await page.getByText(/daily/i).first().waitFor(); await shot(page, "05-rewards"); }); await step("leaderboard + profile", async () => { await page.goto(`${BASE}/leaderboard`, { waitUntil: "networkidle" }); await shot(page, "06-leaderboard"); await page.goto(`${BASE}/profile`, { waitUntil: "networkidle" }); await page.getByText(username).first().waitFor(); await shot(page, "07-profile"); }); await step("mobile lobby + game", async () => { const m = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true, storageState: await ctx.storageState() }); const mp = await m.newPage(); await mp.goto(BASE, { waitUntil: "networkidle" }); await shot(mp, "08-mobile-lobby"); await mp.goto(`${BASE}/games/spinza-original`, { waitUntil: "networkidle" }); await mp.waitForTimeout(2500); await shot(mp, "09-mobile-game"); await m.close(); }); await browser.close(); console.log(failures ? `${failures} step(s) failed` : "all steps passed"); process.exit(failures ? 1 : 0);