Fix flaky E2E tests with proper selectors and response waits

- Use data-testid attributes to target specific day cards in availability tests
- Wait for networkidle before interacting with page elements
- Set up PUT and GET response listeners before triggering actions
- Add retry logic for availability API in booking tests
- Fix appointments test to handle multiple 'Booked' elements with .first()
- Increase parallel workers to 12 for faster test execution
This commit is contained in:
counterweight 2025-12-21 01:13:10 +01:00
parent b3e00b0745
commit 89eec1e9c4
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
8 changed files with 120 additions and 294 deletions

View file

@ -178,8 +178,8 @@ test.describe("Appointments Page - With Bookings", () => {
// 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();
});
});

View file

@ -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();
});
});

View file

@ -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", () => {