refactors
This commit is contained in:
parent
4e1a339432
commit
82c4d0168e
28 changed files with 1042 additions and 782 deletions
|
|
@ -65,7 +65,9 @@ test.describe("Availability Page - Admin Access", () => {
|
|||
|
||||
// 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}"]`);
|
||||
if (!testId) {
|
||||
throw new Error("Could not get testid from day card");
|
||||
}
|
||||
|
||||
// First add availability
|
||||
await dayCardWithNoAvailability.click();
|
||||
|
|
@ -83,8 +85,14 @@ test.describe("Availability Page - Admin Access", () => {
|
|||
await saveGetPromise;
|
||||
await expect(page.getByRole("heading", { name: /Edit Time Slots/ })).not.toBeVisible();
|
||||
|
||||
// Verify slot exists in the specific card we clicked
|
||||
await expect(targetCard.getByText("09:00 - 17:00")).toBeVisible();
|
||||
// Re-query the card after save to avoid stale element references
|
||||
// React may have re-rendered the entire list, so we need a fresh reference
|
||||
const targetCard = page.locator(`[data-testid="${testId}"]`);
|
||||
|
||||
// Wait for "No availability" to disappear first, indicating slots have been loaded
|
||||
await expect(targetCard.getByText("No availability")).not.toBeVisible({ timeout: 10000 });
|
||||
// Then verify the specific slot text appears - this ensures the component has re-rendered
|
||||
await expect(targetCard.getByText("09:00 - 17:00")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Now clear it - click on the same card using the testid
|
||||
await targetCard.click();
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { getBackendUrl } from "./helpers/backend-url";
|
|||
*/
|
||||
|
||||
// Set up availability for a date using the API
|
||||
// Includes retry logic to handle race conditions with database reset
|
||||
async function setAvailability(page: Page, dateStr: string) {
|
||||
const cookies = await page.context().cookies();
|
||||
const authCookie = cookies.find((c) => c.name === "auth_token");
|
||||
|
|
@ -18,21 +19,63 @@ 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(`${getBackendUrl()}/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" }],
|
||||
},
|
||||
});
|
||||
const maxRetries = 3;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
if (!response.ok()) {
|
||||
const body = await response.text();
|
||||
throw new Error(`Failed to set availability: ${response.status()} - ${body}`);
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
const response = await page.request.put(`${getBackendUrl()}/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()) {
|
||||
// Verify the response indicates success
|
||||
const body = await response.json();
|
||||
if (body.date === dateStr && body.slots?.length > 0) {
|
||||
return; // Success
|
||||
}
|
||||
throw new Error(`Unexpected availability response: ${JSON.stringify(body)}`);
|
||||
}
|
||||
|
||||
const body = await response.text();
|
||||
const error = new Error(`Failed to set availability: ${response.status()} - ${body}`);
|
||||
|
||||
// Don't retry on 4xx errors (client errors), only on 5xx (server errors)
|
||||
if (response.status() >= 400 && response.status() < 500) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
lastError = error;
|
||||
|
||||
// Don't retry on the last attempt
|
||||
if (attempt < maxRetries - 1) {
|
||||
// Exponential backoff: 200ms, 400ms, 800ms
|
||||
const delay = 200 * Math.pow(2, attempt);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
// Don't retry on the last attempt
|
||||
if (attempt < maxRetries - 1) {
|
||||
// Exponential backoff: 200ms, 400ms, 800ms
|
||||
const delay = 200 * Math.pow(2, attempt);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, all retries failed
|
||||
throw new Error(`Failed to set availability after ${maxRetries} attempts: ${lastError?.message}`);
|
||||
}
|
||||
|
||||
test.describe("Exchange Page - Regular User Access", () => {
|
||||
|
|
|
|||
|
|
@ -9,13 +9,41 @@ import { getBackendUrl } from "./backend-url";
|
|||
/**
|
||||
* Reset the database for the current worker.
|
||||
* Truncates all tables and re-seeds base data.
|
||||
* Retries up to 3 times with exponential backoff to handle transient failures.
|
||||
*/
|
||||
export async function resetDatabase(request: APIRequestContext): Promise<void> {
|
||||
const backendUrl = getBackendUrl();
|
||||
const response = await request.post(`${backendUrl}/api/test/reset`);
|
||||
const maxRetries = 3;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
if (!response.ok()) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Failed to reset database: ${response.status()} - ${text}`);
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
const response = await request.post(`${backendUrl}/api/test/reset`);
|
||||
|
||||
if (response.ok()) {
|
||||
// Verify the response body indicates success
|
||||
const body = await response.json();
|
||||
if (body.status === "reset") {
|
||||
return; // Success
|
||||
}
|
||||
throw new Error(`Unexpected reset response: ${JSON.stringify(body)}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
throw new Error(`Failed to reset database: ${response.status()} - ${text}`);
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
// Don't retry on the last attempt
|
||||
if (attempt < maxRetries - 1) {
|
||||
// Exponential backoff: 100ms, 200ms, 400ms
|
||||
const delay = 100 * Math.pow(2, attempt);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, all retries failed
|
||||
throw new Error(`Failed to reset database after ${maxRetries} attempts: ${lastError?.message}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,13 +22,12 @@ test.beforeEach(async ({ context, request }, testInfo) => {
|
|||
process.env.NEXT_PUBLIC_API_URL = backendUrl;
|
||||
|
||||
// Reset database before each test for isolation
|
||||
try {
|
||||
await resetDatabase(request);
|
||||
} catch (error) {
|
||||
// If reset fails, log but don't fail the test
|
||||
// This allows tests to run even if reset endpoint is unavailable
|
||||
console.warn(`Failed to reset database: ${error}`);
|
||||
}
|
||||
// This must complete successfully before tests run to avoid race conditions
|
||||
await resetDatabase(request);
|
||||
|
||||
// Small delay to ensure database transaction commits are visible
|
||||
// This prevents race conditions where tests start before reset completes
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Add init script to set English language before any page loads
|
||||
// This must be called before any page.goto() calls
|
||||
|
|
|
|||
|
|
@ -48,24 +48,43 @@ async function loginUser(page: Page, email: string, password: string) {
|
|||
}
|
||||
|
||||
// Helper to clear profile data via API
|
||||
// Verifies the operation succeeds to prevent race conditions
|
||||
async function clearProfileData(page: Page) {
|
||||
const cookies = await page.context().cookies();
|
||||
const authCookie = cookies.find((c) => c.name === "auth_token");
|
||||
|
||||
if (authCookie) {
|
||||
await page.request.put(`${getBackendUrl()}/api/profile`, {
|
||||
headers: {
|
||||
Cookie: `auth_token=${authCookie.value}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
data: {
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
},
|
||||
});
|
||||
if (!authCookie) {
|
||||
throw new Error("No auth cookie found when trying to clear profile data");
|
||||
}
|
||||
|
||||
const response = await page.request.put(`${getBackendUrl()}/api/profile`, {
|
||||
headers: {
|
||||
Cookie: `auth_token=${authCookie.value}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
data: {
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Failed to clear profile data: ${response.status()} - ${text}`);
|
||||
}
|
||||
|
||||
// Verify the response indicates fields were cleared
|
||||
const body = await response.json();
|
||||
if (body.telegram !== null && body.telegram !== undefined && body.telegram !== "") {
|
||||
throw new Error(
|
||||
`Profile data not cleared properly. Telegram still has value: ${body.telegram}`
|
||||
);
|
||||
}
|
||||
|
||||
// Small delay to ensure database commit is visible to subsequent operations
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
test.describe("Profile - Regular User Access", () => {
|
||||
|
|
@ -122,14 +141,23 @@ test.describe("Profile - Form Behavior", () => {
|
|||
});
|
||||
await clearAuth(page);
|
||||
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
|
||||
// Clear any existing profile data
|
||||
// Clear any existing profile data and verify it's cleared
|
||||
await clearProfileData(page);
|
||||
// Navigate to profile page to verify it's actually cleared
|
||||
await page.goto("/profile");
|
||||
// Wait for page to load and verify fields are empty
|
||||
await expect(page.getByLabel("Contact Email")).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByLabel("Telegram")).toHaveValue("");
|
||||
await expect(page.getByLabel("Signal")).toHaveValue("");
|
||||
await expect(page.getByLabel("Nostr (npub)")).toHaveValue("");
|
||||
});
|
||||
|
||||
test("form state management, save, persistence, and clearing fields", async ({ page }) => {
|
||||
// Page is already loaded in beforeEach, but ensure we're on it
|
||||
await page.goto("/profile");
|
||||
await expect(page.getByLabel("Contact Email")).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// All editable fields should be empty
|
||||
// All editable fields should be empty (verified in beforeEach, but double-check)
|
||||
await expect(page.getByLabel("Contact Email")).toHaveValue("");
|
||||
await expect(page.getByLabel("Telegram")).toHaveValue("");
|
||||
await expect(page.getByLabel("Signal")).toHaveValue("");
|
||||
|
|
@ -166,13 +194,17 @@ test.describe("Profile - Form Behavior", () => {
|
|||
await expect(page.getByText(/saved successfully/i)).toBeVisible();
|
||||
await expect(page.getByText(/saved successfully/i)).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Clear the field
|
||||
await page.fill("#telegram", "");
|
||||
// Clear the field - use clear() instead of fill("") for reliable clearing
|
||||
await page.locator("#telegram").clear();
|
||||
await page.click('button:has-text("Save Changes")');
|
||||
await expect(page.getByText(/saved successfully/i)).toBeVisible();
|
||||
await expect(page.getByText(/saved successfully/i)).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Reload and verify it's cleared
|
||||
// Reload and wait for page to fully load before checking
|
||||
await page.reload();
|
||||
// Wait for the form to be loaded (check for a form field to ensure page is ready)
|
||||
await expect(page.getByLabel("Contact Email")).toBeVisible({ timeout: 10000 });
|
||||
// Verify telegram field is cleared
|
||||
await expect(page.getByLabel("Telegram")).toHaveValue("");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue