SPB Git

Repository Public

helpers.ts 3.9 KB 64 lines typescript

1import { execSync } from "node:child_process";2import { readFileSync } from "node:fs";3import { expect, type Page } from "@playwright/test";4import AxeBuilder from "@axe-core/playwright";56export const OUTBOX = "/tmp/spcal-e2e-outbox.jsonl";7export interface Mail { to: string; subject: string; text: string; html: string; ics?: { method: string; content: string } | null }8export const outbox = (): Mail[] => readFileSync(OUTBOX, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l) as Mail);9export async function waitForMail(match: (m: Mail) => boolean, timeoutMs = 30_000): Promise<Mail> {10  const until = Date.now() + timeoutMs;11  for (;;) { const m = outbox().find(match); if (m) return m; if (Date.now() > until) throw new Error("mail not delivered by the worker in time"); await new Promise((r) => setTimeout(r, 500)); }12}1314/** Runs a project script with the environment of the e2e run. */15export const run = (cmd: string) => execSync(cmd, { encoding: "utf8", env: process.env });16export const psql = (q: string) => execSync(`psql -At "${process.env.DATABASE_URL}" -c "${q.replaceAll('"', '\\"')}"`, { encoding: "utf8" }).trim();1718export async function expectAccessible(page: Page) {19  const r = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]).analyze();20  expect(r.violations.map((v) => `${v.id}: ${v.nodes.map((n) => n.target.join(" ")).slice(0, 3).join(" | ")}`)).toEqual([]);21}22export async function expectNoHorizontalScroll(page: Page) {23  const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);24  expect(overflow).toBeLessThanOrEqual(1);25}2627/** Books the first free slot of the first available day; returns the management token. */28export async function bookFirstSlot(page: Page, slug: string, who: { name: string; email: string; reason?: string }) {29  await page.goto(`/book/${slug}`);30  await page.locator('[data-calendar] button:not([disabled])').first().click();31  const time = page.locator('ul[aria-label] button').first();32  const label = (await time.textContent())!.trim();33  await time.click();34  await page.getByLabel(/Nom complet|Full name/).fill(who.name);35  await page.getByLabel(/^Courriel|^E-mail/).fill(who.email);36  await page.getByLabel(/Motif|Reason/).fill(who.reason ?? "Discussion de projet");37  await page.getByRole("checkbox").check();38  await page.waitForTimeout(2700); // production refuses a form sent within 2.5 s of being issued (anti-bot)39  await page.getByRole("button", { name: /Envoyer la demande|Confirmer le rendez-vous|Send the request|Confirm the appointment/ }).click();40  const result = page.getByTestId("booking-result");41  await expect(result).toBeVisible();42  const href = await page.getByRole("link", { name: /Suivre ou gérer|Follow or manage/ }).getAttribute("href");43  return { token: href!.split("/").pop()!, timeLabel: label, status: await result.getAttribute("data-status") };44}4546/** Chromium virtual authenticator: a real WebAuthn ceremony with user verification, without hardware. */47export async function virtualAuthenticator(page: Page) {48  const cdp = await page.context().newCDPSession(page);49  await cdp.send("WebAuthn.enable");50  await cdp.send("WebAuthn.addVirtualAuthenticator", { options: { protocol: "ctap2", transport: "internal", hasResidentKey: true, hasUserVerification: true, isUserVerified: true, automaticPresenceSimulation: true } });51  return cdp;52}53/** Enrols a passkey with a fresh one-time link and lands in the admin area. */54export async function signInAsOwner(page: Page) {55  await virtualAuthenticator(page);56  const url = run("node_modules/.bin/tsx scripts/setup-token.ts").split("\n")[0].trim();57  await page.goto(url.replace(/^https?:\/\/[^/]+/, ""));58  await page.getByRole("button", { name: /Créer la clé|Create the passkey/ }).click();59  const cont = page.getByRole("button", { name: /J'ai conservé|I saved/ });60  await cont.waitFor();61  await cont.click();62  await page.waitForURL(/\/admin$/);63}64
Files main
Menu

Theme