Fix E2E test reliability issues and stale data bugs
- Fix race conditions: Set workers: 1 since all tests share test user state - Fix stale data: GET /api/user and /api/cycle/current now fetch fresh data from database instead of returning stale PocketBase auth store cache - Fix timing: Replace waitForTimeout with retry-based Playwright assertions - Fix mobile test: Use exact heading match to avoid strict mode violation - Add test user setup: Include notificationTime and update rule for users All 1014 unit tests and 190 E2E tests pass. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -129,12 +129,13 @@ test.describe("dashboard", () => {
|
||||
// Click the toggle
|
||||
await toggleCheckbox.click();
|
||||
|
||||
// Wait a moment for the API call
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Toggle should change state
|
||||
const afterChecked = await toggleCheckbox.isChecked();
|
||||
expect(afterChecked).not.toBe(initialChecked);
|
||||
// Wait for the checkbox state to change using retry-based assertion
|
||||
// The API call completes and React re-renders asynchronously
|
||||
if (initialChecked) {
|
||||
await expect(toggleCheckbox).not.toBeChecked({ timeout: 5000 });
|
||||
} else {
|
||||
await expect(toggleCheckbox).toBeChecked({ timeout: 5000 });
|
||||
}
|
||||
} else {
|
||||
test.skip();
|
||||
}
|
||||
@@ -244,59 +245,39 @@ test.describe("dashboard", () => {
|
||||
});
|
||||
|
||||
test("displays cycle day in 'Day X' format", async ({ page }) => {
|
||||
// Wait for dashboard to finish loading
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Look for "Day" followed by a number
|
||||
const cycleDayText = page.getByText(/Day \d+/i);
|
||||
const hasCycleDay = await cycleDayText
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
// Wait for either cycle day or onboarding - both are valid states
|
||||
// Use Playwright's expect with retry for reliable detection
|
||||
const cycleDayText = page.getByText(/Day \d+/i).first();
|
||||
const onboarding = page.getByText(/set.*period|log.*period/i).first();
|
||||
|
||||
// Either has cycle day or onboarding (both valid states)
|
||||
if (!hasCycleDay) {
|
||||
const onboarding = page.getByText(/set.*period|log.*period/i);
|
||||
const hasOnboarding = await onboarding
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
expect(hasCycleDay || hasOnboarding).toBe(true);
|
||||
try {
|
||||
// First try waiting for cycle day with a short timeout
|
||||
await expect(cycleDayText).toBeVisible({ timeout: 5000 });
|
||||
} catch {
|
||||
// If no cycle day, expect onboarding banner
|
||||
await expect(onboarding).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
});
|
||||
|
||||
test("displays current phase name", async ({ page }) => {
|
||||
// Wait for dashboard to finish loading
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Look for phase names
|
||||
const phaseNames = [
|
||||
"MENSTRUAL",
|
||||
"FOLLICULAR",
|
||||
"OVULATION",
|
||||
"EARLY_LUTEAL",
|
||||
"LATE_LUTEAL",
|
||||
];
|
||||
let foundPhase = false;
|
||||
// Wait for either a phase name or onboarding - both are valid states
|
||||
const phaseRegex =
|
||||
/MENSTRUAL|FOLLICULAR|OVULATION|EARLY_LUTEAL|LATE_LUTEAL/i;
|
||||
const phaseText = page.getByText(phaseRegex).first();
|
||||
const onboarding = page.getByText(/set.*period|log.*period/i).first();
|
||||
|
||||
for (const phase of phaseNames) {
|
||||
const phaseText = page.getByText(new RegExp(phase, "i"));
|
||||
const isVisible = await phaseText
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
if (isVisible) {
|
||||
foundPhase = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Either has phase or shows onboarding
|
||||
if (!foundPhase) {
|
||||
const onboarding = page.getByText(/set.*period|log.*period/i);
|
||||
const hasOnboarding = await onboarding
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
expect(foundPhase || hasOnboarding).toBe(true);
|
||||
try {
|
||||
// First try waiting for a phase name with a short timeout
|
||||
await expect(phaseText).toBeVisible({ timeout: 5000 });
|
||||
} catch {
|
||||
// If no phase, expect onboarding banner
|
||||
await expect(onboarding).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -381,81 +362,62 @@ test.describe("dashboard", () => {
|
||||
});
|
||||
|
||||
test("displays seed cycling recommendation", async ({ page }) => {
|
||||
// Wait for dashboard to finish loading
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Look for seed names (flax, pumpkin, sesame, sunflower)
|
||||
const seedText = page.getByText(/flax|pumpkin|sesame|sunflower/i);
|
||||
const hasSeeds = await seedText
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
// Wait for either seed info or onboarding - both are valid states
|
||||
const seedText = page.getByText(/flax|pumpkin|sesame|sunflower/i).first();
|
||||
const onboarding = page.getByText(/set.*period|log.*period/i).first();
|
||||
|
||||
// Either has seeds info or onboarding
|
||||
if (!hasSeeds) {
|
||||
const onboarding = page.getByText(/set.*period|log.*period/i);
|
||||
const hasOnboarding = await onboarding
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
expect(hasSeeds || hasOnboarding).toBe(true);
|
||||
try {
|
||||
await expect(seedText).toBeVisible({ timeout: 5000 });
|
||||
} catch {
|
||||
await expect(onboarding).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
});
|
||||
|
||||
test("displays carbohydrate range", async ({ page }) => {
|
||||
// Wait for dashboard to finish loading
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Look for carb-related text
|
||||
const carbText = page.getByText(/carb|carbohydrate/i);
|
||||
const hasCarbs = await carbText
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
// Wait for either carb info or onboarding - both are valid states
|
||||
const carbText = page.getByText(/carb|carbohydrate/i).first();
|
||||
const onboarding = page.getByText(/set.*period|log.*period/i).first();
|
||||
|
||||
if (!hasCarbs) {
|
||||
const onboarding = page.getByText(/set.*period|log.*period/i);
|
||||
const hasOnboarding = await onboarding
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
expect(hasCarbs || hasOnboarding).toBe(true);
|
||||
try {
|
||||
await expect(carbText).toBeVisible({ timeout: 5000 });
|
||||
} catch {
|
||||
await expect(onboarding).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
});
|
||||
|
||||
test("displays keto guidance", async ({ page }) => {
|
||||
// Wait for dashboard to finish loading
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Look for keto-related text
|
||||
const ketoText = page.getByText(/keto/i);
|
||||
const hasKeto = await ketoText
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
// Wait for either keto info or onboarding - both are valid states
|
||||
const ketoText = page.getByText(/keto/i).first();
|
||||
const onboarding = page.getByText(/set.*period|log.*period/i).first();
|
||||
|
||||
if (!hasKeto) {
|
||||
const onboarding = page.getByText(/set.*period|log.*period/i);
|
||||
const hasOnboarding = await onboarding
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
expect(hasKeto || hasOnboarding).toBe(true);
|
||||
try {
|
||||
await expect(ketoText).toBeVisible({ timeout: 5000 });
|
||||
} catch {
|
||||
await expect(onboarding).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
});
|
||||
|
||||
test("displays nutrition section header", async ({ page }) => {
|
||||
// Wait for dashboard to finish loading
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Nutrition panel should have a header
|
||||
// Wait for nutrition header or text
|
||||
const nutritionHeader = page.getByRole("heading", { name: /nutrition/i });
|
||||
const hasHeader = await nutritionHeader.isVisible().catch(() => false);
|
||||
const nutritionText = page.getByText(/nutrition/i).first();
|
||||
|
||||
if (!hasHeader) {
|
||||
// May be text label instead of heading
|
||||
const nutritionText = page.getByText(/nutrition/i);
|
||||
const hasText = await nutritionText
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
expect(hasHeader || hasText).toBe(true);
|
||||
try {
|
||||
await expect(nutritionHeader).toBeVisible({ timeout: 5000 });
|
||||
} catch {
|
||||
await expect(nutritionText).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user