SPB Git

Repository Public

public.spec.ts 6.0 KB 94 lines typescript

1import { expect, test } from "@playwright/test";2import { bookFirstSlot, expectAccessible, expectNoHorizontalScroll, psql, waitForMail } from "./helpers";34test.describe.configure({ mode: "serial" });56test("home lists the appointment types and is accessible", async ({ page }) => {7  await page.goto("/");8  await expect(page.getByRole("heading", { level: 1 })).toContainText("Camille Tremblay");9  await expect(page.getByRole("link", { name: /Rencontre de 30 minutes/ })).toBeVisible();10  await expectAccessible(page);11  await expectNoHorizontalScroll(page);12});1314test("a visitor submits a request: received, pending, never shown as confirmed", async ({ page }, info) => {15  const email = `visiteur-${info.project.name}@example.test`;16  const r = await bookFirstSlot(page, "rencontre-30", { name: "Alex Visiteur", email, reason: "Motif-confidentiel-ZZZ" });17  expect(r.status).toBe("pending");18  await expect(page.getByRole("heading", { name: "Demande reçue" })).toBeVisible();19  await expect(page.getByText("En attente d'approbation")).toBeVisible();20  await expect(page.getByText(/Rendez-vous confirmé/)).toHaveCount(0);21  await expect(page.getByRole("link", { name: /\.ics/ })).toHaveCount(0);22  await expectAccessible(page);23  // nothing confirmed in the database either24  expect(psql(`SELECT status FROM booking_requests r JOIN contacts c ON c.id = r.contact_id WHERE c.email = '${email}'`)).toBe("pending");25  expect(psql(`SELECT count(*) FROM bookings b JOIN contacts c ON c.id = b.contact_id WHERE c.email = '${email}'`)).toBe("0");26  // the worker really delivers the acknowledgement; it says "not confirmed" and never carries the reason or an invitation27  const mail = await waitForMail((m) => m.to === email && m.subject.startsWith("Demande reçue"));28  expect(mail.text).toContain("pas encore un rendez-vous confirmé");29  expect(mail.text + mail.html).not.toContain("ZZZ");30  expect(mail.ics ?? null).toBeNull();3132  // management link: pending state, and the held slot is no longer offered to someone else33  await page.goto(`/manage/${r.token}`);34  await expect(page.getByTestId("manage")).toHaveAttribute("data-state", "pending");35  await expectAccessible(page);36  await expectNoHorizontalScroll(page);37  const held = psql(`SELECT to_char(r.starts_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS".000Z"') FROM booking_requests r JOIN contacts c ON c.id = r.contact_id WHERE c.email = '${email}'`);38  const day = held.slice(0, 10);39  const res = await page.request.get(`/api/v1/public/event-types/rencontre-30/slots?from=${day}&to=${day}`);40  expect(((await res.json()) as { slots: string[] }).slots).not.toContain(held);41});4243test("the booking page is usable with the keyboard and adapts to the visitor's time zone", async ({ browser }) => {44  const ctx = await browser.newContext({ timezoneId: "Europe/Paris", locale: "fr-FR" });45  const page = await ctx.newPage();46  await page.goto("/book/rencontre-30");47  await expect(page.getByText(/Paris/).first()).toBeVisible();48  const first = page.locator('[data-calendar] button:not([disabled])').first();49  await first.focus(); await page.keyboard.press("Enter");50  await expect(page.locator('ul[aria-label] button').first()).toBeVisible();51  // Toronto opens 9:00–17:00, i.e. 15:00–23:00 in Paris: every time shown is in the visitor's zone52  const times = await page.locator('ul[aria-label] button').allTextContents();53  expect(times.length).toBeGreaterThan(0);54  for (const t of times) expect(Number(t.trim().split(/\s|:/)[0])).toBeGreaterThanOrEqual(15);55  await expect(page.getByText(/à Toronto/)).toBeVisible();56  await expectAccessible(page);57  await ctx.close();58});5960test("server-side validation and anti-abuse on the public API", async ({ request }) => {61  const post = (data: Record<string, unknown>, headers: Record<string, string> = {}) => request.post("/api/v1/public/requests", { data, headers });62  const token = ((await (await request.get("/api/v1/public/form-token")).json()) as { formToken: string }).formToken;63  const base = { slug: "rencontre-30", start: "2030-01-07T15:00:00.000Z", name: "Bot", email: "bot@example.test", reason: "x", visitorTz: "America/Toronto", locale: "fr", idempotencyKey: crypto.randomUUID(), consent: true, formToken: token };64  await new Promise((r) => setTimeout(r, 2700)); // the form token refuses instant submissions in production65  expect((await post({ ...base, consent: false })).status()).toBe(400);                       // consent is mandatory66  expect((await post({ ...base, email: "not-an-email" })).status()).toBe(400);67  expect((await post({ ...base, formToken: "123.forged" })).status()).toBe(400);              // forged form token68  expect((await post({ ...base, website: "http://spam" })).status()).toBe(400);               // honeypot69  expect((await post(base, { origin: "https://evil.example" })).status()).toBe(403);          // cross-site POST70  const far = await post(base);                                                                // outside the horizon71  expect(far.status()).toBe(409);72  const past = await post({ ...base, start: "2020-01-06T15:00:00.000Z", idempotencyKey: crypto.randomUUID() });73  expect(past.status()).toBe(409);74  expect(((await past.json()) as { error: { code: string } }).error.code).toBe("slot_past");75});7677test("invalid management link and unknown pages", async ({ page }) => {78  await page.goto("/manage/this-token-does-not-exist");79  await expect(page.getByTestId("manage-error")).toHaveAttribute("data-code", "link_invalid");80  await expect(page.getByText("Lien invalide")).toBeVisible();81  const res = await page.goto("/book/does-not-exist");82  expect(res!.status()).toBe(404);83  await expect(page.getByRole("heading", { name: "Page introuvable" })).toBeVisible();84});8586test("English interface", async ({ browser }) => {87  const ctx = await browser.newContext({ locale: "en-CA" });88  const page = await ctx.newPage();89  await page.goto("/");90  await expect(page.getByRole("heading", { level: 1 })).toContainText("Book a time with");91  await expect(page.locator("html")).toHaveAttribute("lang", "en-CA");92  await ctx.close();93});94
Files main
Menu

Theme