import { test, expect, Page, APIRequestContext } from "@playwright/test"; const API_BASE = "http://localhost:8000"; const ADMIN_EMAIL = "admin@example.com"; const ADMIN_PASSWORD = "admin123"; // Helper to generate unique email for each test function uniqueEmail(): string { return `counter-${Date.now()}-${Math.random().toString(36).substring(7)}@example.com`; } // Helper to create an invite via API async function createInvite(request: APIRequestContext): Promise { const loginResp = await request.post(`${API_BASE}/api/auth/login`, { data: { email: ADMIN_EMAIL, password: ADMIN_PASSWORD }, }); const cookies = loginResp.headers()["set-cookie"]; const meResp = await request.get(`${API_BASE}/api/auth/me`, { headers: { Cookie: cookies }, }); const admin = await meResp.json(); const inviteResp = await request.post(`${API_BASE}/api/admin/invites`, { data: { godfather_id: admin.id }, headers: { Cookie: cookies }, }); const invite = await inviteResp.json(); return invite.identifier; } // Helper to authenticate a user with invite-based signup async function authenticate(page: Page, request: APIRequestContext): Promise { const email = uniqueEmail(); const inviteCode = await createInvite(request); await page.context().clearCookies(); await page.goto("/signup"); // Enter invite code first await page.fill('input#inviteCode', inviteCode); // Click and wait for invite check API to complete await Promise.all([ page.waitForResponse((resp) => resp.url().includes("/check") && resp.status() === 200), page.click('button[type="submit"]'), ]); // Wait for registration form await expect(page.locator("h1")).toHaveText("Create account"); // Fill registration await page.fill('input#email', email); await page.fill('input#password', "password123"); await page.fill('input#confirmPassword', "password123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL("/"); return email; } test.describe("Counter - Authenticated", () => { test("displays counter value", async ({ page, request }) => { await authenticate(page, request); await expect(page.locator("h1")).toBeVisible(); // Counter should be a number (not loading state) const text = await page.locator("h1").textContent(); expect(text).toMatch(/^\d+$/); }); test("displays current count label", async ({ page, request }) => { await authenticate(page, request); await expect(page.getByText("Current Count")).toBeVisible(); }); test("clicking increment button increases counter", async ({ page, request }) => { await authenticate(page, request); await expect(page.locator("h1")).not.toHaveText("..."); const before = await page.locator("h1").textContent(); await page.click("text=Increment"); await expect(page.locator("h1")).toHaveText(String(Number(before) + 1)); }); test("clicking increment multiple times", async ({ page, request }) => { await authenticate(page, request); await expect(page.locator("h1")).not.toHaveText("..."); const before = Number(await page.locator("h1").textContent()); // Click increment and wait for each update to complete await page.click("text=Increment"); await expect(page.locator("h1")).not.toHaveText(String(before)); const afterFirst = Number(await page.locator("h1").textContent()); await page.click("text=Increment"); await expect(page.locator("h1")).not.toHaveText(String(afterFirst)); const afterSecond = Number(await page.locator("h1").textContent()); await page.click("text=Increment"); await expect(page.locator("h1")).not.toHaveText(String(afterSecond)); // Final value should be at least 3 more than we started with const final = Number(await page.locator("h1").textContent()); expect(final).toBeGreaterThanOrEqual(before + 3); }); test("counter persists after page reload", async ({ page, request }) => { await authenticate(page, request); await expect(page.locator("h1")).not.toHaveText("..."); const before = await page.locator("h1").textContent(); await page.click("text=Increment"); const expected = String(Number(before) + 1); await expect(page.locator("h1")).toHaveText(expected); await page.reload(); await expect(page.locator("h1")).toHaveText(expected); }); test("counter is shared between users", async ({ page, browser, request }) => { // First user increments await authenticate(page, request); await expect(page.locator("h1")).not.toHaveText("..."); const initialValue = Number(await page.locator("h1").textContent()); await page.click("text=Increment"); await page.click("text=Increment"); // Wait for the counter to update (value should increase by 2 from what this user started with) await expect(page.locator("h1")).not.toHaveText(String(initialValue)); const afterFirstUser = Number(await page.locator("h1").textContent()); expect(afterFirstUser).toBeGreaterThan(initialValue); // Second user in new context sees the current value const page2 = await browser.newPage(); await authenticate(page2, request); await expect(page2.locator("h1")).not.toHaveText("..."); const page2InitialValue = Number(await page2.locator("h1").textContent()); // The value should be at least what user 1 saw (might be higher due to parallel tests) expect(page2InitialValue).toBeGreaterThanOrEqual(afterFirstUser); // Second user increments await page2.click("text=Increment"); // Wait for counter to update - use >= because parallel tests may also increment await expect(page2.locator("h1")).not.toHaveText(String(page2InitialValue)); const page2AfterIncrement = Number(await page2.locator("h1").textContent()); expect(page2AfterIncrement).toBeGreaterThanOrEqual(page2InitialValue + 1); // First user reloads and sees the increment (value should be >= what page2 has) await page.reload(); await expect(page.locator("h1")).not.toHaveText("..."); const page1Reloaded = Number(await page.locator("h1").textContent()); expect(page1Reloaded).toBeGreaterThanOrEqual(page2InitialValue + 1); await page2.close(); }); }); test.describe("Counter - Unauthenticated", () => { test("redirects to login when accessing counter without auth", async ({ page }) => { await page.context().clearCookies(); await page.goto("/"); await expect(page).toHaveURL("/login"); }); test("shows login form when redirected", async ({ page }) => { await page.context().clearCookies(); await page.goto("/"); await expect(page.locator("h1")).toHaveText("Welcome back"); }); }); test.describe("Counter - Session Integration", () => { test("can access counter after login", async ({ page, request }) => { const email = uniqueEmail(); const inviteCode = await createInvite(request); // Sign up with invite await page.goto("/signup"); await page.fill('input#inviteCode', inviteCode); await page.click('button[type="submit"]'); await expect(page.locator("h1")).toHaveText("Create account"); await page.fill('input#email', email); await page.fill('input#password', "password123"); await page.fill('input#confirmPassword', "password123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL("/"); // Logout await page.click("text=Sign out"); await expect(page).toHaveURL("/login"); // Login again await page.fill('input[type="email"]', email); await page.fill('input[type="password"]', "password123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL("/"); // Counter should be visible - wait for it to load (not showing "...") await expect(page.locator("h1")).toBeVisible(); await expect(page.locator("h1")).not.toHaveText("..."); const text = await page.locator("h1").textContent(); expect(text).toMatch(/^\d+$/); }); test("counter API requires authentication", async ({ page }) => { // Try to access counter API directly without auth const response = await page.request.get("http://localhost:8000/api/counter"); expect(response.status()).toBe(401); }); test("counter increment API requires authentication", async ({ page }) => { const response = await page.request.post("http://localhost:8000/api/counter/increment"); expect(response.status()).toBe(401); }); });