Files
MineGeo/scripts/test-multiplayer-browser.mjs

278 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { randomUUID, randomBytes } from "node:crypto";
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 = [],
accounts = [];
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 email = `multiplayer-browser-${randomUUID()}@example.com`,
password = randomBytes(24).toString("base64url");
const fixture = await db.auth.admin.createUser({
email,
password,
email_confirm: true,
user_metadata: { display_name: "Browser Host" },
});
if (fixture.error) throw fixture.error;
accounts.push(fixture.data.user.id);
const loggedIn = await desktop.request.post(`${origin}/api/v1/auth`, {
headers: { origin },
data: { action: "login", email, password },
});
assert.equal(loggedIn.status(), 200);
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 expect(host.locator(".multiplayer-settings")).toContainText(
"Browser Host",
);
await expect(host.getByLabel("Your player name")).toHaveCount(0);
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 expect(friend.getByRole("button", { name: /^Chat/ })).toHaveCount(0);
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 });
await host.getByRole("button", { name: /^Chat/ }).click();
await host
.getByLabel("Message", { exact: true })
.fill("Hello from the lobby (: <script>window.chatInjected=true</script>");
await host.getByRole("button", { name: "Send message", exact: true }).click();
await expect(host.getByRole("log")).toContainText("Hello from the lobby");
await expect(friend.locator(".room-chat-unread")).toHaveText("1", {
timeout: 15000,
});
await friend.getByRole("button", { name: /^Chat/ }).click();
await expect(friend.getByRole("log")).toContainText("Hello from the lobby");
assert.equal(
await friend.evaluate(() => window.chatInjected),
undefined,
"chat renders HTML as plain text",
);
await friend
.getByLabel("Message", { exact: true })
.fill("Привет с телефона!");
await friend.getByLabel("Message", { exact: true }).press("Enter");
await expect(host.getByRole("log")).toContainText("Привет с телефона!", {
timeout: 15000,
});
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();
await expect(friend.getByRole("log")).toContainText("Hello from the lobby", {
timeout: 15000,
});
await friend.getByRole("button", { name: "Close chat", exact: true }).click();
await host.getByRole("button", { name: "Close chat", 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 host.getByRole("button", { name: /^Chat/ }).click();
await expect(host.getByRole("log")).toContainText("Привет с телефона!");
await host
.getByLabel("Message", { exact: true })
.fill("Good luck this round!");
await host
.getByRole("button", { name: "Send message", exact: true })
.click();
await expect(friend.locator(".room-chat-unread")).toHaveText("1", {
timeout: 15000,
});
await friend.getByRole("button", { name: /^Chat/ }).click();
await expect(friend.getByRole("log")).toContainText(
"Good luck this round!",
);
await friend.screenshot({
path: "artifacts/multiplayer/mobile-chat.png",
});
await host.screenshot({ path: "artifacts/multiplayer/desktop-chat.png" });
assert.ok(
await friend.evaluate(
() => document.documentElement.scrollWidth <= innerWidth,
),
"mobile chat fits the viewport",
);
await friend.getByLabel("Message", { exact: true }).press("Escape");
await expect(friend.getByRole("button", { name: /^Chat/ })).toBeFocused();
await host
.getByRole("button", { name: "Close chat", exact: true })
.click();
}
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 friend.getByRole("button", { name: /^Chat/ }).click();
await friend
.getByLabel("Message", { exact: true })
.fill("GG, thanks for playing!");
await friend
.getByRole("button", { name: "Send message", exact: true })
.click();
await host.getByRole("button", { name: /^Chat/ }).click();
await expect(host.getByRole("log")).toContainText("GG, thanks for playing!", {
timeout: 15000,
});
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: lobby and in-game chat, unread badges, plain-text rendering, keyboard controls, reconnect, five shared rounds, map guesses, and final standings chat. 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}`);
}
for (const id of accounts) {
const profile = await db.from("profiles").delete().eq("id", id);
if (profile.error) throw profile.error;
const account = await db.auth.admin.deleteUser(id);
if (account.error) throw account.error;
}
}