// Cloud-only (browser): the whole auth-routing arc in ONE session, end to end // through the real dev server (the e2e cloud target boots `vite dev` with the // SSR gate active, so this is the same routing that runs in prod): // // signed out → /login // click Sign in → WorkOS (emulator) callback → org-less → /create-org // create the first org → canonical dashboard at / // // The individual hops are also covered piecewise (cloud/unauthenticated-skeleton, // cloud/session-gate, scenarios/org-slug-routing) — those pin edge cases this // narrative doesn't (invalid-cookie clearing, token refresh, returnTo // open-redirect, unknown-slug 404). This one is the regression guard for the // happy path as a single continuous flow, and is the only test that pins the // bare-`/` → `//` dashboard canonicalization a fresh login lands on. // // The headless helpers sign in via a `login_hint` query param, but the // emulator's hosted AuthKit also serves a real form ("continue as a new user"), // so the browser flow just fills it — genuinely end-to-end through the UI. import { randomUUID } from "node:crypto"; import { expect } from "@effect/vitest"; import { Effect } from "effect"; import { scenario } from "../src/scenario"; import { Browser, Target } from "../src/services"; scenario( "Auth routing · signed out → login → onboarding → dashboard, one browser session", {}, Effect.gen(function* () { const browser = yield* Browser; yield* Target; // A brand-new user the emulator provisions on first login_hint — no org, so // the post-callback document is gated to onboarding. const email = `flow-${randomUUID().slice(0, 8)}@e2e.test`; // No cookies → a clean, signed-out browser. const anonymous = { label: "anonymous" }; yield* browser.session(anonymous, async ({ page, step }) => { await step("Signed out, the root redirects to /login", async () => { await page.goto("/", { waitUntil: "commit" }); await page.getByText("Sign in to manage your tools and integrations").waitFor(); }); expect(new URL(page.url()).pathname, "the signed-out root lands on /login").toBe("/login"); await step("Sign in through the UI → org-less session lands on onboarding", async () => { await page.getByRole("link", { name: "Sign in" }).click(); // The emulator's hosted AuthKit form: provision a brand-new user. await page.getByPlaceholder("new-user@example.com").fill(email); await page.getByRole("button", { name: /Continue/ }).click(); // callback mints the session → SSR gate sees no org → /create-org. await page.waitForURL((url) => url.pathname === "/create-org", { timeout: 30_000 }); await page.getByPlaceholder("Northwind Labs").waitFor({ timeout: 30_000 }); }); await step("Create the first org → canonical dashboard at /", async () => { const orgName = "Flow Test Org"; const orgNameInput = page.getByPlaceholder("Northwind Labs"); await page.waitForLoadState("networkidle"); await orgNameInput.fill(orgName); await page.waitForTimeout(250); if ((await orgNameInput.inputValue()) !== orgName) { await orgNameInput.fill(orgName); } expect(await orgNameInput.inputValue(), "org name survives create-org hydration").toBe( orgName, ); await page.getByRole("button", { name: "Create organization" }).click(); await page.getByText("Connect your MCP client").waitFor({ timeout: 30_000 }); await page.getByRole("button", { name: "Continue to app" }).click(); // The bare landing canonicalizes onto the new org's slug. await page.waitForURL((url) => /^\/[a-z0-9-]+\/?$/.test(url.pathname), { timeout: 30_000 }); await page.getByText("Integrations").first().waitFor({ timeout: 30_000 }); }); expect( /^\/[a-z0-9-]+\/?$/.test(new URL(page.url()).pathname), "the signed-in user ends on their org-slug dashboard", ).toBe(true); }); }), );