State Testing and Quality Assurance

1. Unit Testing State Logic with React Testing Library

Testing state logic ensures component behavior is correct across state changes and user interactions.

Testing Pattern Description Use Case
render() Render component into testing DOM Component initialization with state
screen.getByRole() Query elements by accessibility role Find buttons, inputs, headings
fireEvent.click() Simulate user click events Test state changes on interaction
userEvent.type() Simulate realistic user typing Test form input state updates
waitFor() Wait for async state updates Async operations, API calls
act() Wrap state updates in act Manual state updates in tests

Example: Testing Counter State with useState

// Counter.jsx
import { useState } from 'react';

export function Counter({ initialCount = 0 }) {
  const [count, setCount] = useState(initialCount);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

// Counter.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';

describe('Counter', () => {
  test('renders with initial count', () => {
    render(<Counter initialCount={5} />);
    expect(screen.getByText('Count: 5')).toBeInTheDocument();
  });

  test('increments count when increment button clicked', async () => {
    const user = userEvent.setup();
    render(<Counter />);
    
    const incrementBtn = screen.getByRole('button', { name: /increment/i });
    await user.click(incrementBtn);
    
    expect(screen.getByText('Count: 1')).toBeInTheDocument();
  });

  test('decrements count when decrement button clicked', async () => {
    const user = userEvent.setup();
    render(<Counter initialCount={5} />);
    
    const decrementBtn = screen.getByRole('button', { name: /decrement/i });
    await user.click(decrementBtn);
    
    expect(screen.getByText('Count: 4')).toBeInTheDocument();
  });

  test('resets count to zero', async () => {
    const user = userEvent.setup();
    render(<Counter initialCount={10} />);
    
    const resetBtn = screen.getByRole('button', { name: /reset/i });
    await user.click(resetBtn);
    
    expect(screen.getByText('Count: 0')).toBeInTheDocument();
  });
});

Example: Testing Async State Updates

// UserProfile.jsx
import { useState, useEffect } from 'react';

export function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('Failed to fetch');
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchUser();
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>User: {user?.name}</div>;
}

// UserProfile.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from './UserProfile';

global.fetch = jest.fn();

describe('UserProfile', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('displays loading state initially', () => {
    fetch.mockImplementation(() => new Promise(() => {})); // Never resolves
    render(<UserProfile userId={1} />);
    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });

  test('displays user data after successful fetch', async () => {
    const mockUser = { id: 1, name: 'John Doe' };
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser
    });

    render(<UserProfile userId={1} />);

    await waitFor(() => {
      expect(screen.getByText('User: John Doe')).toBeInTheDocument();
    });
    expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
  });

  test('displays error message on fetch failure', async () => {
    fetch.mockResolvedValueOnce({
      ok: false
    });

    render(<UserProfile userId={1} />);

    await waitFor(() => {
      expect(screen.getByText(/Error:/)).toBeInTheDocument();
    });
  });
});
Note: Use userEvent instead of fireEvent for more realistic user interactions. Always use async/await with userEvent methods.

2. Testing useReducer and Action Dispatching

Test reducer logic in isolation and verify action dispatching updates state correctly.

Testing Strategy Description Use Case
Reducer unit tests Test reducer function independently Pure logic testing without React
Action dispatching Test dispatch calls update state Component integration testing
Initial state Verify initial state setup Component initialization
Multiple actions Test action sequences Complex state transitions
Invalid actions Test unknown action types Error handling validation

Example: Testing Reducer Function in Isolation

// todoReducer.js
export const initialState = { todos: [], filter: 'all' };

export function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.payload, completed: false }]
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
        )
      };
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      };
    case 'SET_FILTER':
      return { ...state, filter: action.payload };
    default:
      return state;
  }
}

// todoReducer.test.js
import { todoReducer, initialState } from './todoReducer';

