arbret/frontend/e2e/appointments.spec.ts

266 lines
8.2 KiB
TypeScript
Raw Normal View History

import { test, expect, Page } from "@playwright/test";
import { formatDateLocal, getTomorrowDateStr } from "./helpers/date";
/**
* 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 });
}
// 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 (use first() since there may be multiple bookings)
await expect(page.getByText("Booked", { exact: true }).first()).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);
}
});
});