who_need_help/e2e/tests/helpers.ts

161 lines
4.4 KiB
TypeScript

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<BrowserContext> {
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<string> {
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("&amp;", "&"));
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<AuthenticatedBrowser> {
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<void> {
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);
}