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:
@@ -9,9 +9,29 @@ import type { User } from "@/types";
|
||||
// Module-level variable to control mock user in tests
|
||||
let currentMockUser: User | null = null;
|
||||
|
||||
// Create mock PocketBase getOne function that returns fresh user data
|
||||
const mockPbGetOne = vi.fn().mockImplementation(() => {
|
||||
if (!currentMockUser) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
return Promise.resolve({
|
||||
id: currentMockUser.id,
|
||||
email: currentMockUser.email,
|
||||
lastPeriodDate: currentMockUser.lastPeriodDate?.toISOString(),
|
||||
cycleLength: currentMockUser.cycleLength,
|
||||
});
|
||||
});
|
||||
|
||||
// Create mock PocketBase client
|
||||
const mockPb = {
|
||||
collection: vi.fn(() => ({
|
||||
getOne: mockPbGetOne,
|
||||
})),
|
||||
};
|
||||
|
||||
// Mock PocketBase client for database operations
|
||||
vi.mock("@/lib/pocketbase", () => ({
|
||||
createPocketBaseClient: vi.fn(() => ({})),
|
||||
createPocketBaseClient: vi.fn(() => mockPb),
|
||||
loadAuthFromCookies: vi.fn(),
|
||||
isAuthenticated: vi.fn(() => currentMockUser !== null),
|
||||
getCurrentUser: vi.fn(() => currentMockUser),
|
||||
@@ -24,7 +44,7 @@ vi.mock("@/lib/auth-middleware", () => ({
|
||||
if (!currentMockUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
return handler(request, currentMockUser);
|
||||
return handler(request, currentMockUser, mockPb);
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -40,9 +40,18 @@ function getDaysUntilNextPhase(cycleDay: number, cycleLength: number): number {
|
||||
return nextPhaseStart - cycleDay;
|
||||
}
|
||||
|
||||
export const GET = withAuth(async (_request, user) => {
|
||||
export const GET = withAuth(async (_request, user, pb) => {
|
||||
// Fetch fresh user data from database to get latest values
|
||||
// The user param from withAuth is from auth store cache which may be stale
|
||||
const freshUser = await pb.collection("users").getOne(user.id);
|
||||
|
||||
// Validate user has required cycle data
|
||||
if (!user.lastPeriodDate) {
|
||||
const lastPeriodDate = freshUser.lastPeriodDate
|
||||
? new Date(freshUser.lastPeriodDate as string)
|
||||
: null;
|
||||
const cycleLength = (freshUser.cycleLength as number) || 28;
|
||||
|
||||
if (!lastPeriodDate) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
@@ -53,20 +62,16 @@ export const GET = withAuth(async (_request, user) => {
|
||||
}
|
||||
|
||||
// Calculate current cycle position
|
||||
const cycleDay = getCycleDay(
|
||||
user.lastPeriodDate,
|
||||
user.cycleLength,
|
||||
new Date(),
|
||||
);
|
||||
const phase = getPhase(cycleDay, user.cycleLength);
|
||||
const cycleDay = getCycleDay(lastPeriodDate, cycleLength, new Date());
|
||||
const phase = getPhase(cycleDay, cycleLength);
|
||||
const phaseConfig = getPhaseConfig(phase);
|
||||
const daysUntilNextPhase = getDaysUntilNextPhase(cycleDay, user.cycleLength);
|
||||
const daysUntilNextPhase = getDaysUntilNextPhase(cycleDay, cycleLength);
|
||||
|
||||
return NextResponse.json({
|
||||
cycleDay,
|
||||
phase,
|
||||
phaseConfig,
|
||||
daysUntilNextPhase,
|
||||
cycleLength: user.cycleLength,
|
||||
cycleLength,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,10 +12,28 @@ let currentMockUser: User | null = null;
|
||||
// Track PocketBase update calls
|
||||
const mockPbUpdate = vi.fn().mockResolvedValue({});
|
||||
|
||||
// Track PocketBase getOne calls - returns the current mock user data
|
||||
const mockPbGetOne = vi.fn().mockImplementation(() => {
|
||||
if (!currentMockUser) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
return Promise.resolve({
|
||||
id: currentMockUser.id,
|
||||
email: currentMockUser.email,
|
||||
garminConnected: currentMockUser.garminConnected,
|
||||
lastPeriodDate: currentMockUser.lastPeriodDate?.toISOString(),
|
||||
cycleLength: currentMockUser.cycleLength,
|
||||
notificationTime: currentMockUser.notificationTime,
|
||||
timezone: currentMockUser.timezone,
|
||||
activeOverrides: currentMockUser.activeOverrides,
|
||||
});
|
||||
});
|
||||
|
||||
// Create mock PocketBase client
|
||||
const mockPb = {
|
||||
collection: vi.fn(() => ({
|
||||
update: mockPbUpdate,
|
||||
getOne: mockPbGetOne,
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -55,6 +73,7 @@ describe("GET /api/user", () => {
|
||||
vi.clearAllMocks();
|
||||
currentMockUser = null;
|
||||
mockPbUpdate.mockClear();
|
||||
mockPbGetOne.mockClear();
|
||||
});
|
||||
|
||||
it("returns user profile when authenticated", async () => {
|
||||
@@ -133,6 +152,7 @@ describe("PATCH /api/user", () => {
|
||||
vi.clearAllMocks();
|
||||
currentMockUser = null;
|
||||
mockPbUpdate.mockClear();
|
||||
mockPbGetOne.mockClear();
|
||||
});
|
||||
|
||||
// Helper to create mock request with JSON body
|
||||
|
||||
@@ -13,23 +13,28 @@ const TIME_FORMAT_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
||||
/**
|
||||
* GET /api/user
|
||||
* Returns the authenticated user's profile.
|
||||
* Fetches fresh data from database to ensure updates are reflected.
|
||||
* Excludes sensitive fields like encrypted tokens.
|
||||
*/
|
||||
export const GET = withAuth(async (_request, user, _pb) => {
|
||||
export const GET = withAuth(async (_request, user, pb) => {
|
||||
// Fetch fresh user data from database to get latest values
|
||||
// The user param from withAuth is from auth store cache which may be stale
|
||||
const freshUser = await pb.collection("users").getOne(user.id);
|
||||
|
||||
// Format date for consistent API response
|
||||
const lastPeriodDate = user.lastPeriodDate
|
||||
? user.lastPeriodDate.toISOString().split("T")[0]
|
||||
const lastPeriodDate = freshUser.lastPeriodDate
|
||||
? new Date(freshUser.lastPeriodDate as string).toISOString().split("T")[0]
|
||||
: null;
|
||||
|
||||
return NextResponse.json({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
garminConnected: user.garminConnected,
|
||||
cycleLength: user.cycleLength,
|
||||
id: freshUser.id,
|
||||
email: freshUser.email,
|
||||
garminConnected: freshUser.garminConnected ?? false,
|
||||
cycleLength: freshUser.cycleLength,
|
||||
lastPeriodDate,
|
||||
notificationTime: user.notificationTime,
|
||||
timezone: user.timezone,
|
||||
activeOverrides: user.activeOverrides,
|
||||
notificationTime: freshUser.notificationTime,
|
||||
timezone: freshUser.timezone,
|
||||
activeOverrides: freshUser.activeOverrides ?? [],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user