From 7c98ceb5b9e1d7ac0c6f276cd323f61e5a9df8f8 Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Tue, 4 Nov 2025 00:32:07 +0100 Subject: [PATCH] Refactor E2E tests to use ID selectors and enhance mock auth injection - Updated E2E selectors for input fields to use stable IDs instead of `name` attributes, improving reliability and alignment with form field guarantees. - Refined mock auth state injection in Playwright to establish test store state prior to page load. - Optimized test clarity and consistency by consolidating selector logic and introducing stabilization steps where necessary. - Removed redundant `AuthInitializer` mocks and refactored related tests to align with the updated `AuthContext` pattern. - Enhanced readability and maintainability across affected test suites. --- frontend/e2e/helpers/auth.ts | 54 +++++++++------ frontend/e2e/settings-password.spec.ts | 67 +++++++++++-------- frontend/e2e/settings-profile.spec.ts | 57 +++++++++++----- frontend/e2e/test-selectors.spec.ts | 17 +++++ .../settings/profile/page.test.tsx | 34 +++++++--- frontend/tests/app/providers.test.tsx | 14 ---- 6 files changed, 152 insertions(+), 91 deletions(-) create mode 100644 frontend/e2e/test-selectors.spec.ts diff --git a/frontend/e2e/helpers/auth.ts b/frontend/e2e/helpers/auth.ts index 11d3411..d907521 100644 --- a/frontend/e2e/helpers/auth.ts +++ b/frontend/e2e/helpers/auth.ts @@ -117,30 +117,46 @@ export async function setupAuthenticatedMocks(page: Page): Promise { } }); - // Navigate to home first to set up auth state - await page.goto('/'); - - // Inject auth state directly into Zustand store - await page.evaluate((mockUser) => { - // Mock encrypted token storage - localStorage.setItem('auth_tokens', 'mock-encrypted-token'); - localStorage.setItem('auth_storage_method', 'localStorage'); - - // Find and inject into the auth store - // Zustand stores are available on window in dev mode - const stores = Object.keys(window).filter(key => key.includes('Store')); - - // Try to find useAuthStore - const authStore = (window as any).useAuthStore; - if (authStore && authStore.getState) { - authStore.setState({ + // Inject mock auth store BEFORE navigation + // This must happen before the page loads to ensure AuthProvider picks it up + 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 = () => ({ + user: mockUser, + accessToken: 'mock-access-token', + refreshToken: 'mock-refresh-token', + isAuthenticated: true, + isLoading: false, + tokenExpiresAt: Date.now() + 900000, + setAuth: async () => {}, + setTokens: async () => {}, + setUser: () => {}, + clearAuth: async () => {}, + loadAuthFromStorage: async () => {}, + isTokenExpired: () => false, + }); + + // Inject into window for AuthProvider to pick up + (window as any).__TEST_AUTH_STORE__ = mockAuthStore; }, MOCK_USER); } diff --git a/frontend/e2e/settings-password.spec.ts b/frontend/e2e/settings-password.spec.ts index 2ba0b0a..4527728 100644 --- a/frontend/e2e/settings-password.spec.ts +++ b/frontend/e2e/settings-password.spec.ts @@ -18,16 +18,16 @@ test.describe('Password Change', () => { test('should display password change page', async ({ page }) => { // Check page title - await expect(page.locator('h2')).toContainText(/Change Password/i); + await expect(page.locator('h2')).toContainText(/Password Settings/i); // Check form fields exist - await expect(page.locator('input[name="current_password"]')).toBeVisible(); - await expect(page.locator('input[name="new_password"]')).toBeVisible(); - await expect(page.locator('input[name="confirm_password"]')).toBeVisible(); + await expect(page.locator('#current_password')).toBeVisible(); + await expect(page.locator('#new_password')).toBeVisible(); + await expect(page.locator('#confirm_password')).toBeVisible(); }); test('should have submit button disabled when form is pristine', async ({ page }) => { - await page.waitForSelector('input[name="current_password"]'); + await page.waitForSelector('#current_password'); // Submit button should be disabled initially const submitButton = page.locator('button[type="submit"]'); @@ -35,12 +35,15 @@ test.describe('Password Change', () => { }); test('should enable submit button when all fields are filled', async ({ page }) => { - await page.waitForSelector('input[name="current_password"]'); + await page.waitForSelector('#current_password'); // Fill all password fields - await page.fill('input[name="current_password"]', 'Admin123!'); - await page.fill('input[name="new_password"]', 'NewAdmin123!'); - await page.fill('input[name="confirm_password"]', 'NewAdmin123!'); + await page.locator('#current_password').fill('Admin123!'); + await page.locator('#new_password').fill('NewAdmin123!'); + await page.locator('#confirm_password').fill('NewAdmin123!'); + + // Wait a bit for form state to update + await page.waitForTimeout(100); // Submit button should be enabled const submitButton = page.locator('button[type="submit"]'); @@ -48,10 +51,13 @@ test.describe('Password Change', () => { }); test('should show cancel button when form is dirty', async ({ page }) => { - await page.waitForSelector('input[name="current_password"]'); + await page.waitForSelector('#current_password'); // Fill current password - await page.fill('input[name="current_password"]', 'Admin123!'); + await page.locator('#current_password').fill('Admin123!'); + + // Wait for form state to update + await page.waitForTimeout(100); // Cancel button should appear const cancelButton = page.locator('button[type="button"]:has-text("Cancel")'); @@ -59,19 +65,22 @@ test.describe('Password Change', () => { }); test('should clear form when cancel button is clicked', async ({ page }) => { - await page.waitForSelector('input[name="current_password"]'); + await page.waitForSelector('#current_password'); // Fill fields - await page.fill('input[name="current_password"]', 'Admin123!'); - await page.fill('input[name="new_password"]', 'NewAdmin123!'); + await page.locator('#current_password').fill('Admin123!'); + await page.locator('#new_password').fill('NewAdmin123!'); + + // Wait for form state to update + await page.waitForTimeout(100); // Click cancel const cancelButton = page.locator('button[type="button"]:has-text("Cancel")'); await cancelButton.click(); // Fields should be cleared - await expect(page.locator('input[name="current_password"]')).toHaveValue(''); - await expect(page.locator('input[name="new_password"]')).toHaveValue(''); + await expect(page.locator('#current_password')).toHaveValue(''); + await expect(page.locator('#new_password')).toHaveValue(''); }); test('should show password strength requirements', async ({ page }) => { @@ -80,12 +89,12 @@ test.describe('Password Change', () => { }); test('should show validation error for weak password', async ({ page }) => { - await page.waitForSelector('input[name="new_password"]'); + await page.waitForSelector('#new_password'); // Fill with weak password - await page.fill('input[name="current_password"]', 'Admin123!'); - await page.fill('input[name="new_password"]', 'weak'); - await page.fill('input[name="confirm_password"]', 'weak'); + await page.fill('#current_password', 'Admin123!'); + await page.fill('#new_password', 'weak'); + await page.fill('#confirm_password', 'weak'); // Try to submit const submitButton = page.locator('button[type="submit"]'); @@ -98,12 +107,12 @@ test.describe('Password Change', () => { }); test('should show error when passwords do not match', async ({ page }) => { - await page.waitForSelector('input[name="new_password"]'); + await page.waitForSelector('#new_password'); // Fill with mismatched passwords - await page.fill('input[name="current_password"]', 'Admin123!'); - await page.fill('input[name="new_password"]', 'NewAdmin123!'); - await page.fill('input[name="confirm_password"]', 'DifferentPassword123!'); + await page.fill('#current_password', 'Admin123!'); + await page.fill('#new_password', 'NewAdmin123!'); + await page.fill('#confirm_password', 'DifferentPassword123!'); // Tab away to trigger validation await page.keyboard.press('Tab'); @@ -120,16 +129,16 @@ test.describe('Password Change', () => { test('should have password inputs with correct type', async ({ page }) => { // All password fields should have type="password" - await expect(page.locator('input[name="current_password"]')).toHaveAttribute('type', 'password'); - await expect(page.locator('input[name="new_password"]')).toHaveAttribute('type', 'password'); - await expect(page.locator('input[name="confirm_password"]')).toHaveAttribute('type', 'password'); + await expect(page.locator('#current_password')).toHaveAttribute('type', 'password'); + await expect(page.locator('#new_password')).toHaveAttribute('type', 'password'); + await expect(page.locator('#confirm_password')).toHaveAttribute('type', 'password'); }); test('should display card title for password change', async ({ page }) => { - await expect(page.locator('text=Change Password')).toBeVisible(); + await expect(page.locator('text=Change Password').first()).toBeVisible(); }); test('should show description about keeping account secure', async ({ page }) => { - await expect(page.locator('text=/keep your account secure/i')).toBeVisible(); + await expect(page.locator('text=/keep your account secure/i').first()).toBeVisible(); }); }); diff --git a/frontend/e2e/settings-profile.spec.ts b/frontend/e2e/settings-profile.spec.ts index 82daf3e..d31986b 100644 --- a/frontend/e2e/settings-profile.spec.ts +++ b/frontend/e2e/settings-profile.spec.ts @@ -20,31 +20,31 @@ test.describe('Profile Settings', () => { // Check page title await expect(page.locator('h2')).toContainText('Profile'); - // Check form fields exist - await expect(page.locator('input[name="first_name"]')).toBeVisible(); - await expect(page.locator('input[name="last_name"]')).toBeVisible(); - await expect(page.locator('input[name="email"]')).toBeVisible(); + // Check form fields exist (using ID selectors which are guaranteed by FormField) + await expect(page.locator('#first_name')).toBeVisible(); + await expect(page.locator('#last_name')).toBeVisible(); + await expect(page.locator('#email')).toBeVisible(); }); test('should pre-populate form with current user data', async ({ page }) => { // Wait for form to load - await page.waitForSelector('input[name="first_name"]'); + await page.waitForSelector('#first_name'); // Check that fields are populated - const firstName = await page.locator('input[name="first_name"]').inputValue(); - const email = await page.locator('input[name="email"]').inputValue(); + const firstName = await page.locator('#first_name').inputValue(); + const email = await page.locator('#email').inputValue(); expect(firstName).toBeTruthy(); expect(email).toBeTruthy(); }); test('should have email field disabled', async ({ page }) => { - const emailInput = page.locator('input[name="email"]'); + const emailInput = page.locator('#email'); await expect(emailInput).toBeDisabled(); }); test('should show submit button disabled when form is pristine', async ({ page }) => { - await page.waitForSelector('input[name="first_name"]'); + await page.waitForSelector('#first_name'); // Submit button should be disabled initially const submitButton = page.locator('button[type="submit"]'); @@ -52,10 +52,17 @@ test.describe('Profile Settings', () => { }); test('should enable submit button when first name is modified', async ({ page }) => { - await page.waitForSelector('input[name="first_name"]'); + await page.waitForSelector('#first_name'); + + // Wait for form to be populated with user data + await page.waitForFunction(() => { + const input = document.querySelector('#first_name') as HTMLInputElement; + return input && input.value !== ''; + }, { timeout: 5000 }); // Modify first name - const firstNameInput = page.locator('input[name="first_name"]'); + const firstNameInput = page.locator('#first_name'); + await firstNameInput.clear(); await firstNameInput.fill('TestUser'); // Submit button should be enabled @@ -64,10 +71,17 @@ test.describe('Profile Settings', () => { }); test('should show reset button when form is dirty', async ({ page }) => { - await page.waitForSelector('input[name="first_name"]'); + await page.waitForSelector('#first_name'); + + // Wait for form to be populated with user data + await page.waitForFunction(() => { + const input = document.querySelector('#first_name') as HTMLInputElement; + return input && input.value !== ''; + }, { timeout: 5000 }); // Modify first name - const firstNameInput = page.locator('input[name="first_name"]'); + const firstNameInput = page.locator('#first_name'); + await firstNameInput.clear(); await firstNameInput.fill('TestUser'); // Reset button should appear @@ -76,13 +90,20 @@ test.describe('Profile Settings', () => { }); test('should reset form when reset button is clicked', async ({ page }) => { - await page.waitForSelector('input[name="first_name"]'); + await page.waitForSelector('#first_name'); + + // Wait for form to be populated with user data + await page.waitForFunction(() => { + const input = document.querySelector('#first_name') as HTMLInputElement; + return input && input.value !== ''; + }, { timeout: 5000 }); // Get original value - const firstNameInput = page.locator('input[name="first_name"]'); + const firstNameInput = page.locator('#first_name'); const originalValue = await firstNameInput.inputValue(); // Modify first name + await firstNameInput.clear(); await firstNameInput.fill('TestUser'); await expect(firstNameInput).toHaveValue('TestUser'); @@ -95,10 +116,10 @@ test.describe('Profile Settings', () => { }); test('should show validation error for empty first name', async ({ page }) => { - await page.waitForSelector('input[name="first_name"]'); + await page.waitForSelector('#first_name'); // Clear first name - const firstNameInput = page.locator('input[name="first_name"]'); + const firstNameInput = page.locator('#first_name'); await firstNameInput.fill(''); // Tab away to trigger validation @@ -115,7 +136,7 @@ test.describe('Profile Settings', () => { }); test('should display profile information card title', async ({ page }) => { - await expect(page.locator('text=Profile Information')).toBeVisible(); + await expect(page.getByText('Profile Information', { exact: true })).toBeVisible(); }); test('should show description about email being read-only', async ({ page }) => { diff --git a/frontend/e2e/test-selectors.spec.ts b/frontend/e2e/test-selectors.spec.ts new file mode 100644 index 0000000..c656496 --- /dev/null +++ b/frontend/e2e/test-selectors.spec.ts @@ -0,0 +1,17 @@ +import { test } from '@playwright/test'; +import { setupAuthenticatedMocks } from './helpers/auth'; + +test('debug selectors', async ({ page }) => { + await setupAuthenticatedMocks(page); + await page.goto('/settings/profile'); + await page.waitForTimeout(2000); // Wait for render + + // Print all input elements + const inputs = await page.locator('input').all(); + for (const input of inputs) { + const name = await input.getAttribute('name'); + const id = await input.getAttribute('id'); + const type = await input.getAttribute('type'); + console.log(`Input: id="${id}", name="${name}", type="${type}"`); + } +}); diff --git a/frontend/tests/app/(authenticated)/settings/profile/page.test.tsx b/frontend/tests/app/(authenticated)/settings/profile/page.test.tsx index 729842a..81e3171 100644 --- a/frontend/tests/app/(authenticated)/settings/profile/page.test.tsx +++ b/frontend/tests/app/(authenticated)/settings/profile/page.test.tsx @@ -7,11 +7,19 @@ import { render, screen } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import ProfileSettingsPage from '@/app/(authenticated)/settings/profile/page'; import { AuthProvider } from '@/lib/auth/AuthContext'; -import { useAuthStore } from '@/lib/stores/authStore'; -// Mock authStore -jest.mock('@/lib/stores/authStore'); -const mockUseAuthStore = useAuthStore as jest.MockedFunction; +// Mock API hooks +jest.mock('@/lib/api/hooks/useAuth', () => ({ + useCurrentUser: jest.fn(), +})); + +jest.mock('@/lib/api/hooks/useUser', () => ({ + useUpdateProfile: jest.fn(), +})); + +// Import mocked hooks +import { useCurrentUser } from '@/lib/api/hooks/useAuth'; +import { useUpdateProfile } from '@/lib/api/hooks/useUser'; // Mock store hook for AuthProvider const mockStoreHook = ((selector?: (state: any) => any) => { @@ -58,14 +66,18 @@ describe('ProfileSettingsPage', () => { created_at: '2024-01-01T00:00:00Z', }; + const mockUpdateProfile = jest.fn(); + beforeEach(() => { - // Mock useAuthStore to return user data - mockUseAuthStore.mockImplementation((selector: unknown) => { - if (typeof selector === 'function') { - const mockState = { user: mockUser }; - return selector(mockState); - } - return mockUser; + jest.clearAllMocks(); + + // Mock useCurrentUser to return test user + (useCurrentUser as jest.Mock).mockReturnValue(mockUser); + + // Mock useUpdateProfile to return mutation handlers + (useUpdateProfile as jest.Mock).mockReturnValue({ + mutateAsync: mockUpdateProfile, + isPending: false, }); }); diff --git a/frontend/tests/app/providers.test.tsx b/frontend/tests/app/providers.test.tsx index 95efe91..eb9a001 100644 --- a/frontend/tests/app/providers.test.tsx +++ b/frontend/tests/app/providers.test.tsx @@ -13,10 +13,6 @@ jest.mock('@/components/theme', () => ({ ), })); -jest.mock('@/components/auth', () => ({ - AuthInitializer: () =>
, -})); - // Mock TanStack Query jest.mock('@tanstack/react-query', () => ({ QueryClient: jest.fn().mockImplementation(() => ({})), @@ -56,16 +52,6 @@ describe('Providers', () => { expect(screen.getByTestId('query-provider')).toBeInTheDocument(); }); - it('renders AuthInitializer', () => { - render( - -
Test Content
-
- ); - - expect(screen.getByTestId('auth-initializer')).toBeInTheDocument(); - }); - it('renders children', () => { render(