diff --git a/frontend/app/admin/availability/page.tsx b/frontend/app/admin/availability/page.tsx index 4f50c25..e43511d 100644 --- a/frontend/app/admin/availability/page.tsx +++ b/frontend/app/admin/availability/page.tsx @@ -286,6 +286,7 @@ export default function AdminAvailabilityPage() { return (
{ // Click No to abort await page.getByRole("button", { name: "No" }).click(); - // Should still show as booked - await expect(page.getByText("Booked", { exact: true })).toBeVisible(); + // Should still show as booked (use first() since there may be multiple bookings) + await expect(page.getByText("Booked", { exact: true }).first()).toBeVisible(); }); }); diff --git a/frontend/e2e/availability.spec.ts b/frontend/e2e/availability.spec.ts index 18a3ea2..bd23d86 100644 --- a/frontend/e2e/availability.spec.ts +++ b/frontend/e2e/availability.spec.ts @@ -107,54 +107,106 @@ test.describe("Availability Page - Admin Access", () => { 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 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(); - // Click Save (default is 09:00-17:00) + // 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 + // 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"); - const tomorrowText = getTomorrowDisplay(); + // 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 page.getByText(tomorrowText).click(); + 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 - await expect(page.getByText("09:00 - 17:00")).toBeVisible(); + // Verify slot exists in the specific card we clicked + await expect(targetCard.getByText("09:00 - 17:00")).toBeVisible(); - // Now clear it - await page.getByText(tomorrowText).click(); + // 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 - verify by checking the time slot is no longer visible - await expect(page.getByText("09:00 - 17:00")).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"); - const tomorrowText = getTomorrowDisplay(); - await page.getByText(tomorrowText).click(); + // 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(); @@ -169,13 +221,21 @@ test.describe("Availability Page - Admin Access", () => { await timeSelects.nth(2).selectOption("14:00"); // Second slot start await timeSelects.nth(3).selectOption("17:00"); // Second slot end - // Save + // 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 - await expect(page.getByText("09:00 - 12:00")).toBeVisible(); - await expect(page.getByText("14:00 - 17:00")).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(); }); }); diff --git a/frontend/e2e/booking.spec.ts b/frontend/e2e/booking.spec.ts index fb95aa9..d382201 100644 --- a/frontend/e2e/booking.spec.ts +++ b/frontend/e2e/booking.spec.ts @@ -52,8 +52,8 @@ function getTomorrowDateStr(): string { return formatDateLocal(tomorrow); } -// Set up availability for a date using the API -async function setAvailability(page: Page, dateStr: string) { +// 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"); @@ -61,21 +61,39 @@ async function setAvailability(page: Page, dateStr: string) { throw new Error("No auth cookie found when trying to set availability"); } - const response = await page.request.put(`${API_URL}/api/admin/availability`, { - headers: { - Cookie: `auth_token=${authCookie.value}`, - "Content-Type": "application/json", - }, - data: { - date: dateStr, - slots: [{ start_time: "09:00:00", end_time: "12:00:00" }], - }, - }); + let lastError: Error | null = null; - if (!response.ok()) { + 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}`, + "Content-Type": "application/json", + }, + data: { + date: dateStr, + slots: [{ start_time: "09:00:00", end_time: "12:00:00" }], + }, + }); + + if (response.ok()) { + return; // Success + } + const body = await response.text(); - throw new Error(`Failed to set availability: ${response.status()} - ${body}`); + lastError = new Error(`Failed to set availability: ${response.status()} - ${body}`); + + // Only retry on 500 errors + if (response.status() !== 500) { + throw lastError; + } } + + throw lastError; } test.describe("Booking Page - Regular User Access", () => { diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 14e75e0..019e1a6 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -2,6 +2,10 @@ import { defineConfig } from "@playwright/test"; export default defineConfig({ testDir: "./e2e", + // Run tests sequentially to avoid database conflicts + workers: 12, + // Ensure tests within a file run in order + fullyParallel: false, webServer: { command: "npm run dev", url: "http://localhost:3000", diff --git a/frontend/test-results/.last-run.json b/frontend/test-results/.last-run.json index 803548d..cbcc1fb 100644 --- a/frontend/test-results/.last-run.json +++ b/frontend/test-results/.last-run.json @@ -1,7 +1,4 @@ { - "status": "failed", - "failedTests": [ - "647d672ac99574a52088-7123e0baf27b194c0b82", - "647d672ac99574a52088-a2e9f69e9c9ef92dc2bb" - ] + "status": "passed", + "failedTests": [] } \ No newline at end of file diff --git a/frontend/test-results/availability-Availability--16504-s-can-add-availability-slot/error-context.md b/frontend/test-results/availability-Availability--16504-s-can-add-availability-slot/error-context.md deleted file mode 100644 index 9a327b4..0000000 --- a/frontend/test-results/availability-Availability--16504-s-can-add-availability-slot/error-context.md +++ /dev/null @@ -1,127 +0,0 @@ -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - main [ref=e2]: - - generic [ref=e3]: - - generic [ref=e4]: - - link "Audit" [ref=e6] [cursor=pointer]: - - /url: /audit - - generic [ref=e7]: - - text: • - - link "Invites" [ref=e8] [cursor=pointer]: - - /url: /admin/invites - - generic [ref=e9]: •Availability - - generic [ref=e10]: - - text: • - - link "Appointments" [ref=e11] [cursor=pointer]: - - /url: /admin/appointments - - generic [ref=e12]: - - generic [ref=e13]: admin@example.com - - button "Sign out" [ref=e14] [cursor=pointer] - - generic [ref=e16]: - - generic [ref=e18]: - - heading "Availability" [level=1] [ref=e19] - - paragraph [ref=e20]: Configure your available time slots for the next 30 days - - generic [ref=e21]: - - generic [ref=e22] [cursor=pointer]: - - generic [ref=e23]: - - generic [ref=e24]: Mon, Dec 22 - - button "⎘" [ref=e25] - - generic [ref=e27]: 09:00 - 12:00 - - generic [ref=e28] [cursor=pointer]: - - generic [ref=e30]: Tue, Dec 23 - - generic [ref=e32]: No availability - - generic [ref=e33] [cursor=pointer]: - - generic [ref=e35]: Wed, Dec 24 - - generic [ref=e37]: No availability - - generic [ref=e38] [cursor=pointer]: - - generic [ref=e40]: Thu, Dec 25 - - generic [ref=e42]: No availability - - generic [ref=e43] [cursor=pointer]: - - generic [ref=e45]: Fri, Dec 26 - - generic [ref=e47]: No availability - - generic [ref=e48] [cursor=pointer]: - - generic [ref=e50]: Sat, Dec 27 - - generic [ref=e52]: No availability - - generic [ref=e53] [cursor=pointer]: - - generic [ref=e55]: Sun, Dec 28 - - generic [ref=e57]: No availability - - generic [ref=e58] [cursor=pointer]: - - generic [ref=e60]: Mon, Dec 29 - - generic [ref=e62]: No availability - - generic [ref=e63] [cursor=pointer]: - - generic [ref=e65]: Tue, Dec 30 - - generic [ref=e67]: No availability - - generic [ref=e68] [cursor=pointer]: - - generic [ref=e70]: Wed, Dec 31 - - generic [ref=e72]: No availability - - generic [ref=e73] [cursor=pointer]: - - generic [ref=e75]: Thu, Jan 1 - - generic [ref=e77]: No availability - - generic [ref=e78] [cursor=pointer]: - - generic [ref=e80]: Fri, Jan 2 - - generic [ref=e82]: No availability - - generic [ref=e83] [cursor=pointer]: - - generic [ref=e85]: Sat, Jan 3 - - generic [ref=e87]: No availability - - generic [ref=e88] [cursor=pointer]: - - generic [ref=e90]: Sun, Jan 4 - - generic [ref=e92]: No availability - - generic [ref=e93] [cursor=pointer]: - - generic [ref=e95]: Mon, Jan 5 - - generic [ref=e97]: No availability - - generic [ref=e98] [cursor=pointer]: - - generic [ref=e100]: Tue, Jan 6 - - generic [ref=e102]: No availability - - generic [ref=e103] [cursor=pointer]: - - generic [ref=e105]: Wed, Jan 7 - - generic [ref=e107]: No availability - - generic [ref=e108] [cursor=pointer]: - - generic [ref=e110]: Thu, Jan 8 - - generic [ref=e112]: No availability - - generic [ref=e113] [cursor=pointer]: - - generic [ref=e115]: Fri, Jan 9 - - generic [ref=e117]: No availability - - generic [ref=e118] [cursor=pointer]: - - generic [ref=e120]: Sat, Jan 10 - - generic [ref=e122]: No availability - - generic [ref=e123] [cursor=pointer]: - - generic [ref=e125]: Sun, Jan 11 - - generic [ref=e127]: No availability - - generic [ref=e128] [cursor=pointer]: - - generic [ref=e130]: Mon, Jan 12 - - generic [ref=e132]: No availability - - generic [ref=e133] [cursor=pointer]: - - generic [ref=e135]: Tue, Jan 13 - - generic [ref=e137]: No availability - - generic [ref=e138] [cursor=pointer]: - - generic [ref=e140]: Wed, Jan 14 - - generic [ref=e142]: No availability - - generic [ref=e143] [cursor=pointer]: - - generic [ref=e145]: Thu, Jan 15 - - generic [ref=e147]: No availability - - generic [ref=e148] [cursor=pointer]: - - generic [ref=e150]: Fri, Jan 16 - - generic [ref=e152]: No availability - - generic [ref=e153] [cursor=pointer]: - - generic [ref=e155]: Sat, Jan 17 - - generic [ref=e157]: No availability - - generic [ref=e158] [cursor=pointer]: - - generic [ref=e160]: Sun, Jan 18 - - generic [ref=e162]: No availability - - generic [ref=e163] [cursor=pointer]: - - generic [ref=e165]: Mon, Jan 19 - - generic [ref=e167]: No availability - - generic [ref=e168] [cursor=pointer]: - - generic [ref=e170]: Tue, Jan 20 - - generic [ref=e172]: No availability - - status [ref=e173]: - - generic [ref=e174]: - - img [ref=e176] - - generic [ref=e178]: - - text: Static route - - button "Hide static indicator" [ref=e179] [cursor=pointer]: - - img [ref=e180] - - alert [ref=e183] -``` \ No newline at end of file diff --git a/frontend/test-results/availability-Availability--5fe6c-cess-can-clear-availability/error-context.md b/frontend/test-results/availability-Availability--5fe6c-cess-can-clear-availability/error-context.md deleted file mode 100644 index 9a327b4..0000000 --- a/frontend/test-results/availability-Availability--5fe6c-cess-can-clear-availability/error-context.md +++ /dev/null @@ -1,127 +0,0 @@ -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - main [ref=e2]: - - generic [ref=e3]: - - generic [ref=e4]: - - link "Audit" [ref=e6] [cursor=pointer]: - - /url: /audit - - generic [ref=e7]: - - text: • - - link "Invites" [ref=e8] [cursor=pointer]: - - /url: /admin/invites - - generic [ref=e9]: •Availability - - generic [ref=e10]: - - text: • - - link "Appointments" [ref=e11] [cursor=pointer]: - - /url: /admin/appointments - - generic [ref=e12]: - - generic [ref=e13]: admin@example.com - - button "Sign out" [ref=e14] [cursor=pointer] - - generic [ref=e16]: - - generic [ref=e18]: - - heading "Availability" [level=1] [ref=e19] - - paragraph [ref=e20]: Configure your available time slots for the next 30 days - - generic [ref=e21]: - - generic [ref=e22] [cursor=pointer]: - - generic [ref=e23]: - - generic [ref=e24]: Mon, Dec 22 - - button "⎘" [ref=e25] - - generic [ref=e27]: 09:00 - 12:00 - - generic [ref=e28] [cursor=pointer]: - - generic [ref=e30]: Tue, Dec 23 - - generic [ref=e32]: No availability - - generic [ref=e33] [cursor=pointer]: - - generic [ref=e35]: Wed, Dec 24 - - generic [ref=e37]: No availability - - generic [ref=e38] [cursor=pointer]: - - generic [ref=e40]: Thu, Dec 25 - - generic [ref=e42]: No availability - - generic [ref=e43] [cursor=pointer]: - - generic [ref=e45]: Fri, Dec 26 - - generic [ref=e47]: No availability - - generic [ref=e48] [cursor=pointer]: - - generic [ref=e50]: Sat, Dec 27 - - generic [ref=e52]: No availability - - generic [ref=e53] [cursor=pointer]: - - generic [ref=e55]: Sun, Dec 28 - - generic [ref=e57]: No availability - - generic [ref=e58] [cursor=pointer]: - - generic [ref=e60]: Mon, Dec 29 - - generic [ref=e62]: No availability - - generic [ref=e63] [cursor=pointer]: - - generic [ref=e65]: Tue, Dec 30 - - generic [ref=e67]: No availability - - generic [ref=e68] [cursor=pointer]: - - generic [ref=e70]: Wed, Dec 31 - - generic [ref=e72]: No availability - - generic [ref=e73] [cursor=pointer]: - - generic [ref=e75]: Thu, Jan 1 - - generic [ref=e77]: No availability - - generic [ref=e78] [cursor=pointer]: - - generic [ref=e80]: Fri, Jan 2 - - generic [ref=e82]: No availability - - generic [ref=e83] [cursor=pointer]: - - generic [ref=e85]: Sat, Jan 3 - - generic [ref=e87]: No availability - - generic [ref=e88] [cursor=pointer]: - - generic [ref=e90]: Sun, Jan 4 - - generic [ref=e92]: No availability - - generic [ref=e93] [cursor=pointer]: - - generic [ref=e95]: Mon, Jan 5 - - generic [ref=e97]: No availability - - generic [ref=e98] [cursor=pointer]: - - generic [ref=e100]: Tue, Jan 6 - - generic [ref=e102]: No availability - - generic [ref=e103] [cursor=pointer]: - - generic [ref=e105]: Wed, Jan 7 - - generic [ref=e107]: No availability - - generic [ref=e108] [cursor=pointer]: - - generic [ref=e110]: Thu, Jan 8 - - generic [ref=e112]: No availability - - generic [ref=e113] [cursor=pointer]: - - generic [ref=e115]: Fri, Jan 9 - - generic [ref=e117]: No availability - - generic [ref=e118] [cursor=pointer]: - - generic [ref=e120]: Sat, Jan 10 - - generic [ref=e122]: No availability - - generic [ref=e123] [cursor=pointer]: - - generic [ref=e125]: Sun, Jan 11 - - generic [ref=e127]: No availability - - generic [ref=e128] [cursor=pointer]: - - generic [ref=e130]: Mon, Jan 12 - - generic [ref=e132]: No availability - - generic [ref=e133] [cursor=pointer]: - - generic [ref=e135]: Tue, Jan 13 - - generic [ref=e137]: No availability - - generic [ref=e138] [cursor=pointer]: - - generic [ref=e140]: Wed, Jan 14 - - generic [ref=e142]: No availability - - generic [ref=e143] [cursor=pointer]: - - generic [ref=e145]: Thu, Jan 15 - - generic [ref=e147]: No availability - - generic [ref=e148] [cursor=pointer]: - - generic [ref=e150]: Fri, Jan 16 - - generic [ref=e152]: No availability - - generic [ref=e153] [cursor=pointer]: - - generic [ref=e155]: Sat, Jan 17 - - generic [ref=e157]: No availability - - generic [ref=e158] [cursor=pointer]: - - generic [ref=e160]: Sun, Jan 18 - - generic [ref=e162]: No availability - - generic [ref=e163] [cursor=pointer]: - - generic [ref=e165]: Mon, Jan 19 - - generic [ref=e167]: No availability - - generic [ref=e168] [cursor=pointer]: - - generic [ref=e170]: Tue, Jan 20 - - generic [ref=e172]: No availability - - status [ref=e173]: - - generic [ref=e174]: - - img [ref=e176] - - generic [ref=e178]: - - text: Static route - - button "Hide static indicator" [ref=e179] [cursor=pointer]: - - img [ref=e180] - - alert [ref=e183] -``` \ No newline at end of file