import assert from "node:assert/strict"; import fs from "node:fs"; import { chromium, expect } from "@playwright/test"; import { createClient } from "@supabase/supabase-js"; if (fs.existsSync(".env.local")) process.loadEnvFile(".env.local"); const origin = new URL(process.argv[2] || "http://127.0.0.1:3007").origin; const db = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.SUPABASE_SECRET_KEY, { auth: { persistSession: false, autoRefreshToken: false } }, ); const rooms = []; const browser = await chromium.launch({ headless: true, args: ["--use-angle=swiftshader", "--enable-unsafe-swiftshader"], }); try { const { data: maps, error } = await db .from("datasets") .select("id") .eq("state", "published") .limit(1); if (error) throw error; assert.ok(maps.length); const desktop = await browser.newContext({ viewport: { width: 1280, height: 900 }, locale: "en-US", }); const mobile = await browser.newContext({ viewport: { width: 390, height: 844 }, locale: "en-US", isMobile: true, hasTouch: true, }); const host = await desktop.newPage(), friend = await mobile.newPage(); const pageErrors = []; for (const page of [host, friend]) page.on("pageerror", (error) => pageErrors.push(error.message)); await host.goto(`${origin}/servers/${maps[0].id}`, { waitUntil: "networkidle", timeout: 60000, }); await host.getByRole("button", { name: "Multiplayer", exact: true }).click(); await host.getByLabel("Your player name").fill("Browser Host"); await host.getByLabel("Player limit (including you)").selectOption("2"); await host .getByRole("button", { name: "Create multiplayer room", exact: true }) .click(); await host.waitForURL("**/multiplayer/*", { timeout: 30000 }); const id = host.url().split("/").pop(); rooms.push(id); await expect(host.getByLabel("Invite link")).toHaveValue( `${origin}/multiplayer/${id}`, ); await friend.goto(host.url(), { waitUntil: "domcontentloaded", timeout: 60000, }); await friend.getByLabel("Your player name").fill("Browser Friend"); await friend.getByRole("button", { name: "Join room", exact: true }).click(); await expect( friend.getByText("You’re in! Waiting for the room owner to start.", { exact: true, }), ).toBeVisible(); await expect( host.getByRole("button", { name: "Start multiplayer match", exact: true }), ).toBeEnabled({ timeout: 20000 }); fs.mkdirSync("artifacts/multiplayer", { recursive: true }); await host.screenshot({ path: "artifacts/multiplayer/desktop-lobby.png", fullPage: true, }); await friend.screenshot({ path: "artifacts/multiplayer/mobile-lobby.png", fullPage: true, }); assert.ok( await friend.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, ), "mobile lobby fits viewport", ); await host .getByRole("button", { name: "Start multiplayer match", exact: true }) .click(); for (let round = 0; round < 5; round++) { await Promise.all([ expect(host.locator(".game-info strong").first()).toHaveText( `${round + 1} / 5`, { timeout: 30000 }, ), expect(friend.locator(".game-info strong").first()).toHaveText( `${round + 1} / 5`, { timeout: 30000 }, ), ]); await Promise.all([ expect(host.locator(".game-loading")).toHaveCount(0, { timeout: 45000 }), expect(friend.locator(".game-loading")).toHaveCount(0, { timeout: 45000, }), ]); if (round === 0) { await host.reload({ waitUntil: "domcontentloaded" }); await expect(host.locator(".game-loading")).toHaveCount(0, { timeout: 45000, }); } await friend .getByRole("button", { name: "Open guess map", exact: true }) .click(); for (const [page, fraction] of [ [host, 0.48], [friend, 0.7], ]) { const map = page.getByRole("application", { name: /Minecraft map/ }); const box = await map.boundingBox(); assert.ok(box); await map.click({ position: { x: box.width * fraction, y: box.height * 0.5 }, }); await expect( page.getByRole("button", { name: "Submit guess", exact: true }), ).toBeEnabled(); if (round === 0 && page === host) await page.screenshot({ path: "artifacts/multiplayer/desktop-round.png", }); await page .getByRole("button", { name: "Submit guess", exact: true }) .click(); } if (round < 4) { await expect( host.getByRole("heading", { name: "Round standings", exact: true }), ).toBeVisible({ timeout: 20000 }); await expect( friend.getByRole("heading", { name: "Round standings", exact: true }), ).toBeVisible({ timeout: 20000 }); await host.getByRole("button", { name: /^Next round/ }).click(); } console.log(`Browser round ${round + 1}/5 completed.`); } await expect( host.getByRole("heading", { name: "Final standings", exact: true }), ).toBeVisible({ timeout: 20000 }); await expect( friend.getByRole("heading", { name: "Final standings", exact: true }), ).toBeVisible({ timeout: 20000 }); assert.equal(await host.locator(".multiplayer-final tbody tr").count(), 2); await host.screenshot({ path: "artifacts/multiplayer/desktop-results.png", fullPage: true, }); await friend.screenshot({ path: "artifacts/multiplayer/mobile-results.png", fullPage: true, }); assert.ok( await friend.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, ), "mobile results fit viewport with table scrolling", ); assert.deepEqual(pageErrors, []); console.log( "Desktop/mobile browser multiplayer passed: create, invite, join, reconnect, five shared rounds, map guesses, and final standings. No page errors.", ); } finally { await browser.close(); if (rooms.length) { const { error } = await db .from("multiplayer_rooms") .delete() .in("id", rooms); if (error) throw new Error(`Browser test-room cleanup failed: ${error.message}`); } }