describe('todoReducer', () => {
  test('returns initial state for unknown action', () => {
    const result = todoReducer(initialState, { type: 'UNKNOWN' });
    expect(result).toEqual(initialState);
  });

  test('adds todo on ADD_TODO action', () => {
    const action = { type: 'ADD_TODO', payload: 'Learn testing' };
    const result = todoReducer(initialState, action);
    
    expect(result.todos).toHaveLength(1);
    expect(result.todos[0].text).toBe('Learn testing');
    expect(result.todos[0].completed).toBe(false);
  });

  test('toggles todo completion on TOGGLE_TODO action', () => {
    const stateWithTodo = {
      todos: [{ id: 1, text: 'Test', completed: false }],
      filter: 'all'
    };
    const action = { type: 'TOGGLE_TODO', payload: 1 };
    const result = todoReducer(stateWithTodo, action);
    
    expect(result.todos[0].completed).toBe(true);
  });

  test('deletes todo on DELETE_TODO action', () => {
    const stateWithTodo = {
      todos: [
        { id: 1, text: 'First', completed: false },
        { id: 2, text: 'Second', completed: false }
      ],
      filter: 'all'
    };
    const action = { type: 'DELETE_TODO', payload: 1 };
    const result = todoReducer(stateWithTodo, action);
    
    expect(result.todos).toHaveLength(1);
    expect(result.todos[0].id).toBe(2);
  });

  test('sets filter on SET_FILTER action', () => {
    const action = { type: 'SET_FILTER', payload: 'completed' };
    const result = todoReducer(initialState, action);
    
    expect(result.filter).toBe('completed');
  });
});

Example: Testing useReducer Component Integration

// TodoList.jsx
import { useReducer } from 'react';
import { todoReducer, initialState } from './todoReducer';

export function TodoList() {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  const [input, setInput] = useState('');

  const handleAdd = () => {
    if (input.trim()) {
      dispatch({ type: 'ADD_TODO', payload: input });
      setInput('');
    }
  };

  return (
    <div>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Add todo"
      />
      <button onClick={handleAdd}>Add</button>
      <ul>
        {state.todos.map(todo => (
          <li key={todo.id}>
            <span>{todo.text}</span>
            <button onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}>
              Toggle
            </button>
            <button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

// TodoList.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TodoList } from './TodoList';

describe('TodoList', () => {
  test('adds todo when Add button clicked', async () => {
    const user = userEvent.setup();
    render(<TodoList />);
    
    const input = screen.getByPlaceholderText('Add todo');
    const addBtn = screen.getByRole('button', { name: /add/i });
    
    await user.type(input, 'Learn React Testing');
    await user.click(addBtn);
    
    expect(screen.getByText('Learn React Testing')).toBeInTheDocument();
    expect(input).toHaveValue(''); // Input cleared
  });

  test('toggles todo completion', async () => {
    const user = userEvent.setup();
    render(<TodoList />);
    
    // Add a todo first
    await user.type(screen.getByPlaceholderText('Add todo'), 'Test todo');
    await user.click(screen.getByRole('button', { name: /add/i }));
    
    // Toggle it
    const toggleBtn = screen.getByRole('button', { name: /toggle/i });
    await user.click(toggleBtn);
    
    // Verify state changed (implementation-dependent)
  });

  test('deletes todo', async () => {
    const user = userEvent.setup();
    render(<TodoList />);
    
    // Add a todo
    await user.type(screen.getByPlaceholderText('Add todo'), 'Delete me');
    await user.click(screen.getByRole('button', { name: /add/i }));
    
    // Delete it
    const deleteBtn = screen.getByRole('button', { name: /delete/i });
    await user.click(deleteBtn);
    
    expect(screen.queryByText('Delete me')).not.toBeInTheDocument();
  });
});
Note: Test reducer functions separately from components for easier debugging and better test coverage. Pure reducer functions don't require React Testing Library.

3. Testing Context Providers and Consumer Components

Test context providers supply correct values and consumers react to context changes.

Testing Pattern Description Use Case
Custom wrapper Wrap components with providers Test components consuming context
Mock context values Provide test-specific values Control context state in tests
Context updates Test context value changes Verify consumer re-renders
Multiple consumers Test multiple context users Shared state consistency

Example: Testing Context Provider and Consumer

// ThemeContext.jsx
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// ThemeDisplay.jsx
export function ThemeDisplay() {
  const { theme, toggleTheme } = useTheme();

  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

// ThemeContext.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider, useTheme } from './ThemeContext';
import { ThemeDisplay } from './ThemeDisplay';

describe('ThemeContext', () => {
  test('provides default theme value', () => {
    render(
      <ThemeProvider>
        <ThemeDisplay />
      </ThemeProvider>
    );

    expect(screen.getByText('Current theme: light')).toBeInTheDocument();
  });

  test('toggles theme when button clicked', async () => {
    const user = userEvent.setup();
    render(
      <ThemeProvider>
        <ThemeDisplay />
      </ThemeProvider>
    );

    const toggleBtn = screen.getByRole('button', { name: /toggle theme/i });
    await user.click(toggleBtn);

    expect(screen.getByText('Current theme: dark')).toBeInTheDocument();

    await user.click(toggleBtn);
    expect(screen.getByText('Current theme: light')).toBeInTheDocument();
  });

  test('throws error when useTheme used outside provider', () => {
    // Suppress console.error for this test
    const consoleError = jest.spyOn(console, 'error').mockImplementation();

    const TestComponent = () => {
      useTheme();
      return null;
    };

    expect(() => render(<TestComponent />)).toThrow(
      'useTheme must be used within ThemeProvider'
    );

    consoleError.mockRestore();
  });

  test('multiple consumers share same context', async () => {
    const user = userEvent.setup();
    
    function MultipleDisplays() {
      return (
        <ThemeProvider>
          <ThemeDisplay />
          <ThemeDisplay />
        </ThemeProvider>
      );
    }

    render(<MultipleDisplays />);

    const displays = screen.getAllByText('Current theme: light');
    expect(displays).toHaveLength(2);

    const toggleBtn = screen.getAllByRole('button', { name: /toggle theme/i })[0];
    await user.click(toggleBtn);

    const darkDisplays = screen.getAllByText('Current theme: dark');
    expect(darkDisplays).toHaveLength(2);
  });
});

Example: Custom Render with Context Wrapper

// test-utils.jsx
import { render } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';
import { AuthProvider } from './AuthContext';

export function renderWithProviders(
  ui,
  {
    themeValue = 'light',
    authValue = { user: null, login: jest.fn(), logout: jest.fn() },
    ...renderOptions
  } = {}
) {
  function Wrapper({ children }) {
    return (
      <ThemeProvider initialTheme={themeValue}>
        <AuthProvider initialAuth={authValue}>
          {children}
        </AuthProvider>
      </ThemeProvider>
    );
  }

  return render(ui, { wrapper: Wrapper, ...renderOptions });
}

// Re-export everything from React Testing Library
export * from '@testing-library/react';

// App.test.jsx
import { screen } from '@testing-library/react';
import { renderWithProviders } from './test-utils';
import { App } from './App';

describe('App', () => {
  test('renders with default providers', () => {
    renderWithProviders(<App />);
    // Test assertions
  });

  test('renders with custom auth state', () => {
    const mockUser = { id: 1, name: 'Test User' };
    renderWithProviders(<App />, {
      authValue: { user: mockUser, login: jest.fn(), logout: jest.fn() }
    });

    expect(screen.getByText('Test User')).toBeInTheDocument();
  });
});
Note: Create custom render utilities with common providers to reduce test boilerplate and ensure consistent test setup across your test suite.

4. Mock State for Component Testing

Mock state management hooks and libraries to test components in isolation.

Mocking Strategy Description Use Case
jest.mock() Mock entire modules Replace state management libraries
jest.spyOn() Spy on specific functions Track hook calls, verify usage
Mock return values Control hook return values Test component with specific state
Mock implementations Provide custom hook behavior Simulate complex state scenarios

Example: Mocking useState and useEffect

// DataFetcher.jsx
import { useState, useEffect } from 'react';

export function DataFetcher({ url }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      });
  }, [url]);

  if (loading) return <div>Loading...</div>;
  return <div>{data?.message}</div>;
}

// DataFetcher.test.jsx
import { render, screen } from '@testing-library/react';
import * as React from 'react';
import { DataFetcher } from './DataFetcher';

describe('DataFetcher with mocked hooks', () => {
  test('shows loading state', () => {
    // Mock useState to return loading state
    const mockSetData = jest.fn();
    const mockSetLoading = jest.fn();
    
    jest.spyOn(React, 'useState')
      .mockReturnValueOnce([null, mockSetData])      // data state
      .mockReturnValueOnce([true, mockSetLoading]);  // loading state
    
    jest.spyOn(React, 'useEffect').mockImplementation(f => f());

    render(<DataFetcher url="/api/data" />);
    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });

  test('shows data after loading', () => {
    // Mock useState to return loaded state
    const mockData = { message: 'Test Message' };
    
    jest.spyOn(React, 'useState')
      .mockReturnValueOnce([mockData, jest.fn()])    // data state
      .mockReturnValueOnce([false, jest.fn()]);      // loading state
    
    jest.spyOn(React, 'useEffect').mockImplementation(() => {});

    render(<DataFetcher url="/api/data" />);
    expect(screen.getByText('Test Message')).toBeInTheDocument();
  });
});

Example: Mocking Redux/Zustand Store

// ProductList.jsx
import { useStore } from './store';

export function ProductList() {
  const products = useStore(state => state.products);
  const addToCart = useStore(state => state.addToCart);

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          {product.name}
          <button onClick={() => addToCart(product)}>Add to Cart</button>
        </li>
      ))}
    </ul>
  );
}

// ProductList.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ProductList } from './ProductList';
import * as store from './store';

jest.mock('./store');

describe('ProductList', () => {
  test('renders products from store', () => {
    const mockProducts = [
      { id: 1, name: 'Product 1' },
      { id: 2, name: 'Product 2' }
    ];

    store.useStore.mockImplementation(selector => 
      selector({ products: mockProducts, addToCart: jest.fn() })
    );

    render(<ProductList />);

    expect(screen.getByText('Product 1')).toBeInTheDocument();
    expect(screen.getByText('Product 2')).toBeInTheDocument();
  });

  test('calls addToCart when button clicked', async () => {
    const user = userEvent.setup();
    const mockAddToCart = jest.fn();
    const mockProducts = [{ id: 1, name: 'Product 1' }];

    store.useStore.mockImplementation(selector => 
      selector({ products: mockProducts, addToCart: mockAddToCart })
    );

    render(<ProductList />);

    const addBtn = screen.getByRole('button', { name: /add to cart/i });
    await user.click(addBtn);

    expect(mockAddToCart).toHaveBeenCalledWith(mockProducts[0]);
  });
});
Warning: Mocking React hooks can lead to brittle tests. Prefer integration testing with real hooks when possible. Use mocking primarily for external dependencies or complex state scenarios.

5. Integration Testing with State Management Libraries

Test complete state management flows including store initialization, actions, and selectors.

Library Testing Approach Key Considerations
Redux Test store, reducers, actions separately Use real store in tests, mock API calls
Zustand Create test store instances Reset store between tests
Jotai Use Provider in tests Isolated atom state per test
React Query Wrap with QueryClientProvider Clear cache between tests

Example: Redux Integration Testing

// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';

export const createStore = (preloadedState) => {
  return configureStore({
    reducer: { counter: counterReducer },
    preloadedState
  });
};

// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: state => { state.value += 1; },
    decrement: state => { state.value -= 1; },
    incrementByAmount: (state, action) => { state.value += action.payload; }
  }
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

// Counter.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
import { createStore } from './store';
import { Counter } from './Counter';

describe('Counter with Redux', () => {
  let store;

  beforeEach(() => {
    store = createStore();
  });

  const renderWithStore = (component) => {
    return render(<Provider store={store}>{component}</Provider>);
  };

  test('increments counter value', async () => {
    const user = userEvent.setup();
    renderWithStore(<Counter />);

    const incrementBtn = screen.getByRole('button', { name: /increment/i });
    await user.click(incrementBtn);

    expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
  });

  test('uses preloaded state', () => {
    store = createStore({ counter: { value: 10 } });
    renderWithStore(<Counter />);

    expect(screen.getByText(/count: 10/i)).toBeInTheDocument();
  });

  test('multiple actions update state correctly', async () => {
    const user = userEvent.setup();
    renderWithStore(<Counter />);

    await user.click(screen.getByRole('button', { name: /increment/i }));
    await user.click(screen.getByRole('button', { name: /increment/i }));
    await user.click(screen.getByRole('button', { name: /decrement/i }));

    expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
  });
});

Example: React Query Integration Testing

// UserList.jsx
import { useQuery } from '@tanstack/react-query';

function fetchUsers() {
  return fetch('/api/users').then(res => res.json());
}

export function UserList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// UserList.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { UserList } from './UserList';

global.fetch = jest.fn();

describe('UserList with React Query', () => {
  let queryClient;

  beforeEach(() => {
    queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          retry: false, // Disable retries for tests
        },
      },
    });
    jest.clearAllMocks();
  });

  const renderWithClient = (component) => {
    return render(
      <QueryClientProvider client={queryClient}>
        {component}
      </QueryClientProvider>
    );
  };

  test('displays loading state', () => {
    fetch.mockImplementation(() => new Promise(() => {}));
    renderWithClient(<UserList />);
    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });

  test('displays users after successful fetch', async () => {
    const mockUsers = [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ];

    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockUsers
    });

    renderWithClient(<UserList />);

    await waitFor(() => {
      expect(screen.getByText('Alice')).toBeInTheDocument();
      expect(screen.getByText('Bob')).toBeInTheDocument();
    });
  });

  test('displays error message on fetch failure', async () => {
    fetch.mockRejectedValueOnce(new Error('Network error'));

    renderWithClient(<UserList />);

    await waitFor(() => {
      expect(screen.getByText(/Error: Network error/i)).toBeInTheDocument();
    });
  });
});
Note: Create fresh store/client instances for each test to ensure test isolation. Configure libraries to disable retries and caching behaviors that might cause flaky tests.

6. End-to-End State Testing with Cypress/Playwright

Test complete user workflows including state persistence, navigation, and multi-page state management.

E2E Pattern Description Use Case
User flows Test complete user journeys Login, checkout, multi-step forms
State persistence Verify localStorage/sessionStorage Shopping cart, user preferences
Navigation state Test state across page transitions Wizard forms, checkout flows
API interactions Test real/mocked API calls Data fetching, mutations
Visual regression Test UI based on state changes Theme switching, conditional rendering

Example: Cypress E2E State Testing

// cypress/e2e/shopping-cart.cy.js
describe('Shopping Cart State Management', () => {
  beforeEach(() => {
    cy.visit('/products');
    cy.clearLocalStorage();
  });

  it('adds products to cart and persists state', () => {
    // Add first product
    cy.get('[data-testid="product-1"]').within(() => {
      cy.contains('Add to Cart').click();
    });

    // Verify cart count updated
    cy.get('[data-testid="cart-count"]').should('contain', '1');

    // Add second product
    cy.get('[data-testid="product-2"]').within(() => {
      cy.contains('Add to Cart').click();
    });

    cy.get('[data-testid="cart-count"]').should('contain', '2');

    // Verify localStorage persisted cart state
    cy.window().then(win => {
      const cartState = JSON.parse(win.localStorage.getItem('cart'));
      expect(cartState.items).to.have.length(2);
    });

    // Navigate to cart page
    cy.get('[data-testid="view-cart"]').click();
    cy.url().should('include', '/cart');

    // Verify cart items displayed
    cy.get('[data-testid="cart-item"]').should('have.length', 2);
  });

  it('persists cart across page refresh', () => {
    // Add product to cart
    cy.get('[data-testid="product-1"]').within(() => {
      cy.contains('Add to Cart').click();
    });

    // Reload page
    cy.reload();

    // Verify cart persisted
    cy.get('[data-testid="cart-count"]').should('contain', '1');
  });

  it('clears cart on logout', () => {
    // Setup: Login and add items
    cy.login('test@example.com', 'password');
    cy.get('[data-testid="product-1"]').within(() => {
      cy.contains('Add to Cart').click();
    });

    // Logout
    cy.get('[data-testid="logout"]').click();

    // Verify cart cleared
    cy.get('[data-testid="cart-count"]').should('contain', '0');
    cy.window().then(win => {
      expect(win.localStorage.getItem('cart')).to.be.null;
    });
  });

  it('syncs cart across multiple tabs', () => {
    // Add to cart in first tab
    cy.get('[data-testid="product-1"]').within(() => {
      cy.contains('Add to Cart').click();
    });

    // Open new tab (simulate with new visit)
    cy.visit('/products');

    // Verify cart synced
    cy.get('[data-testid="cart-count"]').should('contain', '1');
  });
});

Example: Playwright E2E State Testing

// tests/auth-state.spec.js
import { test, expect } from '@playwright/test';

test.describe('Authentication State Flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
  });

  test('maintains auth state after login', async ({ page }) => {
    // Fill login form
    await page.fill('[data-testid="email-input"]', 'test@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');
    await page.click('[data-testid="login-button"]');

    // Wait for redirect
    await page.waitForURL('/dashboard');

    // Verify auth state in UI
    await expect(page.locator('[data-testid="user-name"]')).toHaveText('Test User');

    // Verify auth token in localStorage
    const token = await page.evaluate(() => localStorage.getItem('authToken'));
    expect(token).toBeTruthy();

    // Navigate to different page
    await page.goto('/profile');

    // Verify still authenticated
    await expect(page.locator('[data-testid="user-name"]')).toHaveText('Test User');
  });

  test('redirects to login when accessing protected route without auth', async ({ page }) => {
    await page.goto('/dashboard');
    await page.waitForURL('/login');
    await expect(page.locator('h1')).toHaveText('Login');
  });

  test('clears auth state on logout', async ({ page, context }) => {
    // Login first
    await page.fill('[data-testid="email-input"]', 'test@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');
    await page.click('[data-testid="login-button"]');
    await page.waitForURL('/dashboard');

    // Logout
    await page.click('[data-testid="logout-button"]');
    await page.waitForURL('/login');

    // Verify auth state cleared
    const token = await page.evaluate(() => localStorage.getItem('authToken'));
    expect(token).toBeNull();

    // Verify redirected when trying to access protected route
    await page.goto('/dashboard');
    await page.waitForURL('/login');
  });

  test('handles concurrent auth state in multiple tabs', async ({ context }) => {
    // Create two pages (tabs)
    const page1 = await context.newPage();
    const page2 = await context.newPage();

    // Login in first tab
    await page1.goto('/login');
    await page1.fill('[data-testid="email-input"]', 'test@example.com');
    await page1.fill('[data-testid="password-input"]', 'password123');
    await page1.click('[data-testid="login-button"]');
    await page1.waitForURL('/dashboard');

    // Navigate in second tab
    await page2.goto('/dashboard');

    // Verify authenticated in both tabs
    await expect(page1.locator('[data-testid="user-name"]')).toHaveText('Test User');
    await expect(page2.locator('[data-testid="user-name"]')).toHaveText('Test User');

    // Logout in first tab
    await page1.click('[data-testid="logout-button"]');

    // Verify second tab also logged out (if cross-tab sync implemented)
    await page2.reload();
    await page2.waitForURL('/login');
  });
});
Note: E2E tests are slower and more expensive than unit tests. Focus on critical user flows and state persistence scenarios. Use API mocking strategically to improve test speed and reliability.

Section 17 Key Takeaways

  • React Testing Library - Test state logic through user interactions, use userEvent for realistic testing
  • Reducer testing - Test reducer functions in isolation as pure functions, then integration test with components
  • Context testing - Create custom render utilities with providers, test error handling when context missing
  • State mocking - Mock external state libraries, prefer real hooks when possible to avoid brittle tests
  • Integration testing - Use real store instances in tests, clear state between tests for isolation
  • E2E testing - Test complete user flows including state persistence, navigation, and cross-tab synchronization