File
Blob: tests/e2e/helpers.ts
| 1 | import { execFile } from "node:child_process"; |
| 2 | import { randomUUID } from "node:crypto"; |
| 3 | import { promisify } from "node:util"; |
| 4 | import { fileURLToPath } from "node:url"; |
| 5 | import { expect, type Page } from "playwright/test"; |
| 6 | |
| 7 | const execFileAsync = promisify(execFile); |
| 8 | const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? "http://127.0.0.1:4173"; |
| 9 | const projectRoot = fileURLToPath(new URL("../..", import.meta.url)); |
| 10 | |
| 11 | export function makeDomainName(prefix: string) { |
| 12 | return `${prefix}-${randomUUID().slice(0, 8)}.test`; |
| 13 | } |
| 14 | |
| 15 | export 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 | |
| 56 | export 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 | |
| 64 | export 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 | |
| 79 | export 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 | |
| 85 | export 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 | |
| 101 | export 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 | |
| 131 | export 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 | |
| 144 | export 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 | } |