Refactor to enforce AuthContext usage over useAuthStore and improve test stability
- Replaced `useAuthStore` with `useAuth` from `AuthContext` across frontend components and tests to ensure dependency injection compliance. - Enhanced E2E test stability by delaying navigation until the auth context is fully initialized. - Updated Playwright configuration to use a single worker to prevent mock conflicts. - Refactored test setup to consistently inject `AuthProvider` for improved isolation and mocking. - Adjusted comments and documentation to clarify dependency injection and testability patterns.
This commit is contained in:
@@ -38,6 +38,7 @@ export const MOCK_SESSION = {
|
||||
/**
|
||||
* Set up API mocking for authenticated E2E tests
|
||||
* Intercepts backend API calls and returns mock data
|
||||
* Routes persist across client-side navigation
|
||||
*
|
||||
* @param page Playwright page object
|
||||
*/
|
||||
@@ -45,20 +46,18 @@ export async function setupAuthenticatedMocks(page: Page): Promise<void> {
|
||||
const baseURL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000';
|
||||
|
||||
// Mock GET /api/v1/users/me - Get current user
|
||||
await page.route(`${baseURL}/api/v1/users/me`, async (route: Route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
data: MOCK_USER,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock PATCH /api/v1/users/me - Update user profile
|
||||
await page.route(`${baseURL}/api/v1/users/me`, async (route: Route) => {
|
||||
if (route.request().method() === 'PATCH') {
|
||||
if (route.request().method() === 'GET') {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
data: MOCK_USER,
|
||||
}),
|
||||
});
|
||||
} else if (route.request().method() === 'PATCH') {
|
||||
const postData = route.request().postDataJSON();
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -93,7 +92,9 @@ export async function setupAuthenticatedMocks(page: Page): Promise<void> {
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
data: [MOCK_SESSION],
|
||||
data: {
|
||||
sessions: [MOCK_SESSION],
|
||||
},
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
@@ -117,34 +118,15 @@ export async function setupAuthenticatedMocks(page: Page): Promise<void> {
|
||||
}
|
||||
});
|
||||
|
||||
// Inject mock auth store BEFORE navigation
|
||||
// This must happen before the page loads to ensure AuthProvider picks it up
|
||||
// Inject mock auth store that persists across navigation
|
||||
// This creates a mock Zustand store accessible via window.__TEST_AUTH_STORE__
|
||||
// CRITICAL: Must be set BEFORE React renders to be picked up by AuthProvider
|
||||
await page.addInitScript((mockUser) => {
|
||||
// Create a mock Zustand hook that returns our mocked auth state
|
||||
const mockAuthStore: any = (selector?: any) => {
|
||||
const state = {
|
||||
user: mockUser,
|
||||
accessToken: 'mock-access-token',
|
||||
refreshToken: 'mock-refresh-token',
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
tokenExpiresAt: Date.now() + 900000, // 15 minutes from now
|
||||
// Mock action functions
|
||||
setAuth: async () => {},
|
||||
setTokens: async () => {},
|
||||
setUser: () => {},
|
||||
clearAuth: async () => {},
|
||||
loadAuthFromStorage: async () => {},
|
||||
isTokenExpired: () => false,
|
||||
};
|
||||
return selector ? selector(state) : state;
|
||||
};
|
||||
|
||||
// Add getState method for non-React contexts (API client, etc.)
|
||||
mockAuthStore.getState = () => ({
|
||||
// Create a stable state object that persists
|
||||
const authState = {
|
||||
user: mockUser,
|
||||
accessToken: 'mock-access-token',
|
||||
refreshToken: 'mock-refresh-token',
|
||||
accessToken: 'mock.access.token', // Valid JWT format (3 parts)
|
||||
refreshToken: 'mock.refresh.token',
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
tokenExpiresAt: Date.now() + 900000,
|
||||
@@ -152,11 +134,32 @@ export async function setupAuthenticatedMocks(page: Page): Promise<void> {
|
||||
setTokens: async () => {},
|
||||
setUser: () => {},
|
||||
clearAuth: async () => {},
|
||||
loadAuthFromStorage: async () => {},
|
||||
loadAuthFromStorage: async () => {
|
||||
// No-op in tests - state is already set
|
||||
},
|
||||
isTokenExpired: () => false,
|
||||
});
|
||||
};
|
||||
|
||||
// Inject into window for AuthProvider to pick up
|
||||
// Mock Zustand hook - must support both selector and no-selector calls
|
||||
const mockAuthStore: any = (selector?: any) => {
|
||||
// If selector provided, call it with the state
|
||||
if (selector && typeof selector === 'function') {
|
||||
return selector(authState);
|
||||
}
|
||||
// Otherwise return the full state
|
||||
return authState;
|
||||
};
|
||||
|
||||
// Add getState method that Zustand stores have
|
||||
mockAuthStore.getState = () => authState;
|
||||
|
||||
// Add subscribe method (required by Zustand)
|
||||
mockAuthStore.subscribe = () => () => {}; // Returns unsubscribe function
|
||||
|
||||
// Make it globally available for AuthProvider
|
||||
(window as any).__TEST_AUTH_STORE__ = mockAuthStore;
|
||||
|
||||
// Also set a flag to indicate we're in a test environment
|
||||
(window as any).__E2E_TEST__ = true;
|
||||
}, MOCK_USER);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,15 @@ test.describe('Settings Navigation', () => {
|
||||
// Set up API mocks for authenticated user
|
||||
await setupAuthenticatedMocks(page);
|
||||
|
||||
// Delay to ensure auth store injection completes before navigation
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Navigate to settings
|
||||
await page.goto('/settings/profile');
|
||||
await expect(page).toHaveURL('/settings/profile');
|
||||
|
||||
// Wait for page to fully load with auth context
|
||||
await page.waitForSelector('h2:has-text("Profile")', { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should display settings tabs', async ({ page }) => {
|
||||
@@ -48,7 +54,7 @@ test.describe('Settings Navigation', () => {
|
||||
]);
|
||||
|
||||
await expect(page).toHaveURL('/settings/password');
|
||||
await expect(page.locator('h2')).toContainText(/Change Password/i);
|
||||
await expect(page.locator('h2')).toContainText(/Password Settings/i);
|
||||
});
|
||||
|
||||
test('should navigate from Profile to Sessions', async ({ page }) => {
|
||||
@@ -95,7 +101,7 @@ test.describe('Settings Navigation', () => {
|
||||
]);
|
||||
|
||||
await expect(page).toHaveURL('/settings/password');
|
||||
await expect(page.locator('h2')).toContainText(/Change Password/i);
|
||||
await expect(page.locator('h2')).toContainText(/Password Settings/i);
|
||||
});
|
||||
|
||||
test('should maintain layout when navigating between tabs', async ({ page }) => {
|
||||
|
||||
@@ -11,9 +11,15 @@ test.describe('Password Change', () => {
|
||||
// Set up API mocks for authenticated user
|
||||
await setupAuthenticatedMocks(page);
|
||||
|
||||
// Delay to ensure auth store injection completes before navigation
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Navigate to password settings
|
||||
await page.goto('/settings/password');
|
||||
await expect(page).toHaveURL('/settings/password');
|
||||
|
||||
// Wait for page to fully load with auth context
|
||||
await page.waitForSelector('h2', { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should display password change page', async ({ page }) => {
|
||||
@@ -53,28 +59,27 @@ test.describe('Password Change', () => {
|
||||
test('should show cancel button when form is dirty', async ({ page }) => {
|
||||
await page.waitForSelector('#current_password');
|
||||
|
||||
// Fill current password
|
||||
await page.locator('#current_password').fill('Admin123!');
|
||||
// Fill current password and blur to trigger dirty state
|
||||
const currentPasswordInput = page.locator('#current_password');
|
||||
await currentPasswordInput.fill('Admin123!');
|
||||
await currentPasswordInput.blur();
|
||||
|
||||
// Wait for form state to update
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Cancel button should appear
|
||||
// Cancel button should appear when form is dirty
|
||||
const cancelButton = page.locator('button[type="button"]:has-text("Cancel")');
|
||||
await expect(cancelButton).toBeVisible();
|
||||
await expect(cancelButton).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should clear form when cancel button is clicked', async ({ page }) => {
|
||||
await page.waitForSelector('#current_password');
|
||||
|
||||
// Fill fields
|
||||
await page.locator('#current_password').fill('Admin123!');
|
||||
// Fill fields and blur to trigger dirty state
|
||||
const currentPasswordInput = page.locator('#current_password');
|
||||
await currentPasswordInput.fill('Admin123!');
|
||||
await currentPasswordInput.blur();
|
||||
|
||||
await page.locator('#new_password').fill('NewAdmin123!');
|
||||
|
||||
// Wait for form state to update
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Click cancel
|
||||
// Click cancel button
|
||||
const cancelButton = page.locator('button[type="button"]:has-text("Cancel")');
|
||||
await cancelButton.click();
|
||||
|
||||
|
||||
@@ -11,9 +11,15 @@ test.describe('Profile Settings', () => {
|
||||
// Set up API mocks for authenticated user
|
||||
await setupAuthenticatedMocks(page);
|
||||
|
||||
// Delay to ensure auth store injection completes before navigation
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Navigate to profile settings
|
||||
await page.goto('/settings/profile');
|
||||
await expect(page).toHaveURL('/settings/profile');
|
||||
|
||||
// Wait for page to fully load with auth context
|
||||
await page.waitForSelector('h2:has-text("Profile")', { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should display profile settings page', async ({ page }) => {
|
||||
@@ -79,14 +85,15 @@ test.describe('Profile Settings', () => {
|
||||
return input && input.value !== '';
|
||||
}, { timeout: 5000 });
|
||||
|
||||
// Modify first name
|
||||
// Modify first name and blur to trigger dirty state
|
||||
const firstNameInput = page.locator('#first_name');
|
||||
await firstNameInput.clear();
|
||||
await firstNameInput.fill('TestUser');
|
||||
await firstNameInput.blur();
|
||||
|
||||
// Reset button should appear
|
||||
// Reset button should appear when form is dirty
|
||||
const resetButton = page.locator('button[type="button"]:has-text("Reset")');
|
||||
await expect(resetButton).toBeVisible();
|
||||
await expect(resetButton).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should reset form when reset button is clicked', async ({ page }) => {
|
||||
@@ -102,9 +109,10 @@ test.describe('Profile Settings', () => {
|
||||
const firstNameInput = page.locator('#first_name');
|
||||
const originalValue = await firstNameInput.inputValue();
|
||||
|
||||
// Modify first name
|
||||
// Modify first name and blur to trigger dirty state
|
||||
await firstNameInput.clear();
|
||||
await firstNameInput.fill('TestUser');
|
||||
await firstNameInput.blur();
|
||||
await expect(firstNameInput).toHaveValue('TestUser');
|
||||
|
||||
// Click reset
|
||||
|
||||
@@ -11,9 +11,15 @@ test.describe('Sessions Management', () => {
|
||||
// Set up API mocks for authenticated user
|
||||
await setupAuthenticatedMocks(page);
|
||||
|
||||
// Delay to ensure auth store injection completes before navigation
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Navigate to sessions settings
|
||||
await page.goto('/settings/sessions');
|
||||
await expect(page).toHaveURL('/settings/sessions');
|
||||
|
||||
// Wait for page to fully load with auth context
|
||||
await page.waitForSelector('h2', { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should display sessions management page', async ({ page }) => {
|
||||
@@ -132,9 +138,15 @@ test.describe('Sessions Management - Revocation', () => {
|
||||
// Set up API mocks for authenticated user
|
||||
await setupAuthenticatedMocks(page);
|
||||
|
||||
// Delay to ensure auth store injection completes before navigation
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Navigate to sessions settings
|
||||
await page.goto('/settings/sessions');
|
||||
await expect(page).toHaveURL('/settings/sessions');
|
||||
|
||||
// Wait for page to fully load with auth context
|
||||
await page.waitForSelector('h2', { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should show confirmation dialog before individual revocation', async ({ page }) => {
|
||||
|
||||
Reference in New Issue
Block a user