Skip to content
File

Blob: tests/e2e/specs/invite-flow.spec.ts

typescript86 lines
1import { readFileSync } from "node:fs";
2 
3import { expect, test } from "@playwright/test";
4 
5import { E2E_CONTEXT_PATH_ENV, type PersistedE2eContext } from "../global-setup";
6 
7const loadContext = (): PersistedE2eContext => {
8 const path = process.env[E2E_CONTEXT_PATH_ENV];
9 if (!path) throw new Error(`${E2E_CONTEXT_PATH_ENV} is not set; global-setup didn't run.`);
10 return JSON.parse(readFileSync(path, "utf8")) as PersistedE2eContext;
11};
12 
13const loadSetupState = () => ({ invite: loadContext().invite });
14 
15test.describe("invite → signup → sign-out → sign-in", () => {
16 // Sign-out lands on the landing page; the header pill flip is the
17 // confirmation. There's no dedicated /sign-out route any more.
18 test("walks a new user through the invite-driven signup and back through sign-in", async ({ page }) => {
19 const { invite } = loadSetupState();
20 
21 // 1. Land on the invite acceptance page. Email is locked because the
22 // invite was minted with a specific address — UI guards against
23 // sign-up under a different email than the one invited.
24 await page.goto(`/invite/${encodeURIComponent(invite.token)}`);
25 await expect(page.getByRole("heading", { name: /accept your invite/i })).toBeVisible();
26 
27 const emailInput = page.getByLabel("Email");
28 await expect(emailInput).toHaveValue(invite.email);
29 await expect(emailInput).toBeDisabled();
30 
31 await page.getByLabel("Name").fill("E2E Tester");
32 await page.getByLabel("Password").fill(invite.password);
33 
34 // Turnstile test keys (1x00…AA) always pass, but the widget still has
35 // to mount before the submit button becomes enabled. Wait on the
36 // button instead of polling the iframe state.
37 const submit = page.getByRole("button", { name: /accept invite/i });
38 await expect(submit).toBeEnabled({ timeout: 15_000 });
39 await submit.click();
40 
41 // 2. Successful acceptance lands on /account. invite-accept calls
42 // `session.refetch()` before navigating so the nanostore is fresh —
43 // don't poll networkidle here, Turnstile keeps that signal busy.
44 await page.waitForURL(/\/account$/);
45 await expect(page.getByRole("heading", { name: /your account/i })).toBeVisible({ timeout: 15_000 });
46 await expect(page.getByText(invite.email)).toBeVisible();
47 
48 // 3. Sign out via the header button, then verify we're not signed in.
49 // The control is a <button> in header.tsx (not a link), and the
50 // label sits in a tooltip / aria-label rather than visible text on
51 // narrow viewports — match by accessible name to cover both modes.
52 const [signOutResp] = await Promise.all([
53 page.waitForResponse((r) => r.url().includes("/api/auth/sign-out")),
54 page.getByRole("button", { name: /sign out/i }).click(),
55 ]);
56 expect(signOutResp.status()).toBe(200);
57 // After sign-out we land on the landing page (no dedicated /sign-out
58 // route — the header pill flip is the confirmation).
59 await page.waitForURL((url) => url.pathname === "/");
60 // The session cookie must be gone after Max-Age=0 takes effect; if it
61 // isn't, the next page.goto won't render the sign-in form.
62 const cookiesAfterSignOut = await page.context().cookies();
63 const lingering = cookiesAfterSignOut.find((c) => c.name === "better-auth.session_token");
64 expect(lingering).toBeUndefined();
65 await expect(page.getByRole("link", { name: /sign in/i }).first()).toBeVisible();
66 
67 // 4. Re-login through /sign-in with the credentials we just set up.
68 await page.goto("/sign-in");
69 await page.getByLabel("Email").fill(invite.email);
70 await page.getByLabel("Password").fill(invite.password);
71 
72 const signIn = page.getByRole("button", { name: /sign in/i });
73 await expect(signIn).toBeEnabled({ timeout: 15_000 });
74 const [signInResp] = await Promise.all([
75 page.waitForResponse((r) => r.url().includes("/api/sign-in")),
76 signIn.click(),
77 ]);
78 expect(signInResp.status()).toBe(200);
79 
80 // 5. Lands signed-in — account page is reachable and shows the email
81 // we registered with.
82 await page.waitForURL(/\/account$/);
83 await expect(page.getByText(invite.email)).toBeVisible();
84 });
85});