Skip to content
File

Blob: tests/e2e/helpers.ts

typescript149 lines
1import { execFile } from "node:child_process";
2import { randomUUID } from "node:crypto";
3import { promisify } from "node:util";
4import { fileURLToPath } from "node:url";
5import { expect, type Page } from "playwright/test";
6 
7const execFileAsync = promisify(execFile);
8const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? "http://127.0.0.1:4173";
9const projectRoot = fileURLToPath(new URL("../..", import.meta.url));
10 
11export function makeDomainName(prefix: string) {
12 return `${prefix}-${randomUUID().slice(0, 8)}.test`;
13}
14 
15export async function stubTurnstile(page: Page, token = "e2e-turnstile-token") {
16 await page.route("https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit", async (route) => {
17 await route.fulfill({
18 contentType: "application/javascript",
19 body: `
20(() => {
21 const issuedToken = ${JSON.stringify(token)};
22 let counter = 0;
23 const widgets = new Map();
24
25 window.turnstile = {
26 render(container, options) {
27 const widgetId = \`widget-\${++counter}\`;
28 const host = document.createElement("div");
29 host.setAttribute("data-e2e-turnstile", options.action ?? "");
30 host.textContent = "E2E Turnstile";
31 container.replaceChildren(host);
32 widgets.set(widgetId, { container, options });
33 Promise.resolve().then(() => options.callback(issuedToken));
34 return widgetId;
35 },
36 reset(widgetId) {
37 const widget = widgets.get(widgetId);
38 if (widget) {
39 Promise.resolve().then(() => widget.options.callback(issuedToken));
40 }
41 },
42 remove(widgetId) {
43 const widget = widgets.get(widgetId);
44 if (widget) {
45 widget.container.replaceChildren();
46 widgets.delete(widgetId);
47 }
48 },
49 };
50})();
51 `,
52 });
53 });
54}
55 
56export async function signInAdmin(page: Page) {
57 await page.goto("/admin");
58 const signInLink = page.getByRole("link", { name: /sign in with tessera/i });
59 await expect(signInLink).toBeVisible({ timeout: 15_000 });
60 await signInLink.click();
61 await expect(page.getByText("Authenticated via tessera")).toBeVisible({ timeout: 15_000 });
62}
63 
64export function getDomainCard(page: Page, domain: string) {
65 // Scope to the per-domain row (`.rounded-lg`) rather than the outer
66 // domain manager card (`.rounded-xl`); otherwise multiple domains
67 // collapse onto the same locator and strict-mode rejects the click.
68 return page
69 .locator("div.rounded-lg")
70 .filter({
71 has: page.locator("strong", { hasText: domain }),
72 })
73 .filter({
74 has: page.getByRole("button", { name: /Disable|Enable/ }),
75 })
76 .first();
77}
78 
79export async function addDomain(page: Page, domain: string) {
80 await page.getByPlaceholder("example.com").fill(domain);
81 await page.getByRole("button", { name: "Add domain" }).click();
82 await expect(getDomainCard(page, domain)).toContainText(domain);
83}
84 
85export async function createTemporaryInbox(page: Page, domain: string) {
86 await page.goto("/");
87 await page.getByLabel("Domain").selectOption(domain);
88 const createInboxButton = page.getByRole("button", { name: "Create inbox" });
89 await expect(createInboxButton).toBeEnabled({ timeout: 15_000 });
90 await createInboxButton.click();
91 await expect(page).toHaveURL(/\/inbox\//);
92 const address = (await page.getByRole("heading", { level: 1 }).textContent())?.trim();
93 
94 if (!address) {
95 throw new Error("Inbox address was not rendered.");
96 }
97 
98 return address;
99}
100 
101export async function sendLocalEmail(options: {
102 htmlRemoteTest?: boolean;
103 htmlTest?: boolean;
104 subject: string;
105 to: string;
106}) {
107 const args = [
108 "run",
109 "email:local",
110 "--",
111 "--to",
112 options.to,
113 "--endpoint",
114 `${baseURL}/cdn-cgi/handler/email`,
115 "--subject",
116 options.subject,
117 ];
118 
119 if (options.htmlRemoteTest) {
120 args.push("--html-remote-test");
121 } else if (options.htmlTest) {
122 args.push("--html-test");
123 }
124 
125 await execFileAsync("npm", args, {
126 cwd: projectRoot,
127 env: process.env,
128 });
129}
130 
131export async function waitForEmail(page: Page, subject: string) {
132 const emailItem = page.locator("aside button").filter({ hasText: subject }).first();
133 
134 try {
135 await expect(emailItem).toBeVisible({ timeout: 15_000 });
136 } catch {
137 await page.getByRole("button", { name: "Refresh" }).click();
138 await expect(emailItem).toBeVisible({ timeout: 15_000 });
139 }
140 
141 return emailItem;
142}
143 
144export async function openEmail(page: Page, subject: string) {
145 const emailItem = await waitForEmail(page, subject);
146 await emailItem.click();
147 await expect(page.getByRole("heading", { level: 2, name: subject })).toBeVisible();
148}