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.
This commit is contained in:
@@ -117,30 +117,46 @@ export async function setupAuthenticatedMocks(page: Page): Promise<void> {
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
17
frontend/e2e/test-selectors.spec.ts
Normal file
17
frontend/e2e/test-selectors.spec.ts
Normal file
@@ -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}"`);
|
||||
}
|
||||
});
|
||||
@@ -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<typeof useAuthStore>;
|
||||
// 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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,10 +13,6 @@ jest.mock('@/components/theme', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/auth', () => ({
|
||||
AuthInitializer: () => <div data-testid="auth-initializer" />,
|
||||
}));
|
||||
|
||||
// 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(
|
||||
<Providers>
|
||||
<div>Test Content</div>
|
||||
</Providers>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('auth-initializer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders children', () => {
|
||||
render(
|
||||
<Providers>
|
||||
|
||||
Reference in New Issue
Block a user