import { test, expect, Page } from "@playwright/test"; /** * Appointments Page E2E Tests * * Tests for viewing and cancelling user appointments. */ const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; function getRequiredEnv(name: string): string { const value = process.env[name]; if (!value) { throw new Error(`Required environment variable ${name} is not set.`); } return value; } const REGULAR_USER = { email: getRequiredEnv("DEV_USER_EMAIL"), password: getRequiredEnv("DEV_USER_PASSWORD"), }; const ADMIN_USER = { email: getRequiredEnv("DEV_ADMIN_EMAIL"), password: getRequiredEnv("DEV_ADMIN_PASSWORD"), }; async function clearAuth(page: Page) { await page.context().clearCookies(); } async function loginUser(page: Page, email: string, password: string) { await page.goto("/login"); await page.fill('input[type="email"]', email); await page.fill('input[type="password"]', password); await page.click('button[type="submit"]'); await page.waitForURL((url) => !url.pathname.includes("/login"), { timeout: 10000 }); } // Helper to format date as YYYY-MM-DD in local timezone function formatDateLocal(d: Date): string { const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } function getTomorrowDateStr(): string { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); return formatDateLocal(tomorrow); } // Set up availability and create a booking async function createTestBooking(page: Page) { const dateStr = getTomorrowDateStr(); // First login as admin to set availability await clearAuth(page); await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); const adminCookies = await page.context().cookies(); const adminAuthCookie = adminCookies.find(c => c.name === "auth_token"); if (!adminAuthCookie) throw new Error("No admin auth cookie"); await page.request.put(`${API_URL}/api/admin/availability`, { headers: { Cookie: `auth_token=${adminAuthCookie.value}`, "Content-Type": "application/json", }, data: { date: dateStr, slots: [{ start_time: "09:00:00", end_time: "12:00:00" }], }, }); // Login as regular user await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); const userCookies = await page.context().cookies(); const userAuthCookie = userCookies.find(c => c.name === "auth_token"); if (!userAuthCookie) throw new Error("No user auth cookie"); // Create booking - use a random minute to avoid conflicts with parallel tests const randomMinute = Math.floor(Math.random() * 11) * 15; // 0, 15, 30, 45 etc up to 165 min const hour = 9 + Math.floor(randomMinute / 60); const minute = randomMinute % 60; const timeStr = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:00`; const response = await page.request.post(`${API_URL}/api/booking`, { headers: { Cookie: `auth_token=${userAuthCookie.value}`, "Content-Type": "application/json", }, data: { slot_start: `${dateStr}T${timeStr}Z`, note: "Test appointment", }, }); return response.json(); } test.describe("Appointments Page - Regular User Access", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); }); test("regular user can access appointments page", async ({ page }) => { await page.goto("/appointments"); await expect(page).toHaveURL("/appointments"); await expect(page.getByRole("heading", { name: "My Appointments" })).toBeVisible(); }); test("regular user sees Appointments link in navigation", async ({ page }) => { await page.goto("/"); await expect(page.getByRole("link", { name: "Appointments" })).toBeVisible(); }); test("shows empty state when no appointments", async ({ page }) => { await page.goto("/appointments"); await expect(page.getByText("don't have any appointments")).toBeVisible(); await expect(page.getByRole("link", { name: "Book an appointment" })).toBeVisible(); }); }); test.describe("Appointments Page - With Bookings", () => { test("shows user's appointments", async ({ page }) => { // Create a booking first await createTestBooking(page); // Go to appointments page await page.goto("/appointments"); // Should see the appointment await expect(page.getByText("Test appointment")).toBeVisible(); await expect(page.getByText("Booked", { exact: true })).toBeVisible(); }); test("can cancel an appointment", async ({ page }) => { // Create a booking await createTestBooking(page); // Go to appointments page await page.goto("/appointments"); // Click cancel button await page.getByRole("button", { name: "Cancel" }).first().click(); // Confirm cancellation await page.getByRole("button", { name: "Confirm" }).click(); // Should show cancelled status await expect(page.getByText("Cancelled by you")).toBeVisible(); }); test("can abort cancellation", async ({ page }) => { // Create a booking await createTestBooking(page); // Go to appointments page await page.goto("/appointments"); // Wait for appointments to load await expect(page.getByRole("heading", { name: /Upcoming/ })).toBeVisible({ timeout: 10000 }); // Click cancel button await page.getByRole("button", { name: "Cancel" }).first().click(); // Click No to abort await page.getByRole("button", { name: "No" }).click(); // Should still show as booked await expect(page.getByText("Booked", { exact: true })).toBeVisible(); }); }); test.describe("Appointments Page - Access Control", () => { test("admin cannot access appointments page", async ({ page }) => { await clearAuth(page); await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); await page.goto("/appointments"); // Should be redirected await expect(page).not.toHaveURL("/appointments"); }); test("admin does not see Appointments link", async ({ page }) => { await clearAuth(page); await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); await page.goto("/audit"); await expect(page.getByRole("link", { name: "Appointments" })).not.toBeVisible(); }); test("unauthenticated user redirected to login", async ({ page }) => { await clearAuth(page); await page.goto("/appointments"); await expect(page).toHaveURL("/login"); }); }); test.describe("Appointments API", () => { test("regular user can view appointments via API", async ({ page }) => { await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); const cookies = await page.context().cookies(); const authCookie = cookies.find(c => c.name === "auth_token"); if (authCookie) { const response = await page.request.get(`${API_URL}/api/appointments`, { headers: { Cookie: `auth_token=${authCookie.value}`, }, }); expect(response.status()).toBe(200); expect(Array.isArray(await response.json())).toBe(true); } }); test("regular user can cancel appointment via API", async ({ page }) => { // Create a booking const booking = await createTestBooking(page); const cookies = await page.context().cookies(); const authCookie = cookies.find(c => c.name === "auth_token"); if (authCookie && booking && booking.id) { const response = await page.request.post( `${API_URL}/api/appointments/${booking.id}/cancel`, { headers: { Cookie: `auth_token=${authCookie.value}`, "Content-Type": "application/json", }, data: {}, } ); expect(response.status()).toBe(200); const data = await response.json(); expect(data.status).toBe("cancelled_by_user"); } }); test("admin cannot view user appointments via API", async ({ page }) => { await clearAuth(page); await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); const cookies = await page.context().cookies(); const authCookie = cookies.find(c => c.name === "auth_token"); if (authCookie) { const response = await page.request.get(`${API_URL}/api/appointments`, { headers: { Cookie: `auth_token=${authCookie.value}`, }, }); expect(response.status()).toBe(403); } }); });