Phase 2: Admin availability UI with calendar grid, edit modal, and e2e tests
This commit is contained in:
parent
64d2e99d73
commit
f6cf093cb1
5 changed files with 1130 additions and 1 deletions
254
frontend/e2e/availability.spec.ts
Normal file
254
frontend/e2e/availability.spec.ts
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
import { test, expect, Page } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* 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");
|
||||
|
||||
// Click on the first day
|
||||
const tomorrowText = getTomorrowDisplay();
|
||||
await page.getByText(tomorrowText).click();
|
||||
|
||||
// Wait for modal
|
||||
await expect(page.getByRole("heading", { name: /Edit Availability/ })).toBeVisible();
|
||||
|
||||
// Click Save (default is 09:00-17:00)
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
|
||||
// Wait for modal to close
|
||||
await expect(page.getByRole("heading", { name: /Edit Availability/ })).not.toBeVisible();
|
||||
|
||||
// Should now show the slot
|
||||
await expect(page.getByText("09:00 - 17:00")).toBeVisible();
|
||||
});
|
||||
|
||||
test("can clear availability", async ({ page }) => {
|
||||
await page.goto("/admin/availability");
|
||||
|
||||
const tomorrowText = getTomorrowDisplay();
|
||||
|
||||
// First add availability
|
||||
await page.getByText(tomorrowText).click();
|
||||
await expect(page.getByRole("heading", { name: /Edit Availability/ })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expect(page.getByRole("heading", { name: /Edit Availability/ })).not.toBeVisible();
|
||||
|
||||
// Verify slot exists
|
||||
await expect(page.getByText("09:00 - 17:00")).toBeVisible();
|
||||
|
||||
// Now clear it
|
||||
await page.getByText(tomorrowText).click();
|
||||
await expect(page.getByRole("heading", { name: /Edit Availability/ })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Clear All" }).click();
|
||||
|
||||
// Wait for modal to close
|
||||
await expect(page.getByRole("heading", { name: /Edit Availability/ })).not.toBeVisible();
|
||||
|
||||
// Slot should be gone - verify by checking the time slot is no longer visible
|
||||
await expect(page.getByText("09:00 - 17:00")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("can add multiple slots", async ({ page }) => {
|
||||
await page.goto("/admin/availability");
|
||||
|
||||
const tomorrowText = getTomorrowDisplay();
|
||||
await page.getByText(tomorrowText).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
|
||||
|
||||
// Save
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expect(page.getByRole("heading", { name: /Edit Availability/ })).not.toBeVisible();
|
||||
|
||||
// Should see both slots
|
||||
await expect(page.getByText("09:00 - 12:00")).toBeVisible();
|
||||
await expect(page.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 tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const dateStr = tomorrow.toISOString().split("T")[0];
|
||||
|
||||
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 tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const dateStr = tomorrow.toISOString().split("T")[0];
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue