React Testing Strategies and Patterns
1. React Testing Library Best Practices
Practice
Description
Example
Rationale
Test User Behavior
Test components as users interact with them, not implementation details
screen.getByRole('button')
Tests are resilient to refactoring and reflect real usage
Query Priority
Use accessible queries first: getByRole, getByLabelText, getByPlaceholderText, getByText
getByRole('textbox', {name: /email/i})
Ensures accessibility and semantic HTML
Avoid Test IDs
Use data-testid only as last resort when no semantic query available
getByTestId('custom-widget')
Encourages better accessibility practices
User-Event Library
Use @testing-library/user-event for realistic interactions
await user.click(button)
Simulates real browser events more accurately than fireEvent
Async Utilities
Use waitFor, findBy queries for async operations
await screen.findByText('Loaded')
Handles async updates and avoids race conditions
Avoid act() Warnings
Use RTL's async utilities instead of manual act() wrapping
await waitFor(() => expect(...))
Built-in utilities handle act() automatically
Example: Basic RTL test with best practices
// UserGreeting.test.tsx
import { render, screen } from '@testing-library/react' ;
import userEvent from '@testing-library/user-event' ;
import UserGreeting from './UserGreeting' ;
describe ( 'UserGreeting' , () => {
it ( 'displays greeting after user enters name' , async () => {
const user = userEvent. setup ();
render (< UserGreeting />);
// Use accessible query
const input = screen. getByRole ( 'textbox' , { name: /your name/ i });
const button = screen. getByRole ( 'button' , { name: /submit/ i });
// Simulate user interaction
await user. type (input, 'Alice' );
await user. click (button);
// Assert user-visible outcome
expect ( await screen. findByText ( /hello, alice/ i )). toBeInTheDocument ();
});
});
Example: Query priority guide
// Preferred: Accessible to everyone
screen. getByRole ( 'button' , { name: /submit/ i })
screen. getByLabelText ( /email address/ i )
screen. getByPlaceholderText ( /search ... / i )
screen. getByText ( /welcome back/ i )
// Semantic queries
screen. getByAltText ( /company logo/ i )
screen. getByTitle ( /close dialog/ i )
// Test IDs (last resort)
screen. getByTestId ( 'custom-element' )
2. Component Testing and User Interactions
Test Type
Focus
Query Method
Example
Form Input
User typing and form submission
user.type(), user.click()
Type into input, submit form, verify result
Button Clicks
Click handlers and state changes
user.click()
Click button, verify UI update or API call
Async Data Loading
Fetch and display data
findBy*, waitFor()
Wait for loading state, then verify data displayed
Conditional Rendering
Show/hide elements based on state
queryBy*, expect().not.toBeInTheDocument()
Verify element appears/disappears correctly
Keyboard Navigation
Tab, Enter, Escape key handling
user.keyboard(), user.tab()
Navigate with keyboard, verify focus management
Selection Controls
Radio, checkbox, select interactions
user.selectOptions(), user.click()
Select options, verify state and behavior
// LoginForm.test.tsx
import { render, screen } from '@testing-library/react' ;
import userEvent from '@testing-library/user-event' ;
import LoginForm from './LoginForm' ;
describe ( 'LoginForm' , () => {
it ( 'shows validation errors for invalid inputs' , async () => {
const user = userEvent. setup ();
const mockSubmit = jest. fn ();
render (< LoginForm onSubmit = {mockSubmit} />);
const emailInput = screen. getByLabelText ( /email/ i );
const passwordInput = screen. getByLabelText ( /password/ i );
const submitButton = screen. getByRole ( 'button' , { name: /log in/ i });
// Submit with invalid email
await user. type (emailInput, 'invalid-email' );
await user. type (passwordInput, '123' );
await user. click (submitButton);
// Verify validation errors appear
expect ( await screen. findByText ( /invalid email format/ i )). toBeInTheDocument ();
expect ( await screen. findByText ( /password must be at least 8 characters/ i )). toBeInTheDocument ();
expect (mockSubmit).not. toHaveBeenCalled ();
});
it ( 'submits form with valid inputs' , async () => {
const user = userEvent. setup ();
const mockSubmit = jest. fn ();
render (< LoginForm onSubmit = {mockSubmit} />);
await user. type (screen. getByLabelText ( /email/ i ), 'user@example.com' );
await user. type (screen. getByLabelText ( /password/ i ), 'securePassword123' );
await user. click (screen. getByRole ( 'button' , { name: /log in/ i }));
expect (mockSubmit). toHaveBeenCalledWith ({
email: 'user@example.com' ,
password: 'securePassword123'
});
});
});
Example: Testing async data fetching
// UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react' ;
import UserProfile from './UserProfile' ;
import { fetchUser } from './api' ;
jest. mock ( './api' );
describe ( 'UserProfile' , () => {
it ( 'displays loading state then user data' , async () => {
fetchUser. mockResolvedValue ({
id: 1 ,
name: 'Alice' ,
email: 'alice@example.com'
});
render (< UserProfile userId = { 1 } />);
// Verify loading state
expect (screen. getByText ( /loading ... / i )). toBeInTheDocument ();
// Wait for data to load
expect ( await screen. findByText ( 'Alice' )). toBeInTheDocument ();
expect (screen. getByText ( 'alice@example.com' )). toBeInTheDocument ();
expect (screen. queryByText ( /loading ... / i )).not. toBeInTheDocument ();
});
it ( 'displays error message on fetch failure' , async () => {
fetchUser. mockRejectedValue ( new Error ( 'Network error' ));
render (< UserProfile userId = { 1 } />);
expect ( await screen. findByText ( /failed to load user/ i )). toBeInTheDocument ();
});
});
3. Hook Testing with renderHook
Hook Type
Testing Approach
Key Method
Example
Custom Hooks
Test hook logic in isolation
renderHook()
Test custom hook without wrapping component
State Updates
Test state changes and side effects
result.current, act()
Access hook values and trigger updates
Async Hooks
Test async operations in hooks
waitFor(() => expect(...))
Wait for async state updates
Hook Dependencies
Test effect dependencies and re-runs
rerender()
Change props and verify effect behavior
Context Hooks
Test hooks requiring context
wrapper: Provider
Wrap hook with necessary providers
Cleanup Testing
Verify cleanup functions run
unmount()
Test effect cleanup and subscriptions
Example: Testing custom hook with state
// useCounter.test.ts
import { renderHook, act } from '@testing-library/react' ;
import useCounter from './useCounter' ;
describe ( 'useCounter' , () => {
it ( 'initializes with default value' , () => {
const { result } = renderHook (() => useCounter ());
expect (result.current.count). toBe ( 0 );
});
it ( 'initializes with custom value' , () => {
const { result } = renderHook (() => useCounter ( 10 ));
expect (result.current.count). toBe ( 10 );
});
it ( 'increments count' , () => {
const { result } = renderHook (() => useCounter ());
act (() => {
result.current. increment ();
});
expect (result.current.count). toBe ( 1 );
});
it ( 'decrements count' , () => {
const { result } = renderHook (() => useCounter ( 5 ));
act (() => {
result.current. decrement ();
});
expect (result.current.count). toBe ( 4 );
});
it ( 'resets count to initial value' , () => {
const { result } = renderHook (() => useCounter ( 10 ));
act (() => {
result.current. increment ();
result.current. increment ();
result.current. reset ();
});
expect (result.current.count). toBe ( 10 );
});
});
Example: Testing async hook with API calls
// useFetchUser.test.ts
import { renderHook, waitFor } from '@testing-library/react' ;
import useFetchUser from './useFetchUser' ;
import { fetchUser } from './api' ;
jest. mock ( './api' );
describe ( 'useFetchUser' , () => {
it ( 'fetches user data successfully' , async () => {
const mockUser = { id: 1 , name: 'Alice' };
fetchUser. mockResolvedValue (mockUser);
const { result } = renderHook (() => useFetchUser ( 1 ));
// Initial state
expect (result.current.loading). toBe ( true );
expect (result.current.data). toBeNull ();
expect (result.current.error). toBeNull ();
// Wait for data to load
await waitFor (() => {
expect (result.current.loading). toBe ( false );
});
expect (result.current.data). toEqual (mockUser);
expect (result.current.error). toBeNull ();
});
it ( 'handles fetch errors' , async () => {
fetchUser. mockRejectedValue ( new Error ( 'Network error' ));
const { result } = renderHook (() => useFetchUser ( 1 ));
await waitFor (() => {
expect (result.current.loading). toBe ( false );
});
expect (result.current.data). toBeNull ();
expect (result.current.error). toEqual ( new Error ( 'Network error' ));
});
it ( 'refetches when userId changes' , async () => {
fetchUser. mockResolvedValue ({ id: 1 , name: 'Alice' });
const { result , rerender } = renderHook (
({ userId }) => useFetchUser (userId),
{ initialProps: { userId: 1 } }
);
await waitFor (() => expect (result.current.loading). toBe ( false ));
expect (fetchUser). toHaveBeenCalledWith ( 1 );
// Change userId
fetchUser. mockResolvedValue ({ id: 2 , name: 'Bob' });
rerender ({ userId: 2 });
await waitFor (() => expect (result.current.data.name). toBe ( 'Bob' ));
expect (fetchUser). toHaveBeenCalledWith ( 2 );
});
});
Example: Testing hook with context
// useAuth.test.tsx
import { renderHook } from '@testing-library/react' ;
import { AuthProvider } from './AuthContext' ;
import useAuth from './useAuth' ;
const wrapper = ({ children }) => (
< AuthProvider initialUser = {{ id: 1 , name: 'Alice' }}>
{children}
</ AuthProvider >
);
describe ( 'useAuth' , () => {
it ( 'provides auth context values' , () => {
const { result } = renderHook (() => useAuth (), { wrapper });
expect (result.current.user). toEqual ({ id: 1 , name: 'Alice' });
expect (result.current.isAuthenticated). toBe ( true );
});
it ( 'throws error when used outside provider' , () => {
// Suppress console.error for this test
const spy = jest. spyOn (console, 'error' ). mockImplementation (() => {});
expect (() => {
renderHook (() => useAuth ());
}). toThrow ( 'useAuth must be used within AuthProvider' );
spy. mockRestore ();
});
});
4. Mocking and Test Doubles for Components
Mock Type
Use Case
Implementation
Example
Module Mocking
Replace entire module with mock
jest.mock('./module')
Mock API clients, utilities
Function Mocking
Mock specific functions
jest.fn()
Mock callbacks, event handlers
Component Mocking
Replace child components
jest.mock('./Component')
Isolate component under test
Spy Functions
Track function calls without replacing
jest.spyOn(obj, 'method')
Verify method calls on objects
Timer Mocking
Control setTimeout, setInterval
jest.useFakeTimers()
Test debounce, throttle, delays
Network Mocking
Mock HTTP requests
MSW (Mock Service Worker)
Mock fetch, axios requests
Example: Mocking API calls
// TodoList.test.tsx
import { render, screen, waitFor } from '@testing-library/react' ;
import userEvent from '@testing-library/user-event' ;
import TodoList from './TodoList' ;
import * as api from './api' ;
// Mock the entire API module
jest. mock ( './api' );
describe ( 'TodoList' , () => {
beforeEach (() => {
jest. clearAllMocks ();
});
it ( 'loads and displays todos' , async () => {
const mockTodos = [
{ id: 1 , title: 'Buy milk' , completed: false },
{ id: 2 , title: 'Walk dog' , completed: true }
];
api.fetchTodos. mockResolvedValue (mockTodos);
render (< TodoList />);
expect ( await screen. findByText ( 'Buy milk' )). toBeInTheDocument ();
expect (screen. getByText ( 'Walk dog' )). toBeInTheDocument ();
expect (api.fetchTodos). toHaveBeenCalledTimes ( 1 );
});
it ( 'adds new todo' , async () => {
const user = userEvent. setup ();
api.fetchTodos. mockResolvedValue ([]);
api.createTodo. mockResolvedValue ({ id: 3 , title: 'New task' , completed: false });
render (< TodoList />);
const input = screen. getByPlaceholderText ( /new todo/ i );
await user. type (input, 'New task' );
await user. click (screen. getByRole ( 'button' , { name: /add/ i }));
await waitFor (() => {
expect (api.createTodo). toHaveBeenCalledWith ({ title: 'New task' });
});
expect ( await screen. findByText ( 'New task' )). toBeInTheDocument ();
});
});
Example: Mocking child components
// Dashboard.test.tsx
import { render, screen } from '@testing-library/react' ;
import Dashboard from './Dashboard' ;
// Mock complex child components
jest. mock ( './UserProfile' , () => {
return function MockUserProfile ({ userId }) {
return < div >User Profile: {userId}</ div >;
};
});
jest. mock ( './ActivityFeed' , () => {
return function MockActivityFeed () {
return < div >Activity Feed</ div >;
};
});
describe ( 'Dashboard' , () => {
it ( 'renders main layout with mocked children' , () => {
render (< Dashboard userId = { 123 } />);
expect (screen. getByText ( 'User Profile: 123' )). toBeInTheDocument ();
expect (screen. getByText ( 'Activity Feed' )). toBeInTheDocument ();
});
});
Example: Using Mock Service Worker (MSW)
// setupTests.ts
import { setupServer } from 'msw/node' ;
import { rest } from 'msw' ;
const handlers = [
rest. get ( '/api/users' , ( req , res , ctx ) => {
return res (
ctx. json ([
{ id: 1 , name: 'Alice' },
{ id: 2 , name: 'Bob' }
])
);
}),
];
export const server = setupServer ( ... handlers);
beforeAll (() => server. listen ());
afterEach (() => server. resetHandlers ());
afterAll (() => server. close ());
// UserList.test.tsx
import { render, screen } from '@testing-library/react' ;
import { server } from './setupTests' ;
import { rest } from 'msw' ;
import UserList from './UserList' ;
describe ( 'UserList' , () => {
it ( 'displays users from API' , async () => {
render (< UserList />);
expect ( await screen. findByText ( 'Alice' )). toBeInTheDocument ();
expect (screen. getByText ( 'Bob' )). toBeInTheDocument ();
});
it ( 'handles API errors' , async () => {
// Override handler for this test
server. use (
rest. get ( '/api/users' , ( req , res , ctx ) => {
return res (ctx. status ( 500 ), ctx. json ({ error: 'Server error' }));
})
);
render (< UserList />);
expect ( await screen. findByText ( /failed to load users/ i )). toBeInTheDocument ();
});
});
Example: Testing with fake timers
// SearchInput.test.tsx - testing debounced input
import { render, screen } from '@testing-library/react' ;
import userEvent from '@testing-library/user-event' ;
import SearchInput from './SearchInput' ;
describe ( 'SearchInput with debounce' , () => {
beforeEach (() => {
jest. useFakeTimers ();
});
afterEach (() => {
jest. runOnlyPendingTimers ();
jest. useRealTimers ();
});
it ( 'debounces search callback' , async () => {
const mockSearch = jest. fn ();
const user = userEvent. setup ({ delay: null }); // Disable userEvent delays
render (< SearchInput onSearch = {mockSearch} debounceMs = { 500 } />);
const input = screen. getByRole ( 'textbox' );
// Type quickly
await user. type (input, 'test' );
// Callback not called yet
expect (mockSearch).not. toHaveBeenCalled ();
// Fast-forward time
jest. advanceTimersByTime ( 500 );
// Now callback is called once with final value
expect (mockSearch). toHaveBeenCalledTimes ( 1 );
expect (mockSearch). toHaveBeenCalledWith ( 'test' );
});
});
5. Integration Testing with React Router
Test Scenario
Setup
Key Utilities
Example
Route Rendering
Wrap with MemoryRouter
MemoryRouter
Test component renders at specific route
Navigation
Simulate link clicks
user.click(), screen queries
Click link, verify new route content
Route Parameters
Initialize with initialEntries
initialEntries: ['/user/123']
Test dynamic route params
Protected Routes
Test auth redirects
Navigate, conditional rendering
Verify redirect when unauthorized
History Manipulation
Create test history
createMemoryHistory()
Test back/forward navigation
Search Params
Include query strings
initialEntries: ['?tab=2']
Test URL search params handling
Example: Testing navigation and routes
// App.test.tsx
import { render, screen } from '@testing-library/react' ;
import userEvent from '@testing-library/user-event' ;
import { MemoryRouter } from 'react-router-dom' ;
import App from './App' ;
describe ( 'App Navigation' , () => {
it ( 'navigates from home to about page' , async () => {
const user = userEvent. setup ();
render (
< MemoryRouter initialEntries = {[ '/' ]}>
< App />
</ MemoryRouter >
);
// Verify home page
expect (screen. getByText ( /welcome home/ i )). toBeInTheDocument ();
// Click about link
const aboutLink = screen. getByRole ( 'link' , { name: /about/ i });
await user. click (aboutLink);
// Verify about page
expect (screen. getByText ( /about us/ i )). toBeInTheDocument ();
expect (screen. queryByText ( /welcome home/ i )).not. toBeInTheDocument ();
});
it ( 'renders 404 page for unknown routes' , () => {
render (
< MemoryRouter initialEntries = {[ '/unknown-route' ]}>
< App />
</ MemoryRouter >
);
expect (screen. getByText ( /404 . * not found/ i )). toBeInTheDocument ();
});
});
Example: Testing dynamic routes with params
// UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react' ;
import { MemoryRouter, Route, Routes } from 'react-router-dom' ;
import UserProfile from './UserProfile' ;
import * as api from './api' ;
jest. mock ( './api' );
describe ( 'UserProfile' , () => {
it ( 'loads user data based on route param' , async () => {
const mockUser = { id: '123' , name: 'Alice' , email: 'alice@example.com' };
api.fetchUser. mockResolvedValue (mockUser);
render (
< MemoryRouter initialEntries = {[ '/user/123' ]}>
< Routes >
< Route path = "/user/:userId" element = {< UserProfile />} />
</ Routes >
</ MemoryRouter >
);
await waitFor (() => {
expect (api.fetchUser). toHaveBeenCalledWith ( '123' );
});
expect ( await screen. findByText ( 'Alice' )). toBeInTheDocument ();
expect (screen. getByText ( 'alice@example.com' )). toBeInTheDocument ();
});
});
Example: Testing protected routes
// ProtectedRoute.test.tsx
import { render, screen } from '@testing-library/react' ;
import { MemoryRouter } from 'react-router-dom' ;
import App from './App' ;
import { AuthProvider } from './AuthContext' ;
describe ( 'Protected Routes' , () => {
it ( 'redirects to login when not authenticated' , () => {
render (
< MemoryRouter initialEntries = {[ '/dashboard' ]}>
< AuthProvider initialAuth = {{ user: null , isAuthenticated: false }}>
< App />
</ AuthProvider >
</ MemoryRouter >
);
// Should be redirected to login
expect (screen. getByText ( /please log in/ i )). toBeInTheDocument ();
expect (screen. queryByText ( /dashboard/ i )).not. toBeInTheDocument ();
});
it ( 'allows access when authenticated' , () => {
render (
< MemoryRouter initialEntries = {[ '/dashboard' ]}>
< AuthProvider initialAuth = {{
user: { id: 1 , name: 'Alice' },
isAuthenticated: true
}}>
< App />
</ AuthProvider >
</ MemoryRouter >
);
// Should show dashboard
expect (screen. getByText ( /dashboard/ i )). toBeInTheDocument ();
expect (screen. queryByText ( /please log in/ i )).not. toBeInTheDocument ();
});
});
6. End-to-End Testing with Cypress/Playwright
Tool
Strengths
Test Pattern
Use Case
Cypress
Developer-friendly, time-travel debugging, real-time reloading
cy.visit(), cy.get(), cy.click()
Full user flows, visual testing, network mocking
Playwright
Multi-browser, parallel execution, mobile emulation, auto-wait
page.goto(), page.click(), page.fill()
Cross-browser testing, mobile testing, API testing
Cypress Commands
Custom reusable commands
Cypress.Commands.add()
Login flows, common interactions
Network Interception
Mock/stub API responses
cy.intercept(), page.route()
Test error states, loading states
Visual Testing
Screenshot comparison
cy.screenshot(), expect(screenshot)
Detect visual regressions
Component Testing
Mount React components in test runner
cy.mount(), mount()
Component isolation with real browser
Example: Cypress E2E test
// cypress/e2e/login.cy.js
describe ( 'Login Flow' , () => {
beforeEach (() => {
cy. visit ( 'http://localhost:3000' );
});
it ( 'allows user to log in with valid credentials' , () => {
// Intercept API call
cy. intercept ( 'POST' , '/api/login' , {
statusCode: 200 ,
body: {
user: { id: 1 , name: 'Alice' , email: 'alice@example.com' },
token: 'fake-jwt-token'
}
}). as ( 'loginRequest' );
// Fill login form
cy. get ( 'input[name="email"]' ). type ( 'alice@example.com' );
cy. get ( 'input[name="password"]' ). type ( 'password123' );
cy. get ( 'button[type="submit"]' ). click ();
// Wait for API call
cy. wait ( '@loginRequest' );
// Verify redirect to dashboard
cy. url (). should ( 'include' , '/dashboard' );
cy. contains ( 'Welcome, Alice' ). should ( 'be.visible' );
// Verify token stored
cy. window (). its ( 'localStorage.token' ). should ( 'exist' );
});
it ( 'shows error message for invalid credentials' , () => {
cy. intercept ( 'POST' , '/api/login' , {
statusCode: 401 ,
body: { error: 'Invalid credentials' }
}). as ( 'loginRequest' );
cy. get ( 'input[name="email"]' ). type ( 'wrong@example.com' );
cy. get ( 'input[name="password"]' ). type ( 'wrongpassword' );
cy. get ( 'button[type="submit"]' ). click ();
cy. wait ( '@loginRequest' );
// Verify error message
cy. contains ( 'Invalid credentials' ). should ( 'be.visible' );
cy. url (). should ( 'include' , '/login' );
});
it ( 'validates form inputs' , () => {
cy. get ( 'button[type="submit"]' ). click ();
// Check validation errors
cy. contains ( 'Email is required' ). should ( 'be.visible' );
cy. contains ( 'Password is required' ). should ( 'be.visible' );
// Fill invalid email
cy. get ( 'input[name="email"]' ). type ( 'invalid-email' );
cy. get ( 'button[type="submit"]' ). click ();
cy. contains ( 'Invalid email format' ). should ( 'be.visible' );
});
});
Example: Cypress custom commands
// cypress/support/commands.js
Cypress.Commands. add ( 'login' , ( email , password ) => {
cy. visit ( '/login' );
cy. get ( 'input[name="email"]' ). type (email);
cy. get ( 'input[name="password"]' ). type (password);
cy. get ( 'button[type="submit"]' ). click ();
cy. url (). should ( 'include' , '/dashboard' );
});
Cypress.Commands. add ( 'logout' , () => {
cy. get ( '[data-testid="user-menu"]' ). click ();
cy. contains ( 'Logout' ). click ();
cy. url (). should ( 'include' , '/login' );
});
// Usage in tests
describe ( 'Dashboard' , () => {
beforeEach (() => {
cy. login ( 'alice@example.com' , 'password123' );
});
it ( 'displays user data' , () => {
cy. contains ( 'Welcome, Alice' ). should ( 'be.visible' );
});
it ( 'allows user to logout' , () => {
cy. logout ();
});
});
Example: Playwright E2E test
// tests/login.spec.ts
import { test, expect } from '@playwright/test' ;
test. describe ( 'Login Flow' , () => {
test. beforeEach ( async ({ page }) => {
await page. goto ( 'http://localhost:3000' );
});
test ( 'allows user to log in with valid credentials' , async ({ page }) => {
// Mock API response
await page. route ( '**/api/login' , async ( route ) => {
await route. fulfill ({
status: 200 ,
contentType: 'application/json' ,
body: JSON . stringify ({
user: { id: 1 , name: 'Alice' , email: 'alice@example.com' },
token: 'fake-jwt-token'
})
});
});
// Fill and submit form
await page. fill ( 'input[name="email"]' , 'alice@example.com' );
await page. fill ( 'input[name="password"]' , 'password123' );
await page. click ( 'button[type="submit"]' );
// Verify redirect
await expect (page). toHaveURL ( / . * dashboard/ );
await expect (page. locator ( 'text=Welcome, Alice' )). toBeVisible ();
});
test ( 'handles network errors gracefully' , async ({ page }) => {
// Simulate network failure
await page. route ( '**/api/login' , ( route ) => route. abort ());
await page. fill ( 'input[name="email"]' , 'alice@example.com' );
await page. fill ( 'input[name="password"]' , 'password123' );
await page. click ( 'button[type="submit"]' );
// Verify error message
await expect (page. locator ( 'text=/network error/i' )). toBeVisible ();
});
});
Example: Playwright with fixtures and page objects
// tests/fixtures.ts
import { test as base } from '@playwright/test' ;
// Create custom fixture for authenticated user
export const test = base. extend ({
authenticatedPage : async ({ page }, use ) => {
await page. goto ( 'http://localhost:3000/login' );
await page. fill ( 'input[name="email"]' , 'alice@example.com' );
await page. fill ( 'input[name="password"]' , 'password123' );
await page. click ( 'button[type="submit"]' );
await page. waitForURL ( '**/dashboard' );
await use (page);
}
});
// tests/pages/DashboardPage.ts
export class DashboardPage {
constructor ( private page ) {}
async navigateToSettings () {
await this .page. click ( '[data-testid="settings-link"]' );
}
async getUserName () {
return await this .page. locator ( '[data-testid="user-name"]' ). textContent ();
}
async createNewProject ( name : string ) {
await this .page. click ( 'button:has-text("New Project")' );
await this .page. fill ( 'input[name="project-name"]' , name);
await this .page. click ( 'button:has-text("Create")' );
}
}
// tests/dashboard.spec.ts
import { test } from './fixtures' ;
import { DashboardPage } from './pages/DashboardPage' ;
import { expect } from '@playwright/test' ;
test ( 'user can create new project' , async ({ authenticatedPage }) => {
const dashboard = new DashboardPage (authenticatedPage);
await dashboard. createNewProject ( 'My New Project' );
await expect (authenticatedPage. locator ( 'text=My New Project' )). toBeVisible ();
});
E2E Testing Best Practices: Start server before tests, use baseURL configuration, avoid
hard-coded waits (use auto-waiting), test critical user paths only, run in CI/CD pipeline, use parallel
execution, implement proper cleanup, mock external services, use data-testid for stable selectors, organize
tests by feature, maintain test independence.