import { APIRequestContext, Browser, BrowserContext, expect, Page, } from "@playwright/test"; const mailpitURL = process.env.MAILPIT_URL; const baseURL = process.env.BASE_URL; if (!mailpitURL || !baseURL) { throw new Error("MAILPIT_URL and BASE_URL are required"); } const tilePng = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAAA1BMVEXu8vcBVSTwAAAAH0lEQVRo3u3BAQ0AAADCoPdPbQ43oAAAAAAAAAAAvg0hAAABfxmcpwAAAABJRU5ErkJggg==", "base64", ); export type AuthenticatedBrowser = { context: BrowserContext; page: Page; email: string; }; export async function newIsolatedContext(browser: Browser): Promise { const context = await browser.newContext(); await context.route("http://tiles.e2e.invalid/**", async (route) => { await route.fulfill({ status: 200, contentType: "image/png", body: tilePng, }); }); return context; } export function captureBrowserFailures(page: Page): () => void { const failures: string[] = []; page.on("console", (message) => { if (message.type() === "error") { failures.push(`console: ${message.text()}`); } }); page.on("pageerror", (error) => { failures.push(`pageerror: ${error.message}`); }); page.on("requestfailed", (request) => { const url = request.url(); if (!url.startsWith("http://tiles.e2e.invalid/")) { failures.push(`requestfailed: ${request.method()} ${url} ${request.failure()?.errorText}`); } }); return () => expect(failures, failures.join("\n")).toEqual([]); } async function waitForMagicLink(request: APIRequestContext, email: string): Promise { let messageID: string | undefined; await expect .poll( async () => { const response = await request.get(`${mailpitURL}/api/v1/search`, { params: { query: `to:"${email}"`, limit: "10", }, }); expect(response.ok()).toBeTruthy(); const body = (await response.json()) as { messages?: Array<{ ID?: string }> }; messageID = body.messages?.[0]?.ID; return messageID; }, { message: `waiting for the Mailpit login message for ${email}`, timeout: 15_000, }, ) .toBeTruthy(); const response = await request.get(`${mailpitURL}/api/v1/message/${messageID}`); expect(response.ok()).toBeTruthy(); const message = (await response.json()) as { Text?: string; HTML?: string; }; const content = `${message.Text ?? ""}\n${message.HTML ?? ""}`; const match = content.match(/https?:\/\/[^\s"'<>]+\/users\/log-in\/[^\s"'<>]+/); if (!match) { throw new Error(`No magic login link found for ${email}`); } const link = new URL(match[0].replaceAll("&", "&")); const applicationOrigin = new URL(baseURL); link.protocol = applicationOrigin.protocol; link.host = applicationOrigin.host; return link.toString(); } export async function registerAndConfirm( browser: Browser, request: APIRequestContext, email: string, displayName: string, ): Promise { const context = await newIsolatedContext(browser); const page = await context.newPage(); await page.goto("/users/register"); await page.getByLabel("Email").fill(email); await page.getByLabel("Display name").fill(displayName); await page .getByLabel("I am 18 or older and accept the safety rules") .check(); await page.getByRole("button", { name: "Create an account" }).click(); await expect(page).toHaveURL(/\/users\/log-in$/); await expect(page.getByText(`An email was sent to ${email}`)).toBeVisible(); await page.goto(await waitForMagicLink(request, email)); await expect(page.getByRole("heading", { name: `Welcome ${email}` })).toBeVisible(); await page .getByRole("button", { name: "Confirm and log in only this time" }) .click(); await expect(page.getByText(email, { exact: true })).toBeVisible(); return { context, page, email }; } export async function selectOptionContaining( page: Page, label: string, expectedText: string, ): Promise { const select = page.getByLabel(label); const options = await select.locator("option").evaluateAll((nodes) => nodes.map((node) => ({ text: node.textContent?.trim() ?? "", value: (node as HTMLOptionElement).value, })), ); const option = options.find(({ text }) => text.includes(expectedText)); if (!option) { throw new Error(`No ${label} option contains ${expectedText}`); } await select.selectOption(option.value); }