Add Prettier for TypeScript formatting
- Install prettier - Configure .prettierrc.json and .prettierignore - Add npm scripts: format, format:check - Add Makefile target: format-frontend - Format all frontend files
This commit is contained in:
parent
4b394b0698
commit
37de6f70e0
44 changed files with 906 additions and 856 deletions
|
|
@ -29,21 +29,21 @@ test.describe("Admin Invites Page", () => {
|
|||
|
||||
test("godfather selection is a dropdown with users, not a number input", async ({ page }) => {
|
||||
await page.goto("/admin/invites");
|
||||
|
||||
|
||||
// The godfather selector should be a <select> element, not an <input type="number">
|
||||
const selectElement = page.locator("select").first();
|
||||
await expect(selectElement).toBeVisible();
|
||||
|
||||
|
||||
// Wait for users to load by checking for a known user in the dropdown
|
||||
await expect(selectElement).toContainText(REGULAR_USER_EMAIL);
|
||||
|
||||
|
||||
// Verify it has user options (at least the seeded users)
|
||||
const options = selectElement.locator("option");
|
||||
const optionCount = await options.count();
|
||||
|
||||
|
||||
// Should have at least 2 options: placeholder + at least one user
|
||||
expect(optionCount).toBeGreaterThanOrEqual(2);
|
||||
|
||||
|
||||
// There should NOT be a number input for godfather ID
|
||||
const numberInput = page.locator('input[type="number"]');
|
||||
await expect(numberInput).toHaveCount(0);
|
||||
|
|
@ -51,20 +51,20 @@ test.describe("Admin Invites Page", () => {
|
|||
|
||||
test("can create invite by selecting user from dropdown", async ({ page }) => {
|
||||
await page.goto("/admin/invites");
|
||||
|
||||
|
||||
// Wait for page to load
|
||||
await page.waitForSelector("select");
|
||||
|
||||
|
||||
// Select the regular user as godfather
|
||||
const godfatherSelect = page.locator("select").first();
|
||||
await godfatherSelect.selectOption({ label: REGULAR_USER_EMAIL });
|
||||
|
||||
|
||||
// Click create invite
|
||||
await page.click('button:has-text("Create Invite")');
|
||||
|
||||
|
||||
// Wait for the invite to appear in the table
|
||||
await expect(page.locator("table")).toContainText(REGULAR_USER_EMAIL);
|
||||
|
||||
|
||||
// Verify an invite code appears (format: word-word-NN)
|
||||
const inviteCodeCell = page.locator("td").first();
|
||||
await expect(inviteCodeCell).toHaveText(/^[a-z]+-[a-z]+-\d{2}$/);
|
||||
|
|
@ -72,18 +72,18 @@ test.describe("Admin Invites Page", () => {
|
|||
|
||||
test("create button is disabled when no user selected", async ({ page }) => {
|
||||
await page.goto("/admin/invites");
|
||||
|
||||
|
||||
// Wait for page to load
|
||||
await page.waitForSelector("select");
|
||||
|
||||
|
||||
// The create button should be disabled initially (no user selected)
|
||||
const createButton = page.locator('button:has-text("Create Invite")');
|
||||
await expect(createButton).toBeDisabled();
|
||||
|
||||
|
||||
// Select a user
|
||||
const godfatherSelect = page.locator("select").first();
|
||||
await godfatherSelect.selectOption({ label: REGULAR_USER_EMAIL });
|
||||
|
||||
|
||||
// Now the button should be enabled
|
||||
await expect(createButton).toBeEnabled();
|
||||
});
|
||||
|
|
@ -91,23 +91,27 @@ test.describe("Admin Invites Page", () => {
|
|||
test("can revoke a ready invite", async ({ page }) => {
|
||||
await page.goto("/admin/invites");
|
||||
await page.waitForSelector("select");
|
||||
|
||||
|
||||
// Create an invite first
|
||||
const godfatherSelect = page.locator("select").first();
|
||||
await godfatherSelect.selectOption({ label: REGULAR_USER_EMAIL });
|
||||
await page.click('button:has-text("Create Invite")');
|
||||
|
||||
|
||||
// Wait for the new invite to appear and capture its code
|
||||
// The new invite should be the first row with godfather = REGULAR_USER_EMAIL and status = ready
|
||||
const newInviteRow = page.locator("tr").filter({ hasText: REGULAR_USER_EMAIL }).filter({ hasText: "ready" }).first();
|
||||
const newInviteRow = page
|
||||
.locator("tr")
|
||||
.filter({ hasText: REGULAR_USER_EMAIL })
|
||||
.filter({ hasText: "ready" })
|
||||
.first();
|
||||
await expect(newInviteRow).toBeVisible();
|
||||
|
||||
|
||||
// Get the invite code from this row (first cell)
|
||||
const inviteCode = await newInviteRow.locator("td").first().textContent();
|
||||
|
||||
|
||||
// Click revoke on this specific row
|
||||
await newInviteRow.locator('button:has-text("Revoke")').click();
|
||||
|
||||
|
||||
// Verify this specific invite now shows "revoked"
|
||||
const revokedRow = page.locator("tr").filter({ hasText: inviteCode! });
|
||||
await expect(revokedRow).toContainText("revoked");
|
||||
|
|
@ -116,20 +120,20 @@ test.describe("Admin Invites Page", () => {
|
|||
test("status filter works", async ({ page }) => {
|
||||
await page.goto("/admin/invites");
|
||||
await page.waitForSelector("select");
|
||||
|
||||
|
||||
// Create an invite
|
||||
const godfatherSelect = page.locator("select").first();
|
||||
await godfatherSelect.selectOption({ label: REGULAR_USER_EMAIL });
|
||||
await page.click('button:has-text("Create Invite")');
|
||||
await expect(page.locator("table")).toContainText("ready");
|
||||
|
||||
|
||||
// Filter by "revoked" status - should show no ready invites
|
||||
const statusFilter = page.locator("select").nth(1); // Second select is the status filter
|
||||
await statusFilter.selectOption("revoked");
|
||||
|
||||
|
||||
// Wait for the filter to apply
|
||||
await page.waitForResponse((resp) => resp.url().includes("status=revoked"));
|
||||
|
||||
|
||||
// Filter by "ready" status - should show our invite
|
||||
await statusFilter.selectOption("ready");
|
||||
await page.waitForResponse((resp) => resp.url().includes("status=ready"));
|
||||
|
|
@ -145,10 +149,10 @@ test.describe("Admin Invites Access Control", () => {
|
|||
await page.fill('input[type="password"]', "user123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL("/");
|
||||
|
||||
|
||||
// Try to access admin invites page
|
||||
await page.goto("/admin/invites");
|
||||
|
||||
|
||||
// Should be redirected away (to home page based on fallbackRedirect)
|
||||
await expect(page).not.toHaveURL("/admin/invites");
|
||||
});
|
||||
|
|
@ -156,9 +160,8 @@ test.describe("Admin Invites Access Control", () => {
|
|||
test("unauthenticated user cannot access admin invites page", async ({ page }) => {
|
||||
await page.context().clearCookies();
|
||||
await page.goto("/admin/invites");
|
||||
|
||||
|
||||
// Should be redirected to login
|
||||
await expect(page).toHaveURL("/login");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,23 +4,23 @@ import { API_URL, REGULAR_USER, ADMIN_USER, clearAuth, loginUser } from "./helpe
|
|||
|
||||
/**
|
||||
* Appointments Page E2E Tests
|
||||
*
|
||||
*
|
||||
* Tests for viewing and cancelling user appointments.
|
||||
*/
|
||||
|
||||
// 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");
|
||||
|
||||
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}`,
|
||||
|
|
@ -31,22 +31,22 @@ async function createTestBooking(page: Page) {
|
|||
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");
|
||||
|
||||
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 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}`,
|
||||
|
|
@ -57,7 +57,7 @@ async function createTestBooking(page: Page) {
|
|||
note: "Test appointment",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
|
|
@ -69,20 +69,20 @@ test.describe("Appointments Page - Regular User Access", () => {
|
|||
|
||||
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();
|
||||
});
|
||||
|
|
@ -92,10 +92,10 @@ 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();
|
||||
|
|
@ -104,16 +104,16 @@ test.describe("Appointments Page - With Bookings", () => {
|
|||
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();
|
||||
});
|
||||
|
|
@ -121,19 +121,19 @@ test.describe("Appointments Page - With Bookings", () => {
|
|||
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();
|
||||
});
|
||||
|
|
@ -143,9 +143,9 @@ 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");
|
||||
});
|
||||
|
|
@ -153,17 +153,17 @@ test.describe("Appointments Page - Access Control", () => {
|
|||
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");
|
||||
});
|
||||
});
|
||||
|
|
@ -172,17 +172,17 @@ 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");
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
@ -191,22 +191,19 @@ test.describe("Appointments API", () => {
|
|||
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");
|
||||
|
||||
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: {},
|
||||
}
|
||||
);
|
||||
|
||||
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");
|
||||
|
|
@ -216,19 +213,18 @@ test.describe("Appointments API", () => {
|
|||
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");
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ async function createInvite(request: APIRequestContext): Promise<string> {
|
|||
data: { email: ADMIN_EMAIL, password: ADMIN_PASSWORD },
|
||||
});
|
||||
const cookies = loginResp.headers()["set-cookie"];
|
||||
|
||||
|
||||
// Get admin user ID (we'll use admin as godfather for simplicity)
|
||||
const meResp = await request.get(`${API_BASE}/api/auth/me`, {
|
||||
headers: { Cookie: cookies },
|
||||
});
|
||||
const admin = await meResp.json();
|
||||
|
||||
|
||||
// Create invite
|
||||
const inviteResp = await request.post(`${API_BASE}/api/admin/invites`, {
|
||||
data: { godfather_id: admin.id },
|
||||
|
|
@ -61,7 +61,7 @@ test.describe("Authentication Flow", () => {
|
|||
test("signup page has invite code form", async ({ page }) => {
|
||||
await page.goto("/signup");
|
||||
await expect(page.locator("h1")).toHaveText("Join with Invite");
|
||||
await expect(page.locator('input#inviteCode')).toBeVisible();
|
||||
await expect(page.locator("input#inviteCode")).toBeVisible();
|
||||
await expect(page.locator('button[type="submit"]')).toHaveText("Continue");
|
||||
await expect(page.locator('a[href="/login"]')).toBeVisible();
|
||||
});
|
||||
|
|
@ -80,19 +80,22 @@ test.describe("Authentication Flow", () => {
|
|||
});
|
||||
|
||||
test.describe("Logged-in User Visiting Invite URL", () => {
|
||||
test("redirects to home when logged-in user visits direct invite URL", async ({ page, request }) => {
|
||||
test("redirects to home when logged-in user visits direct invite URL", async ({
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
const email = uniqueEmail();
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
// First sign up to create a user
|
||||
await page.goto("/signup");
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
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.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("/");
|
||||
|
||||
|
|
@ -110,13 +113,13 @@ test.describe("Logged-in User Visiting Invite URL", () => {
|
|||
|
||||
// Sign up and stay logged in
|
||||
await page.goto("/signup");
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
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.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("/");
|
||||
|
||||
|
|
@ -136,18 +139,18 @@ test.describe("Signup with Invite", () => {
|
|||
const inviteCode = await createInvite(request);
|
||||
|
||||
await page.goto("/signup");
|
||||
|
||||
|
||||
// Step 1: Enter invite code
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
await page.fill("input#inviteCode", inviteCode);
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
|
||||
// Wait for form to transition to registration form
|
||||
await expect(page.locator("h1")).toHaveText("Create account");
|
||||
|
||||
|
||||
// Step 2: Fill registration form
|
||||
await page.fill('input#email', email);
|
||||
await page.fill('input#password', "password123");
|
||||
await page.fill('input#confirmPassword', "password123");
|
||||
await page.fill("input#email", email);
|
||||
await page.fill("input#password", "password123");
|
||||
await page.fill("input#confirmPassword", "password123");
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Should redirect to home after signup
|
||||
|
|
@ -162,17 +165,17 @@ test.describe("Signup with Invite", () => {
|
|||
|
||||
// Use direct URL with code
|
||||
await page.goto(`/signup/${inviteCode}`);
|
||||
|
||||
|
||||
// Should redirect to signup with code in query and validate
|
||||
await page.waitForURL(/\/signup\?code=/);
|
||||
|
||||
|
||||
// Wait for form to transition to registration form
|
||||
await expect(page.locator("h1")).toHaveText("Create account");
|
||||
|
||||
|
||||
// Fill registration form
|
||||
await page.fill('input#email', email);
|
||||
await page.fill('input#password', "password123");
|
||||
await page.fill('input#confirmPassword', "password123");
|
||||
await page.fill("input#email", email);
|
||||
await page.fill("input#password", "password123");
|
||||
await page.fill("input#confirmPassword", "password123");
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Should redirect to home
|
||||
|
|
@ -181,7 +184,7 @@ test.describe("Signup with Invite", () => {
|
|||
|
||||
test("shows error for invalid invite code", async ({ page }) => {
|
||||
await page.goto("/signup");
|
||||
await page.fill('input#inviteCode', "fake-code-99");
|
||||
await page.fill("input#inviteCode", "fake-code-99");
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Should show error
|
||||
|
|
@ -192,14 +195,14 @@ test.describe("Signup with Invite", () => {
|
|||
const inviteCode = await createInvite(request);
|
||||
|
||||
await page.goto("/signup");
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
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', uniqueEmail());
|
||||
await page.fill('input#password', "password123");
|
||||
await page.fill('input#confirmPassword', "differentpassword");
|
||||
|
||||
await page.fill("input#email", uniqueEmail());
|
||||
await page.fill("input#password", "password123");
|
||||
await page.fill("input#confirmPassword", "differentpassword");
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page.getByText("Passwords do not match")).toBeVisible();
|
||||
|
|
@ -209,14 +212,14 @@ test.describe("Signup with Invite", () => {
|
|||
const inviteCode = await createInvite(request);
|
||||
|
||||
await page.goto("/signup");
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
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', uniqueEmail());
|
||||
await page.fill('input#password', "short");
|
||||
await page.fill('input#confirmPassword', "short");
|
||||
|
||||
await page.fill("input#email", uniqueEmail());
|
||||
await page.fill("input#password", "short");
|
||||
await page.fill("input#confirmPassword", "short");
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page.getByText("Password must be at least 6 characters")).toBeVisible();
|
||||
|
|
@ -231,7 +234,7 @@ test.describe("Login", () => {
|
|||
// Create a test user with invite
|
||||
testEmail = uniqueEmail();
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
|
||||
// Register the test user via backend API
|
||||
await request.post(`${API_BASE}/api/auth/register`, {
|
||||
data: {
|
||||
|
|
@ -292,13 +295,13 @@ test.describe("Logout", () => {
|
|||
|
||||
// Sign up
|
||||
await page.goto("/signup");
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
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.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("/");
|
||||
|
||||
|
|
@ -315,13 +318,13 @@ test.describe("Logout", () => {
|
|||
|
||||
// Sign up
|
||||
await page.goto("/signup");
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
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.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("/");
|
||||
|
||||
|
|
@ -342,13 +345,13 @@ test.describe("Session Persistence", () => {
|
|||
|
||||
// Sign up
|
||||
await page.goto("/signup");
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
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.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("/");
|
||||
await expect(page.getByText(email)).toBeVisible();
|
||||
|
|
@ -366,13 +369,13 @@ test.describe("Session Persistence", () => {
|
|||
const inviteCode = await createInvite(request);
|
||||
|
||||
await page.goto("/signup");
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
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.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("/");
|
||||
|
||||
|
|
@ -388,13 +391,13 @@ test.describe("Session Persistence", () => {
|
|||
const inviteCode = await createInvite(request);
|
||||
|
||||
await page.goto("/signup");
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
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.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("/");
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { API_URL, REGULAR_USER, ADMIN_USER, clearAuth, loginUser } from "./helpe
|
|||
|
||||
/**
|
||||
* Availability Page E2E Tests
|
||||
*
|
||||
*
|
||||
* Tests for the admin availability management page.
|
||||
*/
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ test.describe("Availability Page - Admin Access", () => {
|
|||
|
||||
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();
|
||||
|
|
@ -31,29 +31,29 @@ test.describe("Availability Page - Admin Access", () => {
|
|||
|
||||
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();
|
||||
|
|
@ -62,133 +62,142 @@ test.describe("Availability Page - Admin Access", () => {
|
|||
|
||||
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();
|
||||
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 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"
|
||||
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();
|
||||
|
||||
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 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 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"
|
||||
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 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"
|
||||
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 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 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"
|
||||
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();
|
||||
|
|
@ -199,9 +208,9 @@ 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");
|
||||
});
|
||||
|
|
@ -209,18 +218,18 @@ test.describe("Availability Page - Access Control", () => {
|
|||
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");
|
||||
});
|
||||
});
|
||||
|
|
@ -229,13 +238,13 @@ 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");
|
||||
|
||||
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}`,
|
||||
|
|
@ -246,7 +255,7 @@ test.describe("Availability API", () => {
|
|||
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);
|
||||
|
|
@ -257,13 +266,13 @@ test.describe("Availability API", () => {
|
|||
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");
|
||||
|
||||
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}`,
|
||||
{
|
||||
|
|
@ -272,9 +281,8 @@ test.describe("Availability API", () => {
|
|||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
expect(response.status()).toBe(403);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,27 +4,27 @@ import { API_URL, REGULAR_USER, ADMIN_USER, clearAuth, loginUser } from "./helpe
|
|||
|
||||
/**
|
||||
* Booking Page E2E Tests
|
||||
*
|
||||
*
|
||||
* Tests for the user booking page.
|
||||
*/
|
||||
|
||||
// Set up availability for a date using the API with retry logic
|
||||
async function setAvailability(page: Page, dateStr: string, maxRetries = 3) {
|
||||
const cookies = await page.context().cookies();
|
||||
const authCookie = cookies.find(c => c.name === "auth_token");
|
||||
|
||||
const authCookie = cookies.find((c) => c.name === "auth_token");
|
||||
|
||||
if (!authCookie) {
|
||||
throw new Error("No auth cookie found when trying to set availability");
|
||||
}
|
||||
|
||||
|
||||
let lastError: Error | null = null;
|
||||
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
if (attempt > 0) {
|
||||
// Wait before retry
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
|
||||
const response = await page.request.put(`${API_URL}/api/admin/availability`, {
|
||||
headers: {
|
||||
Cookie: `auth_token=${authCookie.value}`,
|
||||
|
|
@ -35,20 +35,20 @@ async function setAvailability(page: Page, dateStr: string, maxRetries = 3) {
|
|||
slots: [{ start_time: "09:00:00", end_time: "12:00:00" }],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (response.ok()) {
|
||||
return; // Success
|
||||
}
|
||||
|
||||
|
||||
const body = await response.text();
|
||||
lastError = new Error(`Failed to set availability: ${response.status()} - ${body}`);
|
||||
|
||||
|
||||
// Only retry on 500 errors
|
||||
if (response.status() !== 500) {
|
||||
throw lastError;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
|
|
@ -60,23 +60,25 @@ test.describe("Booking Page - Regular User Access", () => {
|
|||
|
||||
test("regular user can access booking page", async ({ page }) => {
|
||||
await page.goto("/booking");
|
||||
|
||||
|
||||
await expect(page).toHaveURL("/booking");
|
||||
await expect(page.getByRole("heading", { name: "Book an Appointment" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("regular user sees Book link in navigation", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
|
||||
await expect(page.getByRole("link", { name: "Book" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("booking page shows date selection", async ({ page }) => {
|
||||
await page.goto("/booking");
|
||||
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Select a Date" })).toBeVisible();
|
||||
// Should see multiple date buttons
|
||||
const dateButtons = page.locator("button").filter({ hasText: /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)/ });
|
||||
const dateButtons = page
|
||||
.locator("button")
|
||||
.filter({ hasText: /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)/ });
|
||||
await expect(dateButtons.first()).toBeVisible();
|
||||
});
|
||||
|
||||
|
|
@ -87,14 +89,16 @@ test.describe("Booking Page - Regular User Access", () => {
|
|||
await setAvailability(page, getTomorrowDateStr());
|
||||
await clearAuth(page);
|
||||
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
|
||||
|
||||
|
||||
await page.goto("/booking");
|
||||
|
||||
|
||||
// Wait for availability check to complete
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
|
||||
// Find an enabled date button (one with availability)
|
||||
const dateButtons = page.locator("button").filter({ hasText: /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)/ });
|
||||
const dateButtons = page
|
||||
.locator("button")
|
||||
.filter({ hasText: /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)/ });
|
||||
let enabledButton = null;
|
||||
const buttonCount = await dateButtons.count();
|
||||
for (let i = 0; i < buttonCount; i++) {
|
||||
|
|
@ -105,42 +109,44 @@ test.describe("Booking Page - Regular User Access", () => {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Should have at least one enabled date (tomorrow)
|
||||
expect(enabledButton).not.toBeNull();
|
||||
await enabledButton!.click();
|
||||
|
||||
|
||||
// Should show Available Slots section (use heading to be specific)
|
||||
await expect(page.getByRole("heading", { name: /Available Slots for/ })).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows no slots or message when no availability", async ({ page }) => {
|
||||
await page.goto("/booking");
|
||||
|
||||
|
||||
// Wait for date buttons to load and availability check to complete
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
|
||||
// Find an enabled date button (one that has availability or is still loading)
|
||||
// If all dates are disabled, we can't test clicking, so verify disabled state
|
||||
const dateButtons = page.locator("button").filter({ hasText: /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)/ });
|
||||
const dateButtons = page
|
||||
.locator("button")
|
||||
.filter({ hasText: /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)/ });
|
||||
const enabledButtons = dateButtons.filter({ hasNotText: /disabled/ });
|
||||
const enabledCount = await enabledButtons.count();
|
||||
|
||||
|
||||
if (enabledCount > 0) {
|
||||
// Click the first enabled date button
|
||||
await enabledButtons.first().click();
|
||||
|
||||
|
||||
// Wait for the section to appear
|
||||
await expect(page.getByRole("heading", { name: /Available Slots for/ })).toBeVisible();
|
||||
|
||||
|
||||
// Should either show no slots message OR show no slot buttons
|
||||
// Wait a moment for API to return
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
|
||||
// If no availability is set, we'll see the "No available slots" message
|
||||
const noSlotsMessage = page.getByText("No available slots for this date");
|
||||
const isNoSlotsVisible = await noSlotsMessage.isVisible().catch(() => false);
|
||||
|
||||
|
||||
if (!isNoSlotsVisible) {
|
||||
// There might be some slots from shared state - just verify the section loads
|
||||
await expect(page.getByRole("heading", { name: /Available Slots for/ })).toBeVisible();
|
||||
|
|
@ -166,22 +172,25 @@ test.describe("Booking Page - With Availability", () => {
|
|||
|
||||
test("shows available slots when availability is set", async ({ page }) => {
|
||||
await page.goto("/booking");
|
||||
|
||||
|
||||
// Get tomorrow's display name to click the correct button
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const weekday = tomorrow.toLocaleDateString("en-US", { weekday: "short" });
|
||||
|
||||
|
||||
// Click tomorrow's date using the weekday name
|
||||
const dateButton = page.locator("button").filter({ hasText: new RegExp(`^${weekday}`) }).first();
|
||||
const dateButton = page
|
||||
.locator("button")
|
||||
.filter({ hasText: new RegExp(`^${weekday}`) })
|
||||
.first();
|
||||
await dateButton.click();
|
||||
|
||||
|
||||
// Wait for "Available Slots" section to appear
|
||||
await expect(page.getByRole("heading", { name: /Available Slots for/ })).toBeVisible();
|
||||
|
||||
|
||||
// Wait for loading to finish (no "Loading slots..." text)
|
||||
await expect(page.getByText("Loading slots...")).not.toBeVisible({ timeout: 10000 });
|
||||
|
||||
|
||||
// Should see some slot buttons (look for any button with time-like pattern)
|
||||
// The format might be "09:00" or "9:00 AM" depending on locale
|
||||
const slotButtons = page.locator("button").filter({ hasText: /^\d{1,2}:\d{2}/ });
|
||||
|
|
@ -190,24 +199,27 @@ test.describe("Booking Page - With Availability", () => {
|
|||
|
||||
test("clicking slot shows confirmation form", async ({ page }) => {
|
||||
await page.goto("/booking");
|
||||
|
||||
|
||||
// Get tomorrow's display name
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const weekday = tomorrow.toLocaleDateString("en-US", { weekday: "short" });
|
||||
|
||||
|
||||
// Click tomorrow's date
|
||||
const dateButton = page.locator("button").filter({ hasText: new RegExp(`^${weekday}`) }).first();
|
||||
const dateButton = page
|
||||
.locator("button")
|
||||
.filter({ hasText: new RegExp(`^${weekday}`) })
|
||||
.first();
|
||||
await dateButton.click();
|
||||
|
||||
|
||||
// Wait for any slot to appear
|
||||
await expect(page.getByText("Loading slots...")).not.toBeVisible({ timeout: 10000 });
|
||||
const slotButtons = page.locator("button").filter({ hasText: /^\d{1,2}:\d{2}/ });
|
||||
await expect(slotButtons.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
|
||||
// Click first slot
|
||||
await slotButtons.first().click();
|
||||
|
||||
|
||||
// Should show confirmation form
|
||||
await expect(page.getByText("Confirm Booking")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Book Appointment" })).toBeVisible();
|
||||
|
|
@ -215,68 +227,76 @@ test.describe("Booking Page - With Availability", () => {
|
|||
|
||||
test("can book an appointment with note", async ({ page }) => {
|
||||
await page.goto("/booking");
|
||||
|
||||
|
||||
// Get tomorrow's display name
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const weekday = tomorrow.toLocaleDateString("en-US", { weekday: "short" });
|
||||
|
||||
|
||||
// Click tomorrow's date
|
||||
const dateButton = page.locator("button").filter({ hasText: new RegExp(`^${weekday}`) }).first();
|
||||
const dateButton = page
|
||||
.locator("button")
|
||||
.filter({ hasText: new RegExp(`^${weekday}`) })
|
||||
.first();
|
||||
await dateButton.click();
|
||||
|
||||
|
||||
// Wait for slots to load
|
||||
await expect(page.getByText("Loading slots...")).not.toBeVisible({ timeout: 10000 });
|
||||
const slotButtons = page.locator("button").filter({ hasText: /^\d{1,2}:\d{2}/ });
|
||||
await expect(slotButtons.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
|
||||
// Click second slot (to avoid booking same slot as other tests)
|
||||
await slotButtons.nth(1).click();
|
||||
|
||||
|
||||
// Add a note
|
||||
await page.fill("textarea", "Test booking note");
|
||||
|
||||
|
||||
// Book
|
||||
await page.getByRole("button", { name: "Book Appointment" }).click();
|
||||
|
||||
|
||||
// Should show success message
|
||||
await expect(page.getByText(/Appointment booked/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("booked slot disappears from available slots", async ({ page }) => {
|
||||
await page.goto("/booking");
|
||||
|
||||
|
||||
// Get tomorrow's display name
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const weekday = tomorrow.toLocaleDateString("en-US", { weekday: "short" });
|
||||
|
||||
|
||||
// Click tomorrow's date
|
||||
const dateButton = page.locator("button").filter({ hasText: new RegExp(`^${weekday}`) }).first();
|
||||
const dateButton = page
|
||||
.locator("button")
|
||||
.filter({ hasText: new RegExp(`^${weekday}`) })
|
||||
.first();
|
||||
await dateButton.click();
|
||||
|
||||
|
||||
// Wait for slots to load
|
||||
await expect(page.getByText("Loading slots...")).not.toBeVisible({ timeout: 10000 });
|
||||
const slotButtons = page.locator("button").filter({ hasText: /^\d{1,2}:\d{2}/ });
|
||||
await expect(slotButtons.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
|
||||
// Count initial slots
|
||||
const initialCount = await slotButtons.count();
|
||||
|
||||
|
||||
// Click any slot (3rd to avoid conflicts)
|
||||
const slotToBook = slotButtons.nth(2);
|
||||
const _slotText = await slotToBook.textContent();
|
||||
await slotToBook.click();
|
||||
|
||||
|
||||
// Book it
|
||||
await page.getByRole("button", { name: "Book Appointment" }).click();
|
||||
|
||||
|
||||
// Wait for booking form to disappear (indicates booking completed)
|
||||
await expect(page.getByRole("button", { name: "Book Appointment" })).not.toBeVisible({ timeout: 10000 });
|
||||
|
||||
await expect(page.getByRole("button", { name: "Book Appointment" })).not.toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Wait for success message
|
||||
await expect(page.getByText(/Appointment booked/)).toBeVisible();
|
||||
|
||||
|
||||
// Should have one less slot now
|
||||
const newCount = await slotButtons.count();
|
||||
expect(newCount).toBe(initialCount - 1);
|
||||
|
|
@ -287,9 +307,9 @@ test.describe("Booking Page - Access Control", () => {
|
|||
test("admin cannot access booking page", async ({ page }) => {
|
||||
await clearAuth(page);
|
||||
await loginUser(page, ADMIN_USER.email, ADMIN_USER.password);
|
||||
|
||||
|
||||
await page.goto("/booking");
|
||||
|
||||
|
||||
// Should be redirected away (to audit or home)
|
||||
await expect(page).not.toHaveURL("/booking");
|
||||
});
|
||||
|
|
@ -297,17 +317,17 @@ test.describe("Booking Page - Access Control", () => {
|
|||
test("admin does not see Book 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: "Book" })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("unauthenticated user redirected to login", async ({ page }) => {
|
||||
await clearAuth(page);
|
||||
|
||||
|
||||
await page.goto("/booking");
|
||||
|
||||
|
||||
await expect(page).toHaveURL("/login");
|
||||
});
|
||||
});
|
||||
|
|
@ -320,13 +340,13 @@ test.describe("Booking API", () => {
|
|||
const dateStr = getTomorrowDateStr();
|
||||
await setAvailability(page, dateStr);
|
||||
await clearAuth(page);
|
||||
|
||||
|
||||
// Login as regular user
|
||||
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
|
||||
|
||||
|
||||
const cookies = await page.context().cookies();
|
||||
const authCookie = cookies.find(c => c.name === "auth_token");
|
||||
|
||||
const authCookie = cookies.find((c) => c.name === "auth_token");
|
||||
|
||||
if (authCookie) {
|
||||
// Use 11:45 to avoid conflicts with other tests using 10:00
|
||||
const response = await request.post(`${API_URL}/api/booking`, {
|
||||
|
|
@ -339,7 +359,7 @@ test.describe("Booking API", () => {
|
|||
note: "API test booking",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
const data = await response.json();
|
||||
expect(data.note).toBe("API test booking");
|
||||
|
|
@ -352,10 +372,10 @@ test.describe("Booking API", () => {
|
|||
await loginUser(page, ADMIN_USER.email, ADMIN_USER.password);
|
||||
const dateStr = getTomorrowDateStr();
|
||||
await setAvailability(page, dateStr);
|
||||
|
||||
|
||||
const cookies = await page.context().cookies();
|
||||
const authCookie = cookies.find(c => c.name === "auth_token");
|
||||
|
||||
const authCookie = cookies.find((c) => c.name === "auth_token");
|
||||
|
||||
if (authCookie) {
|
||||
const response = await request.post(`${API_URL}/api/booking`, {
|
||||
headers: {
|
||||
|
|
@ -366,9 +386,8 @@ test.describe("Booking API", () => {
|
|||
slot_start: `${dateStr}T10:15:00Z`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
expect(response.status()).toBe(403);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ async function createInvite(request: APIRequestContext): Promise<string> {
|
|||
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 },
|
||||
|
|
@ -33,28 +33,28 @@ async function createInvite(request: APIRequestContext): Promise<string> {
|
|||
async function authenticate(page: Page, request: APIRequestContext): Promise<string> {
|
||||
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);
|
||||
|
||||
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.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;
|
||||
}
|
||||
|
|
@ -87,19 +87,19 @@ test.describe("Counter - Authenticated", () => {
|
|||
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);
|
||||
|
|
@ -177,13 +177,13 @@ test.describe("Counter - Session Integration", () => {
|
|||
|
||||
// Sign up with invite
|
||||
await page.goto("/signup");
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
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.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("/");
|
||||
|
||||
|
|
|
|||
|
|
@ -37,4 +37,3 @@ export async function loginUser(page: Page, email: string, password: string) {
|
|||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL((url) => !url.pathname.includes("/login"), { timeout: 10000 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,4 +20,3 @@ export function getTomorrowDateStr(): string {
|
|||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
return formatDateLocal(tomorrow);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { test, expect, Page } from "@playwright/test";
|
|||
|
||||
/**
|
||||
* Permission-based E2E tests
|
||||
*
|
||||
*
|
||||
* These tests verify that:
|
||||
* 1. Regular users can only access Counter and Sum pages
|
||||
* 2. Admin users can only access the Audit page
|
||||
|
|
@ -18,7 +18,9 @@ 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. Run 'source .env' or set it in your environment.`);
|
||||
throw new Error(
|
||||
`Required environment variable ${name} is not set. Run 'source .env' or set it in your environment.`
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
|
@ -64,10 +66,10 @@ test.describe("Regular User Access", () => {
|
|||
|
||||
test("can access counter page", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
|
||||
// Should stay on counter page
|
||||
await expect(page).toHaveURL("/");
|
||||
|
||||
|
||||
// Should see counter UI
|
||||
await expect(page.getByText("Current Count")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /increment/i })).toBeVisible();
|
||||
|
|
@ -75,28 +77,28 @@ test.describe("Regular User Access", () => {
|
|||
|
||||
test("can access sum page", async ({ page }) => {
|
||||
await page.goto("/sum");
|
||||
|
||||
|
||||
// Should stay on sum page
|
||||
await expect(page).toHaveURL("/sum");
|
||||
|
||||
|
||||
// Should see sum UI
|
||||
await expect(page.getByText("Sum Calculator")).toBeVisible();
|
||||
});
|
||||
|
||||
test("cannot access audit page - redirected to counter", async ({ page }) => {
|
||||
await page.goto("/audit");
|
||||
|
||||
|
||||
// Should be redirected to counter page (home)
|
||||
await expect(page).toHaveURL("/");
|
||||
});
|
||||
|
||||
test("navigation only shows Counter and Sum", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
|
||||
// Should see Counter and Sum in nav
|
||||
await expect(page.getByText("Counter")).toBeVisible();
|
||||
await expect(page.getByText("Sum")).toBeVisible();
|
||||
|
||||
|
||||
// Should NOT see Audit in nav (for regular users)
|
||||
const auditLinks = page.locator('a[href="/audit"]');
|
||||
await expect(auditLinks).toHaveCount(0);
|
||||
|
|
@ -104,11 +106,11 @@ test.describe("Regular User Access", () => {
|
|||
|
||||
test("can navigate between Counter and Sum", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
|
||||
// Go to Sum
|
||||
await page.click('a[href="/sum"]');
|
||||
await expect(page).toHaveURL("/sum");
|
||||
|
||||
|
||||
// Go back to Counter
|
||||
await page.click('a[href="/"]');
|
||||
await expect(page).toHaveURL("/");
|
||||
|
|
@ -116,31 +118,31 @@ test.describe("Regular User Access", () => {
|
|||
|
||||
test("can use counter functionality", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
|
||||
// Get initial count (might be any number)
|
||||
const countElement = page.locator("h1").first();
|
||||
await expect(countElement).toBeVisible();
|
||||
|
||||
|
||||
// Click increment
|
||||
await page.click('button:has-text("Increment")');
|
||||
|
||||
|
||||
// Wait for update
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
|
||||
// Counter should have updated (we just verify no error occurred)
|
||||
await expect(countElement).toBeVisible();
|
||||
});
|
||||
|
||||
test("can use sum functionality", async ({ page }) => {
|
||||
await page.goto("/sum");
|
||||
|
||||
|
||||
// Fill in numbers
|
||||
await page.fill('input[aria-label="First number"]', "5");
|
||||
await page.fill('input[aria-label="Second number"]', "3");
|
||||
|
||||
|
||||
// Calculate
|
||||
await page.click('button:has-text("Calculate")');
|
||||
|
||||
|
||||
// Should show result
|
||||
await expect(page.getByText("8")).toBeVisible();
|
||||
});
|
||||
|
|
@ -154,24 +156,24 @@ test.describe("Admin User Access", () => {
|
|||
|
||||
test("redirected from counter page to audit", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
|
||||
// Should be redirected to audit page
|
||||
await expect(page).toHaveURL("/audit");
|
||||
});
|
||||
|
||||
test("redirected from sum page to audit", async ({ page }) => {
|
||||
await page.goto("/sum");
|
||||
|
||||
|
||||
// Should be redirected to audit page
|
||||
await expect(page).toHaveURL("/audit");
|
||||
});
|
||||
|
||||
test("can access audit page", async ({ page }) => {
|
||||
await page.goto("/audit");
|
||||
|
||||
|
||||
// Should stay on audit page
|
||||
await expect(page).toHaveURL("/audit");
|
||||
|
||||
|
||||
// Should see audit tables
|
||||
await expect(page.getByText("Counter Activity")).toBeVisible();
|
||||
await expect(page.getByText("Sum Activity")).toBeVisible();
|
||||
|
|
@ -179,10 +181,10 @@ test.describe("Admin User Access", () => {
|
|||
|
||||
test("navigation only shows Audit", async ({ page }) => {
|
||||
await page.goto("/audit");
|
||||
|
||||
|
||||
// Should see Audit as current
|
||||
await expect(page.getByText("Audit")).toBeVisible();
|
||||
|
||||
|
||||
// Should NOT see Counter or Sum links (for admin users)
|
||||
const counterLinks = page.locator('a[href="/"]');
|
||||
const sumLinks = page.locator('a[href="/sum"]');
|
||||
|
|
@ -192,10 +194,10 @@ test.describe("Admin User Access", () => {
|
|||
|
||||
test("audit page shows records", async ({ page }) => {
|
||||
await page.goto("/audit");
|
||||
|
||||
|
||||
// Should see the tables
|
||||
await expect(page.getByRole("table")).toHaveCount(2);
|
||||
|
||||
|
||||
// Should see column headers (use first() since there are two tables with same headers)
|
||||
await expect(page.getByRole("columnheader", { name: "User" }).first()).toBeVisible();
|
||||
await expect(page.getByRole("columnheader", { name: "Date" }).first()).toBeVisible();
|
||||
|
|
@ -228,11 +230,11 @@ test.describe("Permission Boundary via API", () => {
|
|||
// Login as regular user
|
||||
await clearAuth(page);
|
||||
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
|
||||
|
||||
|
||||
// Get cookies
|
||||
const cookies = await page.context().cookies();
|
||||
const authCookie = cookies.find(c => c.name === "auth_token");
|
||||
|
||||
const authCookie = cookies.find((c) => c.name === "auth_token");
|
||||
|
||||
if (authCookie) {
|
||||
// Try to call audit API directly
|
||||
const response = await request.get(`${API_URL}/api/audit/counter`, {
|
||||
|
|
@ -240,7 +242,7 @@ test.describe("Permission Boundary via API", () => {
|
|||
Cookie: `auth_token=${authCookie.value}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
expect(response.status()).toBe(403);
|
||||
}
|
||||
});
|
||||
|
|
@ -249,11 +251,11 @@ test.describe("Permission Boundary via API", () => {
|
|||
// Login as admin
|
||||
await clearAuth(page);
|
||||
await loginUser(page, ADMIN_USER.email, ADMIN_USER.password);
|
||||
|
||||
|
||||
// Get cookies
|
||||
const cookies = await page.context().cookies();
|
||||
const authCookie = cookies.find(c => c.name === "auth_token");
|
||||
|
||||
const authCookie = cookies.find((c) => c.name === "auth_token");
|
||||
|
||||
if (authCookie) {
|
||||
// Try to call counter API directly
|
||||
const response = await request.get(`${API_URL}/api/counter`, {
|
||||
|
|
@ -261,7 +263,7 @@ test.describe("Permission Boundary via API", () => {
|
|||
Cookie: `auth_token=${authCookie.value}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
expect(response.status()).toBe(403);
|
||||
}
|
||||
});
|
||||
|
|
@ -273,11 +275,11 @@ test.describe("Session and Logout", () => {
|
|||
await clearAuth(page);
|
||||
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
|
||||
await expect(page).toHaveURL("/");
|
||||
|
||||
|
||||
// Logout
|
||||
await page.click("text=Sign out");
|
||||
await expect(page).toHaveURL("/login");
|
||||
|
||||
|
||||
// Try to access counter
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveURL("/login");
|
||||
|
|
@ -293,12 +295,11 @@ test.describe("Session and Logout", () => {
|
|||
path: "/",
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
// Try to access protected page
|
||||
await page.goto("/");
|
||||
|
||||
|
||||
// Should be redirected to login
|
||||
await expect(page).toHaveURL("/login");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { test, expect, Page } from "@playwright/test";
|
|||
|
||||
/**
|
||||
* Profile E2E tests
|
||||
*
|
||||
*
|
||||
* These tests verify that:
|
||||
* 1. Regular users can access and use the profile page
|
||||
* 2. Admin users cannot access the profile page
|
||||
|
|
@ -51,8 +51,8 @@ async function loginUser(page: Page, email: string, password: string) {
|
|||
// Helper to clear profile data via API
|
||||
async function clearProfileData(page: Page) {
|
||||
const cookies = await page.context().cookies();
|
||||
const authCookie = cookies.find(c => c.name === "auth_token");
|
||||
|
||||
const authCookie = cookies.find((c) => c.name === "auth_token");
|
||||
|
||||
if (authCookie) {
|
||||
await page.request.put(`${API_URL}/api/profile`, {
|
||||
headers: {
|
||||
|
|
@ -77,10 +77,10 @@ test.describe("Profile - Regular User Access", () => {
|
|||
|
||||
test("can navigate to profile page from counter", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
|
||||
// Should see My Profile link
|
||||
await expect(page.getByText("My Profile")).toBeVisible();
|
||||
|
||||
|
||||
// Click to navigate
|
||||
await page.click('a[href="/profile"]');
|
||||
await expect(page).toHaveURL("/profile");
|
||||
|
|
@ -88,10 +88,10 @@ test.describe("Profile - Regular User Access", () => {
|
|||
|
||||
test("can navigate to profile page from sum", async ({ page }) => {
|
||||
await page.goto("/sum");
|
||||
|
||||
|
||||
// Should see My Profile link
|
||||
await expect(page.getByText("My Profile")).toBeVisible();
|
||||
|
||||
|
||||
// Click to navigate
|
||||
await page.click('a[href="/profile"]');
|
||||
await expect(page).toHaveURL("/profile");
|
||||
|
|
@ -99,17 +99,17 @@ test.describe("Profile - Regular User Access", () => {
|
|||
|
||||
test("profile page displays correct elements", async ({ page }) => {
|
||||
await page.goto("/profile");
|
||||
|
||||
|
||||
// Should see page title
|
||||
await expect(page.getByRole("heading", { name: "My Profile" })).toBeVisible();
|
||||
|
||||
|
||||
// Should see login email label with read-only badge
|
||||
await expect(page.getByText("Login EmailRead only")).toBeVisible();
|
||||
|
||||
|
||||
// Should see contact details section
|
||||
await expect(page.getByText("Contact Details")).toBeVisible();
|
||||
await expect(page.getByText(/communication purposes only/i)).toBeVisible();
|
||||
|
||||
|
||||
// Should see all form fields
|
||||
await expect(page.getByLabel("Contact Email")).toBeVisible();
|
||||
await expect(page.getByLabel("Telegram")).toBeVisible();
|
||||
|
|
@ -119,7 +119,7 @@ test.describe("Profile - Regular User Access", () => {
|
|||
|
||||
test("login email is displayed and read-only", async ({ page }) => {
|
||||
await page.goto("/profile");
|
||||
|
||||
|
||||
// Login email should show the user's email
|
||||
const loginEmailInput = page.locator('input[type="email"][disabled]');
|
||||
await expect(loginEmailInput).toHaveValue(REGULAR_USER.email);
|
||||
|
|
@ -128,7 +128,7 @@ test.describe("Profile - Regular User Access", () => {
|
|||
|
||||
test("navigation shows Counter, Sum, and My Profile", async ({ page }) => {
|
||||
await page.goto("/profile");
|
||||
|
||||
|
||||
// Should see all nav items (Counter and Sum as links)
|
||||
await expect(page.locator('a[href="/"]')).toBeVisible();
|
||||
await expect(page.locator('a[href="/sum"]')).toBeVisible();
|
||||
|
|
@ -147,7 +147,7 @@ test.describe("Profile - Form Behavior", () => {
|
|||
|
||||
test("new user has empty profile fields", async ({ page }) => {
|
||||
await page.goto("/profile");
|
||||
|
||||
|
||||
// All editable fields should be empty
|
||||
await expect(page.getByLabel("Contact Email")).toHaveValue("");
|
||||
await expect(page.getByLabel("Telegram")).toHaveValue("");
|
||||
|
|
@ -157,7 +157,7 @@ test.describe("Profile - Form Behavior", () => {
|
|||
|
||||
test("save button is disabled when no changes", async ({ page }) => {
|
||||
await page.goto("/profile");
|
||||
|
||||
|
||||
// Save button should be disabled
|
||||
const saveButton = page.getByRole("button", { name: /save changes/i });
|
||||
await expect(saveButton).toBeDisabled();
|
||||
|
|
@ -165,10 +165,10 @@ test.describe("Profile - Form Behavior", () => {
|
|||
|
||||
test("save button is enabled after making changes", async ({ page }) => {
|
||||
await page.goto("/profile");
|
||||
|
||||
|
||||
// Make a change
|
||||
await page.fill("#telegram", "@testhandle");
|
||||
|
||||
|
||||
// Save button should be enabled
|
||||
const saveButton = page.getByRole("button", { name: /save changes/i });
|
||||
await expect(saveButton).toBeEnabled();
|
||||
|
|
@ -176,22 +176,22 @@ test.describe("Profile - Form Behavior", () => {
|
|||
|
||||
test("can save profile and values persist", async ({ page }) => {
|
||||
await page.goto("/profile");
|
||||
|
||||
|
||||
// Fill in all fields
|
||||
await page.fill("#contact_email", "contact@test.com");
|
||||
await page.fill("#telegram", "@testuser");
|
||||
await page.fill("#signal", "signal.42");
|
||||
await page.fill("#nostr_npub", VALID_NPUB);
|
||||
|
||||
|
||||
// Save
|
||||
await page.click('button:has-text("Save Changes")');
|
||||
|
||||
|
||||
// Should see success message
|
||||
await expect(page.getByText(/saved successfully/i)).toBeVisible();
|
||||
|
||||
|
||||
// Reload and verify values persist
|
||||
await page.reload();
|
||||
|
||||
|
||||
await expect(page.getByLabel("Contact Email")).toHaveValue("contact@test.com");
|
||||
await expect(page.getByLabel("Telegram")).toHaveValue("@testuser");
|
||||
await expect(page.getByLabel("Signal")).toHaveValue("signal.42");
|
||||
|
|
@ -200,20 +200,20 @@ test.describe("Profile - Form Behavior", () => {
|
|||
|
||||
test("can clear a field and save", async ({ page }) => {
|
||||
await page.goto("/profile");
|
||||
|
||||
|
||||
// First set a value
|
||||
await page.fill("#telegram", "@initial");
|
||||
await page.click('button:has-text("Save Changes")');
|
||||
await expect(page.getByText(/saved successfully/i)).toBeVisible();
|
||||
|
||||
|
||||
// Wait for toast to disappear
|
||||
await expect(page.getByText(/saved successfully/i)).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
|
||||
// Clear the field
|
||||
await page.fill("#telegram", "");
|
||||
await page.click('button:has-text("Save Changes")');
|
||||
await expect(page.getByText(/saved successfully/i)).toBeVisible();
|
||||
|
||||
|
||||
// Reload and verify it's cleared
|
||||
await page.reload();
|
||||
await expect(page.getByLabel("Telegram")).toHaveValue("");
|
||||
|
|
@ -229,26 +229,26 @@ test.describe("Profile - Validation", () => {
|
|||
|
||||
test("auto-prepends @ for telegram when starting with letter", async ({ page }) => {
|
||||
await page.goto("/profile");
|
||||
|
||||
|
||||
// Type a letter without @ - should auto-prepend @
|
||||
await page.fill("#telegram", "testhandle");
|
||||
|
||||
|
||||
// Should show @testhandle in the input
|
||||
await expect(page.locator("#telegram")).toHaveValue("@testhandle");
|
||||
});
|
||||
|
||||
test("shows error for telegram handle with no characters after @", async ({ page }) => {
|
||||
await page.goto("/profile");
|
||||
|
||||
|
||||
// Enter telegram with @ but nothing after (needs at least 1 char)
|
||||
await page.fill("#telegram", "@");
|
||||
|
||||
|
||||
// Wait for debounced validation
|
||||
await page.waitForTimeout(600);
|
||||
|
||||
|
||||
// Should show error about needing at least one character
|
||||
await expect(page.getByText(/at least one character after @/i)).toBeVisible();
|
||||
|
||||
|
||||
// Save button should be disabled
|
||||
const saveButton = page.getByRole("button", { name: /save changes/i });
|
||||
await expect(saveButton).toBeDisabled();
|
||||
|
|
@ -256,13 +256,13 @@ test.describe("Profile - Validation", () => {
|
|||
|
||||
test("shows error for invalid npub", async ({ page }) => {
|
||||
await page.goto("/profile");
|
||||
|
||||
|
||||
// Enter invalid npub
|
||||
await page.fill("#nostr_npub", "invalidnpub");
|
||||
|
||||
|
||||
// Should show error
|
||||
await expect(page.getByText(/must start with 'npub1'/i)).toBeVisible();
|
||||
|
||||
|
||||
// Save button should be disabled
|
||||
const saveButton = page.getByRole("button", { name: /save changes/i });
|
||||
await expect(saveButton).toBeDisabled();
|
||||
|
|
@ -270,38 +270,38 @@ test.describe("Profile - Validation", () => {
|
|||
|
||||
test("can fix validation error and save", async ({ page }) => {
|
||||
await page.goto("/profile");
|
||||
|
||||
|
||||
// Enter invalid telegram (just @ with no handle)
|
||||
await page.fill("#telegram", "@");
|
||||
|
||||
|
||||
// Wait for debounced validation
|
||||
await page.waitForTimeout(600);
|
||||
|
||||
|
||||
await expect(page.getByText(/at least one character after @/i)).toBeVisible();
|
||||
|
||||
|
||||
// Fix it
|
||||
await page.fill("#telegram", "@validhandle");
|
||||
|
||||
|
||||
// Wait for debounced validation
|
||||
await page.waitForTimeout(600);
|
||||
|
||||
|
||||
// Error should disappear
|
||||
await expect(page.getByText(/at least one character after @/i)).not.toBeVisible();
|
||||
|
||||
|
||||
// Should be able to save
|
||||
const saveButton = page.getByRole("button", { name: /save changes/i });
|
||||
await expect(saveButton).toBeEnabled();
|
||||
|
||||
|
||||
await page.click('button:has-text("Save Changes")');
|
||||
await expect(page.getByText(/saved successfully/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows error for invalid email format", async ({ page }) => {
|
||||
await page.goto("/profile");
|
||||
|
||||
|
||||
// Enter invalid email
|
||||
await page.fill("#contact_email", "not-an-email");
|
||||
|
||||
|
||||
// Should show error
|
||||
await expect(page.getByText(/valid email/i)).toBeVisible();
|
||||
});
|
||||
|
|
@ -315,25 +315,25 @@ test.describe("Profile - Admin User Access", () => {
|
|||
|
||||
test("admin does not see My Profile link", async ({ page }) => {
|
||||
await page.goto("/audit");
|
||||
|
||||
|
||||
// Should be on audit page
|
||||
await expect(page).toHaveURL("/audit");
|
||||
|
||||
|
||||
// Should NOT see My Profile link
|
||||
await expect(page.locator('a[href="/profile"]')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("admin cannot access profile page - redirected to audit", async ({ page }) => {
|
||||
await page.goto("/profile");
|
||||
|
||||
|
||||
// Should be redirected to audit
|
||||
await expect(page).toHaveURL("/audit");
|
||||
});
|
||||
|
||||
test("admin API call to profile returns 403", async ({ page, request }) => {
|
||||
const cookies = await page.context().cookies();
|
||||
const authCookie = cookies.find(c => c.name === "auth_token");
|
||||
|
||||
const authCookie = cookies.find((c) => c.name === "auth_token");
|
||||
|
||||
if (authCookie) {
|
||||
// Try to call profile API directly
|
||||
const response = await request.get(`${API_URL}/api/profile`, {
|
||||
|
|
@ -341,7 +341,7 @@ test.describe("Profile - Admin User Access", () => {
|
|||
Cookie: `auth_token=${authCookie.value}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
expect(response.status()).toBe(403);
|
||||
}
|
||||
});
|
||||
|
|
@ -362,4 +362,3 @@ test.describe("Profile - Unauthenticated Access", () => {
|
|||
expect(response.status()).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue