Files
phaseflow/e2e/settings.spec.ts
Petru Paler 8956e04eca
All checks were successful
Deploy / deploy (push) Successful in 1m39s
Fix garmin-sync upsert and add Settings UI for intensity goals
- Fix dailyLog upsert to use range query (matches today route pattern)
- Properly distinguish 404 errors from other failures in upsert logic
- Add logging for dailyLog create/update operations
- Add Settings UI section for weekly intensity goals per phase
- Add unit tests for upsert behavior and intensity goals UI
- Add E2E tests for intensity goals settings flow

This fixes the issue where Garmin sync was creating new dailyLog
records instead of updating existing ones (322 vs 222 intensity
minutes bug, Unknown HRV bug).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 20:53:43 +00:00

970 lines
31 KiB
TypeScript

// ABOUTME: E2E tests for settings page including preferences and logout.
// ABOUTME: Tests form rendering, validation, submission, and logout functionality.
import { expect, test } from "@playwright/test";
test.describe("settings", () => {
test.describe("unauthenticated", () => {
test("redirects to login when not authenticated", async ({ page }) => {
await page.goto("/settings");
// Should redirect to /login
await expect(page).toHaveURL(/\/login/);
});
test("garmin settings redirects to login when not authenticated", async ({
page,
}) => {
await page.goto("/settings/garmin");
// Should redirect to /login
await expect(page).toHaveURL(/\/login/);
});
});
test.describe("authenticated", () => {
// These tests require TEST_USER_EMAIL and TEST_USER_PASSWORD env vars
test.beforeEach(async ({ page }) => {
const email = process.env.TEST_USER_EMAIL;
const password = process.env.TEST_USER_PASSWORD;
if (!email || !password) {
test.skip();
return;
}
// Login via the login page
await page.goto("/login");
await page.waitForLoadState("networkidle");
const emailInput = page.getByLabel(/email/i);
const hasEmailForm = await emailInput.isVisible().catch(() => false);
if (!hasEmailForm) {
test.skip();
return;
}
await emailInput.fill(email);
await page.getByLabel(/password/i).fill(password);
await page.getByRole("button", { name: /sign in/i }).click();
// Wait for redirect to dashboard, then navigate to settings
await page.waitForURL("/", { timeout: 10000 });
await page.goto("/settings");
await page.waitForLoadState("networkidle");
});
test("displays settings form with required fields", async ({ page }) => {
// Check for cycle length input
const cycleLengthInput = page.getByLabel(/cycle length/i);
await expect(cycleLengthInput).toBeVisible();
// Check for notification time input
const notificationTimeInput = page.getByLabel(/notification time/i);
await expect(notificationTimeInput).toBeVisible();
// Check for timezone input
const timezoneInput = page.getByLabel(/timezone/i);
await expect(timezoneInput).toBeVisible();
});
test("shows save button", async ({ page }) => {
const saveButton = page.getByRole("button", { name: /save/i });
await expect(saveButton).toBeVisible();
});
test("shows logout button", async ({ page }) => {
const logoutButton = page.getByRole("button", { name: /log ?out/i });
await expect(logoutButton).toBeVisible();
});
test("shows link to garmin settings", async ({ page }) => {
const garminLink = page.getByRole("link", { name: /manage|garmin/i });
await expect(garminLink).toBeVisible();
});
test("shows back to dashboard link", async ({ page }) => {
const backLink = page.getByRole("link", { name: /back|dashboard/i });
await expect(backLink).toBeVisible();
});
test("can update cycle length", async ({ page }) => {
const cycleLengthInput = page.getByLabel(/cycle length/i);
// Clear and enter new value
await cycleLengthInput.fill("30");
// Click save
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
// Should show success message or no error
await page.waitForTimeout(1000);
// Either success message or value persisted
const errorMessage = page.locator('[role="alert"]').filter({
hasText: /error|failed/i,
});
const hasError = await errorMessage.isVisible().catch(() => false);
// No error means success
expect(hasError).toBe(false);
});
test("validates cycle length range", async ({ page }) => {
const cycleLengthInput = page.getByLabel(/cycle length/i);
// Enter invalid value (too low)
await cycleLengthInput.fill("10");
// Click save
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
// Should show validation error or HTML5 validation
await page.waitForTimeout(500);
});
test("can navigate to garmin settings", async ({ page }) => {
const garminLink = page.getByRole("link", { name: /manage|garmin/i });
await garminLink.click();
await expect(page).toHaveURL(/\/settings\/garmin/);
});
test("can navigate back to dashboard", async ({ page }) => {
const backLink = page.getByRole("link", { name: /back|dashboard/i });
await backLink.click();
await expect(page).toHaveURL("/");
});
test("logout redirects to login", async ({ page }) => {
const logoutButton = page.getByRole("button", { name: /log ?out/i });
await logoutButton.click();
// Should redirect to login page
await expect(page).toHaveURL(/\/login/, { timeout: 10000 });
});
});
test.describe("garmin settings", () => {
test.beforeEach(async ({ page }) => {
const email = process.env.TEST_USER_EMAIL;
const password = process.env.TEST_USER_PASSWORD;
if (!email || !password) {
test.skip();
return;
}
// Login and navigate to garmin settings
await page.goto("/login");
await page.waitForLoadState("networkidle");
const emailInput = page.getByLabel(/email/i);
const hasEmailForm = await emailInput.isVisible().catch(() => false);
if (!hasEmailForm) {
test.skip();
return;
}
await emailInput.fill(email);
await page.getByLabel(/password/i).fill(password);
await page.getByRole("button", { name: /sign in/i }).click();
await page.waitForURL("/", { timeout: 10000 });
await page.goto("/settings/garmin");
await page.waitForLoadState("networkidle");
});
test("displays garmin connection status", async ({ page }) => {
// Look for connection status indicator
const statusText = page.getByText(/connected|not connected|status/i);
await expect(statusText.first()).toBeVisible();
});
test("shows back navigation", async ({ page }) => {
const backLink = page.getByRole("link", { name: /back|settings/i });
await expect(backLink).toBeVisible();
});
});
test.describe("settings form validation", () => {
// These tests require TEST_USER_EMAIL and TEST_USER_PASSWORD env vars
test.beforeEach(async ({ page }) => {
const email = process.env.TEST_USER_EMAIL;
const password = process.env.TEST_USER_PASSWORD;
if (!email || !password) {
test.skip();
return;
}
await page.goto("/login");
await page.waitForLoadState("networkidle");
const emailInput = page.getByLabel(/email/i);
const hasEmailForm = await emailInput.isVisible().catch(() => false);
if (!hasEmailForm) {
test.skip();
return;
}
await emailInput.fill(email);
await page.getByLabel(/password/i).fill(password);
await page.getByRole("button", { name: /sign in/i }).click();
await page.waitForURL("/", { timeout: 10000 });
await page.goto("/settings");
await page.waitForLoadState("networkidle");
});
test("notification time field accepts valid HH:MM format", async ({
page,
}) => {
const notificationTimeInput = page.getByLabel(/notification time/i);
const isVisible = await notificationTimeInput
.isVisible()
.catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Enter a valid time
await notificationTimeInput.fill("07:00");
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
// Wait for save
await page.waitForTimeout(1000);
// No error should be shown
const errorMessage = page.locator('[role="alert"]').filter({
hasText: /error|failed|invalid/i,
});
const hasError = await errorMessage.isVisible().catch(() => false);
expect(hasError).toBe(false);
});
test("cycle length rejects value below minimum (21)", async ({ page }) => {
const cycleLengthInput = page.getByLabel(/cycle length/i);
const isVisible = await cycleLengthInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Enter invalid value (too low)
await cycleLengthInput.fill("20");
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
// Wait for validation
await page.waitForTimeout(500);
// Either HTML5 validation or error message should appear
// Input should have min attribute or form shows error
const inputMin = await cycleLengthInput.getAttribute("min");
if (inputMin) {
expect(Number.parseInt(inputMin, 10)).toBeGreaterThanOrEqual(21);
}
});
test("cycle length rejects value above maximum (45)", async ({ page }) => {
const cycleLengthInput = page.getByLabel(/cycle length/i);
const isVisible = await cycleLengthInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Enter invalid value (too high)
await cycleLengthInput.fill("50");
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
// Wait for validation
await page.waitForTimeout(500);
// Input should have max attribute
const inputMax = await cycleLengthInput.getAttribute("max");
if (inputMax) {
expect(Number.parseInt(inputMax, 10)).toBeLessThanOrEqual(45);
}
});
test("timezone field is editable", async ({ page }) => {
const timezoneInput = page.getByLabel(/timezone/i);
const isVisible = await timezoneInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Timezone could be select or input
const inputType = await timezoneInput.evaluate((el) =>
el.tagName.toLowerCase(),
);
if (inputType === "select") {
// Should have options
const options = timezoneInput.locator("option");
const optionCount = await options.count();
expect(optionCount).toBeGreaterThan(0);
} else {
// Should be able to type in it
const isEditable = await timezoneInput.isEditable();
expect(isEditable).toBe(true);
}
});
test("current cycle length value is displayed", async ({ page }) => {
const cycleLengthInput = page.getByLabel(/cycle length/i);
const isVisible = await cycleLengthInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Should have a current value
const value = await cycleLengthInput.inputValue();
// Value should be a number in valid range
const numValue = Number.parseInt(value, 10);
if (!Number.isNaN(numValue)) {
expect(numValue).toBeGreaterThanOrEqual(21);
expect(numValue).toBeLessThanOrEqual(45);
}
});
test("settings changes persist after page reload", async ({ page }) => {
const cycleLengthInput = page.getByLabel(/cycle length/i);
const isVisible = await cycleLengthInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Get current value
const originalValue = await cycleLengthInput.inputValue();
// Set a different valid value
const newValue = originalValue === "28" ? "30" : "28";
await cycleLengthInput.fill(newValue);
// Save
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
await page.waitForTimeout(1500);
// Reload the page
await page.reload();
await page.waitForLoadState("networkidle");
// Check the value persisted
const cycleLengthAfter = page.getByLabel(/cycle length/i);
const afterValue = await cycleLengthAfter.inputValue();
// Either it persisted or was rejected - check it's a valid number
const numValue = Number.parseInt(afterValue, 10);
expect(numValue).toBeGreaterThanOrEqual(21);
expect(numValue).toBeLessThanOrEqual(45);
// Restore original value
await cycleLengthAfter.fill(originalValue);
await saveButton.click();
await page.waitForTimeout(500);
});
test("save button shows loading state during submission", async ({
page,
}) => {
const saveButton = page.getByRole("button", { name: /save/i });
const isVisible = await saveButton.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Initial state should not be disabled
const isDisabledBefore = await saveButton.isDisabled();
expect(isDisabledBefore).toBe(false);
// Click save and quickly check for loading state
await saveButton.click();
// Wait for submission to complete
await page.waitForTimeout(1000);
// After completion, button should be enabled again
const isDisabledAfter = await saveButton.isDisabled();
expect(isDisabledAfter).toBe(false);
});
test("notification time changes persist after page reload", async ({
page,
}) => {
const notificationTimeInput = page.getByLabel(/notification time/i);
const isVisible = await notificationTimeInput
.isVisible()
.catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Get current value
const originalValue = await notificationTimeInput.inputValue();
// Set a different valid time (toggle between 08:00 and 09:00)
const newValue = originalValue === "08:00" ? "09:00" : "08:00";
await notificationTimeInput.fill(newValue);
// Save and wait for success toast
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
// Reload the page
await page.reload();
await page.waitForLoadState("networkidle");
// Check the value persisted
const notificationTimeAfter = page.getByLabel(/notification time/i);
const afterValue = await notificationTimeAfter.inputValue();
expect(afterValue).toBe(newValue);
// Restore original value
await notificationTimeAfter.fill(originalValue);
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
});
test("timezone changes persist after page reload", async ({ page }) => {
const timezoneInput = page.getByLabel(/timezone/i);
const isVisible = await timezoneInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Get current value
const originalValue = await timezoneInput.inputValue();
// Set a different timezone (toggle between two common timezones)
const newValue =
originalValue === "America/New_York"
? "America/Los_Angeles"
: "America/New_York";
await timezoneInput.fill(newValue);
// Save and wait for success toast
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
// Reload the page
await page.reload();
await page.waitForLoadState("networkidle");
// Check the value persisted
const timezoneAfter = page.getByLabel(/timezone/i);
const afterValue = await timezoneAfter.inputValue();
expect(afterValue).toBe(newValue);
// Restore original value
await timezoneAfter.fill(originalValue);
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
});
test("multiple settings changes persist after page reload", async ({
page,
}) => {
const cycleLengthInput = page.getByLabel(/cycle length/i);
const notificationTimeInput = page.getByLabel(/notification time/i);
const timezoneInput = page.getByLabel(/timezone/i);
const cycleLengthVisible = await cycleLengthInput
.isVisible()
.catch(() => false);
const notificationTimeVisible = await notificationTimeInput
.isVisible()
.catch(() => false);
const timezoneVisible = await timezoneInput
.isVisible()
.catch(() => false);
if (!cycleLengthVisible || !notificationTimeVisible || !timezoneVisible) {
test.skip();
return;
}
// Get all original values
const originalCycleLength = await cycleLengthInput.inputValue();
const originalNotificationTime = await notificationTimeInput.inputValue();
const originalTimezone = await timezoneInput.inputValue();
// Set different values for all fields
const newCycleLength = originalCycleLength === "28" ? "30" : "28";
const newNotificationTime =
originalNotificationTime === "08:00" ? "09:00" : "08:00";
const newTimezone =
originalTimezone === "America/New_York"
? "America/Los_Angeles"
: "America/New_York";
await cycleLengthInput.fill(newCycleLength);
await notificationTimeInput.fill(newNotificationTime);
await timezoneInput.fill(newTimezone);
// Save all changes at once and wait for success toast
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
// Reload the page
await page.reload();
await page.waitForLoadState("networkidle");
// Verify all values persisted
const cycleLengthAfter = page.getByLabel(/cycle length/i);
const notificationTimeAfter = page.getByLabel(/notification time/i);
const timezoneAfter = page.getByLabel(/timezone/i);
expect(await cycleLengthAfter.inputValue()).toBe(newCycleLength);
expect(await notificationTimeAfter.inputValue()).toBe(
newNotificationTime,
);
expect(await timezoneAfter.inputValue()).toBe(newTimezone);
// Restore all original values
await cycleLengthAfter.fill(originalCycleLength);
await notificationTimeAfter.fill(originalNotificationTime);
await timezoneAfter.fill(originalTimezone);
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
});
test("cycle length persistence verifies exact saved value", async ({
page,
}) => {
const cycleLengthInput = page.getByLabel(/cycle length/i);
const isVisible = await cycleLengthInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Get current value
const originalValue = await cycleLengthInput.inputValue();
// Set a specific different valid value
const newValue = originalValue === "28" ? "31" : "28";
await cycleLengthInput.fill(newValue);
// Save and wait for success toast
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
// Reload the page
await page.reload();
await page.waitForLoadState("networkidle");
// Check the exact value persisted (not just range validation)
const cycleLengthAfter = page.getByLabel(/cycle length/i);
const afterValue = await cycleLengthAfter.inputValue();
expect(afterValue).toBe(newValue);
// Restore original value
await cycleLengthAfter.fill(originalValue);
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
});
test("settings form shows correct values after save without reload", async ({
page,
}) => {
const cycleLengthInput = page.getByLabel(/cycle length/i);
const notificationTimeInput = page.getByLabel(/notification time/i);
const cycleLengthVisible = await cycleLengthInput
.isVisible()
.catch(() => false);
const notificationTimeVisible = await notificationTimeInput
.isVisible()
.catch(() => false);
if (!cycleLengthVisible || !notificationTimeVisible) {
test.skip();
return;
}
// Get original values
const originalCycleLength = await cycleLengthInput.inputValue();
const originalNotificationTime = await notificationTimeInput.inputValue();
// Change values
const newCycleLength = originalCycleLength === "28" ? "29" : "28";
const newNotificationTime =
originalNotificationTime === "08:00" ? "10:00" : "08:00";
await cycleLengthInput.fill(newCycleLength);
await notificationTimeInput.fill(newNotificationTime);
// Save
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
await page.waitForTimeout(1500);
// Verify values are still showing the new values without reload
expect(await cycleLengthInput.inputValue()).toBe(newCycleLength);
expect(await notificationTimeInput.inputValue()).toBe(
newNotificationTime,
);
// Restore original values
await cycleLengthInput.fill(originalCycleLength);
await notificationTimeInput.fill(originalNotificationTime);
await saveButton.click();
await page.waitForTimeout(500);
});
});
test.describe("error recovery", () => {
test.beforeEach(async ({ page }) => {
const email = process.env.TEST_USER_EMAIL;
const password = process.env.TEST_USER_PASSWORD;
if (!email || !password) {
test.skip();
return;
}
await page.goto("/login");
await page.waitForLoadState("networkidle");
const emailInput = page.getByLabel(/email/i);
const hasEmailForm = await emailInput.isVisible().catch(() => false);
if (!hasEmailForm) {
test.skip();
return;
}
await emailInput.fill(email);
await page.getByLabel(/password/i).fill(password);
await page.getByRole("button", { name: /sign in/i }).click();
await page.waitForURL("/", { timeout: 10000 });
await page.goto("/settings");
await page.waitForLoadState("networkidle");
});
test("shows error message and allows retry when save fails", async ({
page,
}) => {
const cycleLengthInput = page.getByLabel(/cycle length/i);
const isVisible = await cycleLengthInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Get original value for restoration
const originalValue = await cycleLengthInput.inputValue();
// Intercept the save request and make it fail once, then succeed
let failureCount = 0;
await page.route("**/api/user", async (route) => {
if (route.request().method() === "PATCH") {
if (failureCount === 0) {
failureCount++;
// First request fails with server error
await route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ error: "Server error" }),
});
} else {
// Subsequent requests succeed - let them through
await route.continue();
}
} else {
await route.continue();
}
});
// Change the cycle length
const newValue = originalValue === "28" ? "32" : "28";
await cycleLengthInput.fill(newValue);
// Click save - should fail
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
// Wait for error handling to complete
await page.waitForTimeout(1000);
// The key test is that the form remains usable after a failed save
// Error handling may show a toast or just keep the form editable
// Verify form is still editable (not stuck in loading state)
const isEditable = await cycleLengthInput.isEditable();
expect(isEditable).toBe(true);
// Verify save button is enabled for retry
const isButtonEnabled = !(await saveButton.isDisabled());
expect(isButtonEnabled).toBe(true);
// Try saving again - should succeed this time
await saveButton.click();
await page.waitForTimeout(1500);
// Form should still be functional
const isEditableAfterRetry = await cycleLengthInput.isEditable();
expect(isEditableAfterRetry).toBe(true);
// Clean up route interception
await page.unroute("**/api/user");
// Restore original value
await cycleLengthInput.fill(originalValue);
await saveButton.click();
await page.waitForTimeout(500);
});
});
test.describe("intensity goals section", () => {
test.beforeEach(async ({ page }) => {
const email = process.env.TEST_USER_EMAIL;
const password = process.env.TEST_USER_PASSWORD;
if (!email || !password) {
test.skip();
return;
}
await page.goto("/login");
await page.waitForLoadState("networkidle");
const emailInput = page.getByLabel(/email/i);
const hasEmailForm = await emailInput.isVisible().catch(() => false);
if (!hasEmailForm) {
test.skip();
return;
}
await emailInput.fill(email);
await page.getByLabel(/password/i).fill(password);
await page.getByRole("button", { name: /sign in/i }).click();
await page.waitForURL("/", { timeout: 10000 });
await page.goto("/settings");
await page.waitForLoadState("networkidle");
});
test("displays Weekly Intensity Goals section", async ({ page }) => {
const sectionHeading = page.getByRole("heading", {
name: /weekly intensity goals/i,
});
await expect(sectionHeading).toBeVisible();
});
test("displays input for menstrual phase goal", async ({ page }) => {
const menstrualInput = page.getByLabel(/menstrual/i);
await expect(menstrualInput).toBeVisible();
});
test("displays input for follicular phase goal", async ({ page }) => {
const follicularInput = page.getByLabel(/follicular/i);
await expect(follicularInput).toBeVisible();
});
test("displays input for ovulation phase goal", async ({ page }) => {
const ovulationInput = page.getByLabel(/ovulation/i);
await expect(ovulationInput).toBeVisible();
});
test("displays input for early luteal phase goal", async ({ page }) => {
const earlyLutealInput = page.getByLabel(/early luteal/i);
await expect(earlyLutealInput).toBeVisible();
});
test("displays input for late luteal phase goal", async ({ page }) => {
const lateLutealInput = page.getByLabel(/late luteal/i);
await expect(lateLutealInput).toBeVisible();
});
test("can modify menstrual phase goal and save", async ({ page }) => {
const menstrualInput = page.getByLabel(/menstrual/i);
const isVisible = await menstrualInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Get original value
const originalValue = await menstrualInput.inputValue();
// Set a different value
const newValue = originalValue === "75" ? "80" : "75";
await menstrualInput.fill(newValue);
// Save and wait for success toast
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
// Restore original value
await menstrualInput.fill(originalValue);
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
});
test("persists intensity goal value after page reload", async ({
page,
}) => {
const menstrualInput = page.getByLabel(/menstrual/i);
const isVisible = await menstrualInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Get original value
const originalValue = await menstrualInput.inputValue();
// Set a different value
const newValue = originalValue === "75" ? "85" : "75";
await menstrualInput.fill(newValue);
// Save and wait for success toast
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
// Reload the page
await page.reload();
await page.waitForLoadState("networkidle");
// Check the value persisted
const menstrualAfter = page.getByLabel(/menstrual/i);
const afterValue = await menstrualAfter.inputValue();
expect(afterValue).toBe(newValue);
// Restore original value
await menstrualAfter.fill(originalValue);
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
});
test("intensity goal inputs have number type and min attribute", async ({
page,
}) => {
const menstrualInput = page.getByLabel(/menstrual/i);
const isVisible = await menstrualInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Check type attribute
const inputType = await menstrualInput.getAttribute("type");
expect(inputType).toBe("number");
// Check min attribute
const inputMin = await menstrualInput.getAttribute("min");
expect(inputMin).toBe("0");
});
test("all intensity goal inputs are disabled while saving", async ({
page,
}) => {
const menstrualInput = page.getByLabel(/menstrual/i);
const isVisible = await menstrualInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Start saving (slow down the response to catch disabled state)
await page.route("**/api/user", async (route) => {
if (route.request().method() === "PATCH") {
// Delay response to allow testing disabled state
await new Promise((resolve) => setTimeout(resolve, 500));
await route.continue();
} else {
await route.continue();
}
});
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
// Check inputs are disabled during save
await expect(menstrualInput).toBeDisabled();
// Wait for save to complete
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
// Clean up route interception
await page.unroute("**/api/user");
});
});
});