arbret/frontend/e2e/availability.spec.ts

312 lines
12 KiB
TypeScript
Raw Normal View History

import { test, expect, Page } from "@playwright/test";
import { formatDateLocal, getTomorrowDateStr } from "./helpers/date";
/**
* Availability Page E2E Tests
*
* Tests for the admin availability management page.
*/
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 get tomorrow's date in the format displayed on the page
function getTomorrowDisplay(): string {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
return tomorrow.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
}
test.describe("Availability Page - Admin Access", () => {
test.beforeEach(async ({ page }) => {
await clearAuth(page);
await loginUser(page, ADMIN_USER.email, ADMIN_USER.password);
});
test("admin can access availability page", async ({ page }) => {
await page.goto("/admin/availability");
await expect(page).toHaveURL("/admin/availability");
await expect(page.getByRole("heading", { name: "Availability" })).toBeVisible();
await expect(page.getByText("Configure your available time slots")).toBeVisible();
});
test("admin sees Availability link in nav", async ({ page }) => {
await page.goto("/audit");
const availabilityLink = page.locator('a[href="/admin/availability"]');
await expect(availabilityLink).toBeVisible();
});
test("availability page shows calendar grid", async ({ page }) => {
await page.goto("/admin/availability");
// Should show tomorrow's date in the calendar
const tomorrowText = getTomorrowDisplay();
await expect(page.getByText(tomorrowText)).toBeVisible();
// Should show "No availability" for days without slots
await expect(page.getByText("No availability").first()).toBeVisible();
});
test("can open edit modal by clicking a day", async ({ page }) => {
await page.goto("/admin/availability");
// Click on the first day card
const tomorrowText = getTomorrowDisplay();
await page.getByText(tomorrowText).click();
// Modal should appear
await expect(page.getByRole("heading", { name: /Edit Availability/ })).toBeVisible();
await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
});
test("can add availability slot", async ({ page }) => {
await page.goto("/admin/availability");
// Wait for initial data load to complete
await page.waitForLoadState("networkidle");
// Find a day card with "No availability" and click on it
// This ensures we're clicking on a day without existing slots
const dayCardWithNoAvailability = page.locator('[data-testid^="day-card-"]').filter({
has: page.getByText("No availability")
}).first();
await dayCardWithNoAvailability.click();
// Wait for modal
await expect(page.getByRole("heading", { name: /Edit Availability/ })).toBeVisible();
// Set up listeners for both PUT and GET before clicking Save to avoid race condition
const putPromise = page.waitForResponse(resp =>
resp.url().includes("/api/admin/availability") && resp.request().method() === "PUT"
);
const getPromise = page.waitForResponse(resp =>
resp.url().includes("/api/admin/availability") && resp.request().method() === "GET"
);
await page.getByRole("button", { name: "Save" }).click();
await putPromise;
await getPromise;
// Wait for modal to close
await expect(page.getByRole("heading", { name: /Edit Availability/ })).not.toBeVisible();
// Should now show the slot (the card we clicked should now have this slot)
await expect(page.getByText("09:00 - 17:00")).toBeVisible();
});
test("can clear availability", async ({ page }) => {
await page.goto("/admin/availability");
// Wait for initial data load to complete
await page.waitForLoadState("networkidle");
// Find a day card with "No availability" and click on it
const dayCardWithNoAvailability = page.locator('[data-testid^="day-card-"]').filter({
has: page.getByText("No availability")
}).first();
// Get the testid so we can find the same card later
const testId = await dayCardWithNoAvailability.getAttribute('data-testid');
const targetCard = page.locator(`[data-testid="${testId}"]`);
// First add availability
await dayCardWithNoAvailability.click();
await expect(page.getByRole("heading", { name: /Edit Availability/ })).toBeVisible();
// Set up listeners for both PUT and GET before clicking Save to avoid race condition
const savePutPromise = page.waitForResponse(resp =>
resp.url().includes("/api/admin/availability") && resp.request().method() === "PUT"
);
const saveGetPromise = page.waitForResponse(resp =>
resp.url().includes("/api/admin/availability") && resp.request().method() === "GET"
);
await page.getByRole("button", { name: "Save" }).click();
await savePutPromise;
await saveGetPromise;
await expect(page.getByRole("heading", { name: /Edit Availability/ })).not.toBeVisible();
// Verify slot exists in the specific card we clicked
await expect(targetCard.getByText("09:00 - 17:00")).toBeVisible();
// Now clear it - click on the same card using the testid
await targetCard.click();
await expect(page.getByRole("heading", { name: /Edit Availability/ })).toBeVisible();
// Set up listeners for both PUT and GET before clicking Clear to avoid race condition
const clearPutPromise = page.waitForResponse(resp =>
resp.url().includes("/api/admin/availability") && resp.request().method() === "PUT"
);
const clearGetPromise = page.waitForResponse(resp =>
resp.url().includes("/api/admin/availability") && resp.request().method() === "GET"
);
await page.getByRole("button", { name: "Clear All" }).click();
await clearPutPromise;
await clearGetPromise;
// Wait for modal to close
await expect(page.getByRole("heading", { name: /Edit Availability/ })).not.toBeVisible();
// Slot should be gone from this specific card
await expect(targetCard.getByText("09:00 - 17:00")).not.toBeVisible();
});
test("can add multiple slots", async ({ page }) => {
await page.goto("/admin/availability");
// Wait for initial data load to complete
await page.waitForLoadState("networkidle");
// Find a day card with "No availability" and click on it (to avoid conflicts with booking tests)
const dayCardWithNoAvailability = page.locator('[data-testid^="day-card-"]').filter({
has: page.getByText("No availability")
}).first();
const testId = await dayCardWithNoAvailability.getAttribute('data-testid');
const targetCard = page.locator(`[data-testid="${testId}"]`);
await dayCardWithNoAvailability.click();
await expect(page.getByRole("heading", { name: /Edit Availability/ })).toBeVisible();
// First slot is 09:00-17:00 by default - change it to morning only
const timeSelects = page.locator("select");
await timeSelects.nth(1).selectOption("12:00"); // Change first slot end to 12:00
// Add another slot for afternoon
await page.getByText("+ Add Time Range").click();
// Change second slot times to avoid overlap
await timeSelects.nth(2).selectOption("14:00"); // Second slot start
await timeSelects.nth(3).selectOption("17:00"); // Second slot end
// Set up listeners for both PUT and GET before clicking Save to avoid race condition
const putPromise = page.waitForResponse(resp =>
resp.url().includes("/api/admin/availability") && resp.request().method() === "PUT"
);
const getPromise = page.waitForResponse(resp =>
resp.url().includes("/api/admin/availability") && resp.request().method() === "GET"
);
await page.getByRole("button", { name: "Save" }).click();
await putPromise;
await getPromise;
await expect(page.getByRole("heading", { name: /Edit Availability/ })).not.toBeVisible();
// Should see both slots in the card we clicked
await expect(targetCard.getByText("09:00 - 12:00")).toBeVisible();
await expect(targetCard.getByText("14:00 - 17:00")).toBeVisible();
});
});
test.describe("Availability Page - Access Control", () => {
test("regular user cannot access availability page", async ({ page }) => {
await clearAuth(page);
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
await page.goto("/admin/availability");
// Should be redirected (to counter/home for regular users)
await expect(page).not.toHaveURL("/admin/availability");
});
test("regular user does not see Availability link", async ({ page }) => {
await clearAuth(page);
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
await page.goto("/");
const availabilityLink = page.locator('a[href="/admin/availability"]');
await expect(availabilityLink).toHaveCount(0);
});
test("unauthenticated user redirected to login", async ({ page }) => {
await clearAuth(page);
await page.goto("/admin/availability");
await expect(page).toHaveURL("/login");
});
});
test.describe("Availability API", () => {
test("admin can set availability via API", async ({ page, request }) => {
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 dateStr = getTomorrowDateStr();
const response = await request.put(`${API_URL}/api/admin/availability`, {
headers: {
Cookie: `auth_token=${authCookie.value}`,
"Content-Type": "application/json",
},
data: {
date: dateStr,
slots: [{ start_time: "10:00:00", end_time: "12:00:00" }],
},
});
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.date).toBe(dateStr);
expect(data.slots).toHaveLength(1);
}
});
test("regular user cannot access availability API", async ({ page, request }) => {
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 dateStr = getTomorrowDateStr();
const response = await request.get(
`${API_URL}/api/admin/availability?from=${dateStr}&to=${dateStr}`,
{
headers: {
Cookie: `auth_token=${authCookie.value}`,
},
}
);
expect(response.status()).toBe(403);
}
});
});