+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 147 of 355

βš› ️ Testing React Components: React Testing Library

Master React component testing with TypeScript and React Testing Library, covering props, events, hooks, and complex user interactions πŸš€

πŸš€Intermediate
28 min read

Prerequisites

  • Understanding of React fundamentals and TypeScript πŸ“
  • Knowledge of Jest testing framework and unit testing ⚑
  • Familiarity with React hooks and component patterns πŸ’»

What you'll learn

  • Test React components with comprehensive coverage using React Testing Library 🎯
  • Master testing user interactions, events, and component state πŸ—οΈ
  • Handle async operations, hooks, and context in component tests πŸ›
  • Create maintainable test suites for complex React applications ✨

🎯 Introduction

Welcome to the React component testing laboratory! βš›οΈ If testing regular functions were like checking individual gears in a machine, then testing React components would be like testing entire interactive user interfaces - complete with user inputs, state changes, visual feedback, and complex user workflows that need to work seamlessly together to create amazing user experiences!

React component testing with TypeScript combines the power of type safety with comprehensive user interaction testing. React Testing Library encourages testing components the way users actually interact with them, focusing on behavior rather than implementation details. This approach leads to more robust tests that give you confidence your components work correctly for real users.

By the end of this tutorial, you’ll be a master of React component testing, capable of thoroughly testing everything from simple presentational components to complex interactive widgets with state management, async operations, and user interactions. You’ll learn to test like a user, ensuring your components provide excellent user experiences. Let’s build some bulletproof React component tests! 🌟

πŸ“š Understanding React Testing Library Fundamentals

πŸ€” Why React Testing Library?

React Testing Library focuses on testing components from the user’s perspective, encouraging better testing practices and more maintainable tests.

// 🌟 Setting up comprehensive React Testing Library environment

import React, { useState, useEffect, useCallback, useMemo, useContext, createContext } from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';

// Basic component interfaces and types
interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;
  isActive: boolean;
  role: 'admin' | 'user' | 'moderator';
}

interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
  category: string;
  inStock: boolean;
  imageUrl?: string;
}

interface CartItem {
  product: Product;
  quantity: number;
}

// Context for state management
interface AppContextType {
  user: User | null;
  cart: CartItem[];
  addToCart: (product: Product, quantity: number) => void;
  removeFromCart: (productId: string) => void;
  updateCartQuantity: (productId: string, quantity: number) => void;
  login: (user: User) => void;
  logout: () => void;
}

const AppContext = createContext<AppContextType | undefined>(undefined);

const useAppContext = (): AppContextType => {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error('useAppContext must be used within AppProvider');
  }
  return context;
};

// Basic presentational component
interface ButtonProps {
  children: React.ReactNode;
  onClick?: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
  type?: 'button' | 'submit' | 'reset';
  'data-testid'?: string;
}

const Button: React.FC<ButtonProps> = ({
  children,
  onClick,
  disabled = false,
  variant = 'primary',
  size = 'medium',
  type = 'button',
  'data-testid': testId
}) => {
  const baseClasses = 'btn';
  const variantClasses = {
    primary: 'btn-primary',
    secondary: 'btn-secondary',
    danger: 'btn-danger'
  };
  const sizeClasses = {
    small: 'btn-sm',
    medium: 'btn-md',
    large: 'btn-lg'
  };

  const className = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`;

  return (
    <button
      type={type}
      className={className}
      onClick={onClick}
      disabled={disabled}
      data-testid={testId}
    >
      {children}
    </button>
  );
};

// Input component with validation
interface InputProps {
  label: string;
  value: string;
  onChange: (value: string) => void;
  onBlur?: () => void;
  error?: string;
  type?: 'text' | 'email' | 'password' | 'number';
  placeholder?: string;
  required?: boolean;
  disabled?: boolean;
  'data-testid'?: string;
}

const Input: React.FC<InputProps> = ({
  label,
  value,
  onChange,
  onBlur,
  error,
  type = 'text',
  placeholder,
  required = false,
  disabled = false,
  'data-testid': testId
}) => {
  const inputId = `input-${label.toLowerCase().replace(/\s+/g, '-')}`;

  return (
    <div className="input-group">
      <label htmlFor={inputId} className="input-label">
        {label}
        {required && <span className="required">*</span>}
      </label>
      <input
        id={inputId}
        type={type}
        value={value}
        onChange={(e) => onChange(e.target.value)}
        onBlur={onBlur}
        placeholder={placeholder}
        required={required}
        disabled={disabled}
        className={`input ${error ? 'input-error' : ''}`}
        data-testid={testId}
        aria-describedby={error ? `${inputId}-error` : undefined}
        aria-invalid={!!error}
      />
      {error && (
        <span id={`${inputId}-error`} className="error-message" role="alert">
          {error}
        </span>
      )}
    </div>
  );
};

// User card component
interface UserCardProps {
  user: User;
  onEdit?: (user: User) => void;
  onDelete?: (userId: string) => void;
  onToggleActive?: (userId: string) => void;
  showActions?: boolean;
}

const UserCard: React.FC<UserCardProps> = ({
  user,
  onEdit,
  onDelete,
  onToggleActive,
  showActions = true
}) => {
  const handleEdit = useCallback(() => {
    onEdit?.(user);
  }, [onEdit, user]);

  const handleDelete = useCallback(() => {
    if (window.confirm(`Are you sure you want to delete ${user.name}?`)) {
      onDelete?.(user.id);
    }
  }, [onDelete, user.id, user.name]);

  const handleToggleActive = useCallback(() => {
    onToggleActive?.(user.id);
  }, [onToggleActive, user.id]);

  return (
    <div className={`user-card ${user.isActive ? 'active' : 'inactive'}`} data-testid="user-card">
      <div className="user-avatar">
        {user.avatar ? (
          <img src={user.avatar} alt={`${user.name}'s avatar`} />
        ) : (
          <div className="avatar-placeholder">{user.name.charAt(0).toUpperCase()}</div>
        )}
      </div>
      
      <div className="user-info">
        <h3 className="user-name">{user.name}</h3>
        <p className="user-email">{user.email}</p>
        <span className={`user-role role-${user.role}`}>{user.role}</span>
        <span className={`user-status ${user.isActive ? 'active' : 'inactive'}`}>
          {user.isActive ? 'Active' : 'Inactive'}
        </span>
      </div>

      {showActions && (
        <div className="user-actions">
          {onEdit && (
            <Button variant="secondary" size="small" onClick={handleEdit} data-testid="edit-button">
              Edit
            </Button>
          )}
          {onToggleActive && (
            <Button
              variant="secondary"
              size="small"
              onClick={handleToggleActive}
              data-testid="toggle-active-button"
            >
              {user.isActive ? 'Deactivate' : 'Activate'}
            </Button>
          )}
          {onDelete && (
            <Button variant="danger" size="small" onClick={handleDelete} data-testid="delete-button">
              Delete
            </Button>
          )}
        </div>
      )}
    </div>
  );
};

// Product card with add to cart functionality
interface ProductCardProps {
  product: Product;
  onAddToCart?: (product: Product, quantity: number) => void;
  showAddToCart?: boolean;
}

const ProductCard: React.FC<ProductCardProps> = ({
  product,
  onAddToCart,
  showAddToCart = true
}) => {
  const [quantity, setQuantity] = useState(1);
  const [isAdding, setIsAdding] = useState(false);

  const handleAddToCart = useCallback(async () => {
    if (!onAddToCart || !product.inStock) return;

    setIsAdding(true);
    try {
      // Simulate async operation
      await new Promise(resolve => setTimeout(resolve, 500));
      onAddToCart(product, quantity);
      setQuantity(1); // Reset quantity after adding
    } finally {
      setIsAdding(false);
    }
  }, [onAddToCart, product, quantity]);

  const incrementQuantity = useCallback(() => {
    setQuantity(prev => prev + 1);
  }, []);

  const decrementQuantity = useCallback(() => {
    setQuantity(prev => Math.max(1, prev - 1));
  }, []);

  return (
    <div className={`product-card ${!product.inStock ? 'out-of-stock' : ''}`} data-testid="product-card">
      {product.imageUrl && (
        <img src={product.imageUrl} alt={product.name} className="product-image" />
      )}
      
      <div className="product-info">
        <h3 className="product-name">{product.name}</h3>
        <p className="product-description">{product.description}</p>
        <span className="product-category">{product.category}</span>
        <span className="product-price">${product.price.toFixed(2)}</span>
        
        {!product.inStock && (
          <span className="out-of-stock-badge">Out of Stock</span>
        )}
      </div>

      {showAddToCart && product.inStock && (
        <div className="add-to-cart-section">
          <div className="quantity-controls">
            <Button
              variant="secondary"
              size="small"
              onClick={decrementQuantity}
              disabled={quantity <= 1}
              data-testid="decrement-quantity"
            >
              -
            </Button>
            <span className="quantity-display" data-testid="quantity-display">
              {quantity}
            </span>
            <Button
              variant="secondary"
              size="small"
              onClick={incrementQuantity}
              data-testid="increment-quantity"
            >
              +
            </Button>
          </div>
          
          <Button
            variant="primary"
            onClick={handleAddToCart}
            disabled={isAdding}
            data-testid="add-to-cart-button"
          >
            {isAdding ? 'Adding...' : 'Add to Cart'}
          </Button>
        </div>
      )}
    </div>
  );
};

// Form component with validation
interface ContactFormData {
  name: string;
  email: string;
  message: string;
}

interface ContactFormProps {
  onSubmit: (data: ContactFormData) => Promise<void>;
  initialData?: Partial<ContactFormData>;
}

const ContactForm: React.FC<ContactFormProps> = ({ onSubmit, initialData = {} }) => {
  const [formData, setFormData] = useState<ContactFormData>({
    name: initialData.name || '',
    email: initialData.email || '',
    message: initialData.message || ''
  });

  const [errors, setErrors] = useState<Partial<ContactFormData>>({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submitSuccess, setSubmitSuccess] = useState(false);

  const validateForm = useCallback((): boolean => {
    const newErrors: Partial<ContactFormData> = {};

    if (!formData.name.trim()) {
      newErrors.name = 'Name is required';
    }

    if (!formData.email.trim()) {
      newErrors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = 'Email is invalid';
    }

    if (!formData.message.trim()) {
      newErrors.message = 'Message is required';
    } else if (formData.message.length < 10) {
      newErrors.message = 'Message must be at least 10 characters';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  }, [formData]);

  const handleInputChange = useCallback((field: keyof ContactFormData, value: string) => {
    setFormData(prev => ({ ...prev, [field]: value }));
    
    // Clear error when user starts typing
    if (errors[field]) {
      setErrors(prev => ({ ...prev, [field]: undefined }));
    }
  }, [errors]);

  const handleSubmit = useCallback(async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (!validateForm()) {
      return;
    }

    setIsSubmitting(true);
    try {
      await onSubmit(formData);
      setSubmitSuccess(true);
      setFormData({ name: '', email: '', message: '' }); // Reset form
    } catch (error) {
      console.error('Form submission error:', error);
    } finally {
      setIsSubmitting(false);
    }
  }, [formData, onSubmit, validateForm]);

  if (submitSuccess) {
    return (
      <div className="success-message" data-testid="success-message">
        <h3>Thank you!</h3>
        <p>Your message has been sent successfully.</p>
        <Button onClick={() => setSubmitSuccess(false)}>Send Another Message</Button>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} className="contact-form" data-testid="contact-form">
      <Input
        label="Name"
        value={formData.name}
        onChange={(value) => handleInputChange('name', value)}
        error={errors.name}
        required
        data-testid="name-input"
      />
      
      <Input
        label="Email"
        type="email"
        value={formData.email}
        onChange={(value) => handleInputChange('email', value)}
        error={errors.email}
        required
        data-testid="email-input"
      />
      
      <div className="input-group">
        <label htmlFor="message" className="input-label">
          Message<span className="required">*</span>
        </label>
        <textarea
          id="message"
          value={formData.message}
          onChange={(e) => handleInputChange('message', e.target.value)}
          className={`textarea ${errors.message ? 'input-error' : ''}`}
          rows={4}
          data-testid="message-textarea"
          aria-describedby={errors.message ? 'message-error' : undefined}
          aria-invalid={!!errors.message}
        />
        {errors.message && (
          <span id="message-error" className="error-message" role="alert">
            {errors.message}
          </span>
        )}
      </div>

      <Button
        type="submit"
        variant="primary"
        disabled={isSubmitting}
        data-testid="submit-button"
      >
        {isSubmitting ? 'Sending...' : 'Send Message'}
      </Button>
    </form>
  );
};

// Search component with debounced input
interface SearchProps {
  onSearch: (query: string) => void;
  placeholder?: string;
  debounceMs?: number;
}

const Search: React.FC<SearchProps> = ({
  onSearch,
  placeholder = 'Search...',
  debounceMs = 300
}) => {
  const [query, setQuery] = useState('');

  const debouncedOnSearch = useMemo(
    () => {
      let timeoutId: NodeJS.Timeout;
      
      return (searchQuery: string) => {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
          onSearch(searchQuery);
        }, debounceMs);
      };
    },
    [onSearch, debounceMs]
  );

  useEffect(() => {
    debouncedOnSearch(query);
  }, [query, debouncedOnSearch]);

  return (
    <div className="search-container">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder={placeholder}
        className="search-input"
        data-testid="search-input"
      />
      {query && (
        <button
          onClick={() => setQuery('')}
          className="clear-search"
          data-testid="clear-search"
          aria-label="Clear search"
        >
          Γ—
        </button>
      )}
    </div>
  );
};

βœ… Testing Basic Component Functionality

πŸ§ͺ Rendering and Props Testing

The foundation of component testing is verifying that components render correctly with different props.

// 🌟 Comprehensive basic component testing

describe('Button Component', () => {
  // βœ… Testing basic rendering
  it('should render with default props', () => {
    render(<Button>Click me</Button>);
    
    const button = screen.getByRole('button', { name: /click me/i });
    expect(button).toBeInTheDocument();
    expect(button).toHaveClass('btn', 'btn-primary', 'btn-md');
    expect(button).toHaveAttribute('type', 'button');
    expect(button).not.toBeDisabled();
  });

  it('should render with custom props', () => {
    render(
      <Button
        variant="danger"
        size="large"
        type="submit"
        disabled
        data-testid="custom-button"
      >
        Submit Form
      </Button>
    );
    
    const button = screen.getByTestId('custom-button');
    expect(button).toBeInTheDocument();
    expect(button).toHaveClass('btn', 'btn-danger', 'btn-lg');
    expect(button).toHaveAttribute('type', 'submit');
    expect(button).toBeDisabled();
    expect(button).toHaveTextContent('Submit Form');
  });

  it('should render all variant styles correctly', () => {
    const variants: Array<ButtonProps['variant']> = ['primary', 'secondary', 'danger'];
    
    variants.forEach(variant => {
      const { unmount } = render(<Button variant={variant}>Test</Button>);
      
      const button = screen.getByRole('button');
      expect(button).toHaveClass(`btn-${variant}`);
      
      unmount();
    });
  });

  it('should render all sizes correctly', () => {
    const sizes: Array<ButtonProps['size']> = ['small', 'medium', 'large'];
    
    sizes.forEach(size => {
      const { unmount } = render(<Button size={size}>Test</Button>);
      
      const button = screen.getByRole('button');
      const sizeClass = size === 'small' ? 'btn-sm' : size === 'large' ? 'btn-lg' : 'btn-md';
      expect(button).toHaveClass(sizeClass);
      
      unmount();
    });
  });

  // βœ… Testing click events
  it('should call onClick when clicked', async () => {
    const user = userEvent.setup();
    const mockOnClick = jest.fn();
    
    render(<Button onClick={mockOnClick}>Click me</Button>);
    
    const button = screen.getByRole('button');
    await user.click(button);
    
    expect(mockOnClick).toHaveBeenCalledTimes(1);
  });

  it('should not call onClick when disabled', async () => {
    const user = userEvent.setup();
    const mockOnClick = jest.fn();
    
    render(<Button onClick={mockOnClick} disabled>Click me</Button>);
    
    const button = screen.getByRole('button');
    await user.click(button);
    
    expect(mockOnClick).not.toHaveBeenCalled();
  });

  it('should handle multiple clicks', async () => {
    const user = userEvent.setup();
    const mockOnClick = jest.fn();
    
    render(<Button onClick={mockOnClick}>Click me</Button>);
    
    const button = screen.getByRole('button');
    await user.click(button);
    await user.click(button);
    await user.click(button);
    
    expect(mockOnClick).toHaveBeenCalledTimes(3);
  });

  // βœ… Testing accessibility
  it('should be accessible', () => {
    render(<Button>Accessible Button</Button>);
    
    const button = screen.getByRole('button', { name: /accessible button/i });
    expect(button).toBeInTheDocument();
    
    // Button should be keyboard accessible
    expect(button).toHaveAttribute('type', 'button');
  });

  it('should work with keyboard navigation', async () => {
    const user = userEvent.setup();
    const mockOnClick = jest.fn();
    
    render(<Button onClick={mockOnClick}>Keyboard Test</Button>);
    
    const button = screen.getByRole('button');
    button.focus();
    
    expect(button).toHaveFocus();
    
    // Press Enter
    await user.keyboard('{Enter}');
    expect(mockOnClick).toHaveBeenCalledTimes(1);
    
    // Press Space
    await user.keyboard(' ');
    expect(mockOnClick).toHaveBeenCalledTimes(2);
  });
});

describe('Input Component', () => {
  // βœ… Testing basic input functionality
  it('should render with label and handle changes', async () => {
    const user = userEvent.setup();
    const mockOnChange = jest.fn();
    
    render(
      <Input
        label="Test Input"
        value=""
        onChange={mockOnChange}
        data-testid="test-input"
      />
    );
    
    const input = screen.getByTestId('test-input');
    const label = screen.getByText('Test Input');
    
    expect(input).toBeInTheDocument();
    expect(label).toBeInTheDocument();
    expect(input).toHaveAttribute('id', 'input-test-input');
    expect(label).toHaveAttribute('for', 'input-test-input');
    
    await user.type(input, 'Hello World');
    
    expect(mockOnChange).toHaveBeenCalledTimes(11); // One call per character
    expect(mockOnChange).toHaveBeenLastCalledWith('Hello World');
  });

  it('should display error message', () => {
    render(
      <Input
        label="Email"
        value="invalid-email"
        onChange={() => {}}
        error="Please enter a valid email"
      />
    );
    
    const input = screen.getByRole('textbox', { name: /email/i });
    const errorMessage = screen.getByRole('alert');
    
    expect(input).toHaveClass('input-error');
    expect(input).toHaveAttribute('aria-invalid', 'true');
    expect(input).toHaveAttribute('aria-describedby', 'input-email-error');
    expect(errorMessage).toHaveTextContent('Please enter a valid email');
  });

  it('should handle different input types', () => {
    const types: Array<InputProps['type']> = ['text', 'email', 'password', 'number'];
    
    types.forEach(type => {
      const { unmount } = render(
        <Input label={`${type} input`} value="" onChange={() => {}} type={type} />
      );
      
      const input = screen.getByRole(type === 'password' ? 'textbox' : 'textbox');
      expect(input).toHaveAttribute('type', type);
      
      unmount();
    });
  });

  it('should show required indicator', () => {
    render(
      <Input
        label="Required Field"
        value=""
        onChange={() => {}}
        required
      />
    );
    
    const requiredIndicator = screen.getByText('*');
    expect(requiredIndicator).toBeInTheDocument();
    expect(requiredIndicator).toHaveClass('required');
  });

  it('should handle blur events', async () => {
    const user = userEvent.setup();
    const mockOnBlur = jest.fn();
    
    render(
      <Input
        label="Blur Test"
        value=""
        onChange={() => {}}
        onBlur={mockOnBlur}
      />
    );
    
    const input = screen.getByRole('textbox');
    
    await user.click(input);
    await user.tab(); // This will trigger blur
    
    expect(mockOnBlur).toHaveBeenCalledTimes(1);
  });

  it('should be disabled when disabled prop is true', () => {
    render(
      <Input
        label="Disabled Input"
        value="Cannot edit"
        onChange={() => {}}
        disabled
      />
    );
    
    const input = screen.getByRole('textbox');
    expect(input).toBeDisabled();
  });
});

describe('UserCard Component', () => {
  const mockUser: User = {
    id: 'user-123',
    name: 'John Doe',
    email: '[email protected]',
    isActive: true,
    role: 'admin'
  };

  // βœ… Testing component with complex props
  it('should render user information correctly', () => {
    render(<UserCard user={mockUser} />);
    
    const userCard = screen.getByTestId('user-card');
    expect(userCard).toHaveClass('user-card', 'active');
    
    expect(screen.getByText('John Doe')).toBeInTheDocument();
    expect(screen.getByText('[email protected]')).toBeInTheDocument();
    expect(screen.getByText('admin')).toBeInTheDocument();
    expect(screen.getByText('Active')).toBeInTheDocument();
    
    // Check avatar placeholder
    const avatarPlaceholder = screen.getByText('J');
    expect(avatarPlaceholder).toBeInTheDocument();
  });

  it('should render user with avatar image', () => {
    const userWithAvatar = { ...mockUser, avatar: 'https://example.com/avatar.jpg' };
    
    render(<UserCard user={userWithAvatar} />);
    
    const avatarImage = screen.getByAltText("John Doe's avatar");
    expect(avatarImage).toBeInTheDocument();
    expect(avatarImage).toHaveAttribute('src', 'https://example.com/avatar.jpg');
  });

  it('should render inactive user correctly', () => {
    const inactiveUser = { ...mockUser, isActive: false };
    
    render(<UserCard user={inactiveUser} />);
    
    const userCard = screen.getByTestId('user-card');
    expect(userCard).toHaveClass('user-card', 'inactive');
    expect(screen.getByText('Inactive')).toBeInTheDocument();
  });

  it('should hide actions when showActions is false', () => {
    render(<UserCard user={mockUser} showActions={false} />);
    
    expect(screen.queryByTestId('edit-button')).not.toBeInTheDocument();
    expect(screen.queryByTestId('delete-button')).not.toBeInTheDocument();
    expect(screen.queryByTestId('toggle-active-button')).not.toBeInTheDocument();
  });

  it('should call onEdit when edit button is clicked', async () => {
    const user = userEvent.setup();
    const mockOnEdit = jest.fn();
    
    render(<UserCard user={mockUser} onEdit={mockOnEdit} />);
    
    const editButton = screen.getByTestId('edit-button');
    await user.click(editButton);
    
    expect(mockOnEdit).toHaveBeenCalledTimes(1);
    expect(mockOnEdit).toHaveBeenCalledWith(mockUser);
  });

  it('should call onToggleActive when toggle button is clicked', async () => {
    const user = userEvent.setup();
    const mockOnToggleActive = jest.fn();
    
    render(<UserCard user={mockUser} onToggleActive={mockOnToggleActive} />);
    
    const toggleButton = screen.getByTestId('toggle-active-button');
    expect(toggleButton).toHaveTextContent('Deactivate');
    
    await user.click(toggleButton);
    
    expect(mockOnToggleActive).toHaveBeenCalledTimes(1);
    expect(mockOnToggleActive).toHaveBeenCalledWith('user-123');
  });

  it('should show confirm dialog before delete', async () => {
    const user = userEvent.setup();
    const mockOnDelete = jest.fn();
    
    // Mock window.confirm
    const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValue(true);
    
    render(<UserCard user={mockUser} onDelete={mockOnDelete} />);
    
    const deleteButton = screen.getByTestId('delete-button');
    await user.click(deleteButton);
    
    expect(confirmSpy).toHaveBeenCalledWith('Are you sure you want to delete John Doe?');
    expect(mockOnDelete).toHaveBeenCalledTimes(1);
    expect(mockOnDelete).toHaveBeenCalledWith('user-123');
    
    confirmSpy.mockRestore();
  });

  it('should not delete when user cancels confirm dialog', async () => {
    const user = userEvent.setup();
    const mockOnDelete = jest.fn();
    
    // Mock window.confirm to return false
    const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValue(false);
    
    render(<UserCard user={mockUser} onDelete={mockOnDelete} />);
    
    const deleteButton = screen.getByTestId('delete-button');
    await user.click(deleteButton);
    
    expect(confirmSpy).toHaveBeenCalledWith('Are you sure you want to delete John Doe?');
    expect(mockOnDelete).not.toHaveBeenCalled();
    
    confirmSpy.mockRestore();
  });
});

🎭 Testing User Interactions and Events

πŸ–±οΈ Advanced User Event Testing

Testing user interactions requires simulating real user behavior with mouse, keyboard, and form interactions.

// 🌟 Comprehensive user interaction testing

describe('ProductCard Component - User Interactions', () => {
  const mockProduct: Product = {
    id: 'prod-123',
    name: 'Awesome Widget',
    price: 29.99,
    description: 'A really awesome widget for your needs',
    category: 'Widgets',
    inStock: true,
    imageUrl: 'https://example.com/widget.jpg'
  };

  // βœ… Testing quantity controls
  it('should handle quantity increment and decrement', async () => {
    const user = userEvent.setup();
    const mockOnAddToCart = jest.fn();
    
    render(<ProductCard product={mockProduct} onAddToCart={mockOnAddToCart} />);
    
    const quantityDisplay = screen.getByTestId('quantity-display');
    const incrementButton = screen.getByTestId('increment-quantity');
    const decrementButton = screen.getByTestId('decrement-quantity');
    
    // Initial state
    expect(quantityDisplay).toHaveTextContent('1');
    expect(decrementButton).toBeDisabled();
    
    // Increment quantity
    await user.click(incrementButton);
    expect(quantityDisplay).toHaveTextContent('2');
    expect(decrementButton).not.toBeDisabled();
    
    // Increment again
    await user.click(incrementButton);
    expect(quantityDisplay).toHaveTextContent('3');
    
    // Decrement quantity
    await user.click(decrementButton);
    expect(quantityDisplay).toHaveTextContent('2');
    
    // Decrement to minimum
    await user.click(decrementButton);
    expect(quantityDisplay).toHaveTextContent('1');
    expect(decrementButton).toBeDisabled();
  });

  it('should add product to cart with correct quantity', async () => {
    const user = userEvent.setup();
    const mockOnAddToCart = jest.fn().mockResolvedValue(undefined);
    
    render(<ProductCard product={mockProduct} onAddToCart={mockOnAddToCart} />);
    
    // Increase quantity to 3
    const incrementButton = screen.getByTestId('increment-quantity');
    await user.click(incrementButton);
    await user.click(incrementButton);
    
    const addToCartButton = screen.getByTestId('add-to-cart-button');
    await user.click(addToCartButton);
    
    expect(mockOnAddToCart).toHaveBeenCalledTimes(1);
    expect(mockOnAddToCart).toHaveBeenCalledWith(mockProduct, 3);
  });

  it('should show loading state during add to cart', async () => {
    const user = userEvent.setup();
    const mockOnAddToCart = jest.fn(() => new Promise(resolve => setTimeout(resolve, 1000)));
    
    render(<ProductCard product={mockProduct} onAddToCart={mockOnAddToCart} />);
    
    const addToCartButton = screen.getByTestId('add-to-cart-button');
    
    // Initial state
    expect(addToCartButton).toHaveTextContent('Add to Cart');
    expect(addToCartButton).not.toBeDisabled();
    
    // Click button
    await user.click(addToCartButton);
    
    // Should show loading state
    expect(addToCartButton).toHaveTextContent('Adding...');
    expect(addToCartButton).toBeDisabled();
    
    // Wait for async operation to complete
    await waitFor(() => {
      expect(addToCartButton).toHaveTextContent('Add to Cart');
      expect(addToCartButton).not.toBeDisabled();
    });
  });

  it('should reset quantity after successful add to cart', async () => {
    const user = userEvent.setup();
    const mockOnAddToCart = jest.fn().mockResolvedValue(undefined);
    
    render(<ProductCard product={mockProduct} onAddToCart={mockOnAddToCart} />);
    
    // Set quantity to 5
    const incrementButton = screen.getByTestId('increment-quantity');
    for (let i = 0; i < 4; i++) {
      await user.click(incrementButton);
    }
    
    const quantityDisplay = screen.getByTestId('quantity-display');
    expect(quantityDisplay).toHaveTextContent('5');
    
    // Add to cart
    const addToCartButton = screen.getByTestId('add-to-cart-button');
    await user.click(addToCartButton);
    
    // Wait for reset
    await waitFor(() => {
      expect(quantityDisplay).toHaveTextContent('1');
    });
  });

  it('should hide add to cart section for out of stock products', () => {
    const outOfStockProduct = { ...mockProduct, inStock: false };
    
    render(<ProductCard product={outOfStockProduct} />);
    
    expect(screen.getByText('Out of Stock')).toBeInTheDocument();
    expect(screen.queryByTestId('add-to-cart-button')).not.toBeInTheDocument();
    expect(screen.queryByTestId('quantity-display')).not.toBeInTheDocument();
  });

  it('should not show add to cart when showAddToCart is false', () => {
    render(<ProductCard product={mockProduct} showAddToCart={false} />);
    
    expect(screen.queryByTestId('add-to-cart-button')).not.toBeInTheDocument();
    expect(screen.queryByTestId('quantity-display')).not.toBeInTheDocument();
  });
});

describe('ContactForm Component - Form Interactions', () => {
  const mockOnSubmit = jest.fn();

  beforeEach(() => {
    mockOnSubmit.mockClear();
  });

  // βœ… Testing form validation
  it('should validate required fields', async () => {
    const user = userEvent.setup();
    
    render(<ContactForm onSubmit={mockOnSubmit} />);
    
    const submitButton = screen.getByTestId('submit-button');
    await user.click(submitButton);
    
    // Should show validation errors
    expect(screen.getByText('Name is required')).toBeInTheDocument();
    expect(screen.getByText('Email is required')).toBeInTheDocument();
    expect(screen.getByText('Message is required')).toBeInTheDocument();
    
    // Form should not be submitted
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });

  it('should validate email format', async () => {
    const user = userEvent.setup();
    
    render(<ContactForm onSubmit={mockOnSubmit} />);
    
    const emailInput = screen.getByTestId('email-input');
    
    // Enter invalid email
    await user.type(emailInput, 'invalid-email');
    
    const submitButton = screen.getByTestId('submit-button');
    await user.click(submitButton);
    
    expect(screen.getByText('Email is invalid')).toBeInTheDocument();
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });

  it('should validate message length', async () => {
    const user = userEvent.setup();
    
    render(<ContactForm onSubmit={mockOnSubmit} />);
    
    const messageTextarea = screen.getByTestId('message-textarea');
    
    // Enter short message
    await user.type(messageTextarea, 'Short');
    
    const submitButton = screen.getByTestId('submit-button');
    await user.click(submitButton);
    
    expect(screen.getByText('Message must be at least 10 characters')).toBeInTheDocument();
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });

  it('should clear errors when user starts typing', async () => {
    const user = userEvent.setup();
    
    render(<ContactForm onSubmit={mockOnSubmit} />);
    
    // Submit empty form to trigger errors
    const submitButton = screen.getByTestId('submit-button');
    await user.click(submitButton);
    
    expect(screen.getByText('Name is required')).toBeInTheDocument();
    
    // Start typing in name field
    const nameInput = screen.getByTestId('name-input');
    await user.type(nameInput, 'J');
    
    // Error should be cleared
    expect(screen.queryByText('Name is required')).not.toBeInTheDocument();
  });

  it('should submit form with valid data', async () => {
    const user = userEvent.setup();
    mockOnSubmit.mockResolvedValue(undefined);
    
    render(<ContactForm onSubmit={mockOnSubmit} />);
    
    // Fill out form
    const nameInput = screen.getByTestId('name-input');
    const emailInput = screen.getByTestId('email-input');
    const messageTextarea = screen.getByTestId('message-textarea');
    
    await user.type(nameInput, 'John Doe');
    await user.type(emailInput, '[email protected]');
    await user.type(messageTextarea, 'This is a test message that is long enough.');
    
    const submitButton = screen.getByTestId('submit-button');
    await user.click(submitButton);
    
    expect(mockOnSubmit).toHaveBeenCalledTimes(1);
    expect(mockOnSubmit).toHaveBeenCalledWith({
      name: 'John Doe',
      email: '[email protected]',
      message: 'This is a test message that is long enough.'
    });
  });

  it('should show loading state during submission', async () => {
    const user = userEvent.setup();
    mockOnSubmit.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 1000)));
    
    render(<ContactForm onSubmit={mockOnSubmit} />);
    
    // Fill out form
    await user.type(screen.getByTestId('name-input'), 'John Doe');
    await user.type(screen.getByTestId('email-input'), '[email protected]');
    await user.type(screen.getByTestId('message-textarea'), 'This is a test message.');
    
    const submitButton = screen.getByTestId('submit-button');
    
    // Initial state
    expect(submitButton).toHaveTextContent('Send Message');
    expect(submitButton).not.toBeDisabled();
    
    // Submit form
    await user.click(submitButton);
    
    // Should show loading state
    expect(submitButton).toHaveTextContent('Sending...');
    expect(submitButton).toBeDisabled();
    
    // Wait for submission to complete
    await waitFor(() => {
      expect(screen.getByTestId('success-message')).toBeInTheDocument();
    });
  });

  it('should show success message and reset form after submission', async () => {
    const user = userEvent.setup();
    mockOnSubmit.mockResolvedValue(undefined);
    
    render(<ContactForm onSubmit={mockOnSubmit} />);
    
    // Fill and submit form
    await user.type(screen.getByTestId('name-input'), 'John Doe');
    await user.type(screen.getByTestId('email-input'), '[email protected]');
    await user.type(screen.getByTestId('message-textarea'), 'Test message.');
    
    await user.click(screen.getByTestId('submit-button'));
    
    // Should show success message
    await waitFor(() => {
      expect(screen.getByTestId('success-message')).toBeInTheDocument();
      expect(screen.getByText('Thank you!')).toBeInTheDocument();
      expect(screen.getByText('Your message has been sent successfully.')).toBeInTheDocument();
    });
    
    // Original form should not be visible
    expect(screen.queryByTestId('contact-form')).not.toBeInTheDocument();
    
    // Should be able to send another message
    const sendAnotherButton = screen.getByText('Send Another Message');
    await user.click(sendAnotherButton);
    
    // Form should be visible again with empty fields
    expect(screen.getByTestId('contact-form')).toBeInTheDocument();
    expect(screen.getByTestId('name-input')).toHaveValue('');
    expect(screen.getByTestId('email-input')).toHaveValue('');
    expect(screen.getByTestId('message-textarea')).toHaveValue('');
  });

  it('should populate form with initial data', () => {
    const initialData = {
      name: 'Jane Doe',
      email: '[email protected]',
      message: 'Pre-filled message'
    };
    
    render(<ContactForm onSubmit={mockOnSubmit} initialData={initialData} />);
    
    expect(screen.getByTestId('name-input')).toHaveValue('Jane Doe');
    expect(screen.getByTestId('email-input')).toHaveValue('[email protected]');
    expect(screen.getByTestId('message-textarea')).toHaveValue('Pre-filled message');
  });
});

describe('Search Component - Debounced Input', () => {
  // βœ… Testing debounced search
  it('should debounce search input', async () => {
    const user = userEvent.setup();
    const mockOnSearch = jest.fn();
    
    render(<Search onSearch={mockOnSearch} debounceMs={300} />);
    
    const searchInput = screen.getByTestId('search-input');
    
    // Type quickly
    await user.type(searchInput, 'test query');
    
    // Should not have called onSearch yet
    expect(mockOnSearch).not.toHaveBeenCalled();
    
    // Wait for debounce
    await waitFor(() => {
      expect(mockOnSearch).toHaveBeenCalledTimes(1);
      expect(mockOnSearch).toHaveBeenCalledWith('test query');
    }, { timeout: 500 });
  });

  it('should clear search when clear button is clicked', async () => {
    const user = userEvent.setup();
    const mockOnSearch = jest.fn();
    
    render(<Search onSearch={mockOnSearch} />);
    
    const searchInput = screen.getByTestId('search-input');
    
    // Type something
    await user.type(searchInput, 'search term');
    
    // Clear button should appear
    const clearButton = screen.getByTestId('clear-search');
    expect(clearButton).toBeInTheDocument();
    
    // Click clear button
    await user.click(clearButton);
    
    // Input should be cleared
    expect(searchInput).toHaveValue('');
    
    // Clear button should disappear
    expect(screen.queryByTestId('clear-search')).not.toBeInTheDocument();
    
    // Should trigger search with empty string
    await waitFor(() => {
      expect(mockOnSearch).toHaveBeenLastCalledWith('');
    });
  });

  it('should use custom placeholder', () => {
    render(<Search onSearch={() => {}} placeholder="Search products..." />);
    
    const searchInput = screen.getByTestId('search-input');
    expect(searchInput).toHaveAttribute('placeholder', 'Search products...');
  });

  it('should handle rapid typing correctly', async () => {
    const user = userEvent.setup();
    const mockOnSearch = jest.fn();
    
    render(<Search onSearch={mockOnSearch} debounceMs={200} />);
    
    const searchInput = screen.getByTestId('search-input');
    
    // Type, then quickly change
    await user.type(searchInput, 'first');
    await user.clear(searchInput);
    await user.type(searchInput, 'second query');
    
    // Wait for debounce
    await waitFor(() => {
      expect(mockOnSearch).toHaveBeenCalledWith('second query');
    }, { timeout: 400 });
    
    // Should only have been called with the final value
    expect(mockOnSearch).toHaveBeenCalledTimes(1);
  });
});

πŸ”„ Testing State Management and Hooks

🎣 Advanced Hook and State Testing

Testing components with complex state management requires careful handling of state updates and side effects.

// 🌟 Comprehensive state and hook testing

describe('Components with State Management', () => {
  // βœ… Testing useState behavior
  describe('Counter Component with useState', () => {
    const Counter: React.FC = () => {
      const [count, setCount] = useState(0);
      const [step, setStep] = useState(1);

      return (
        <div data-testid="counter">
          <div data-testid="count-display">Count: {count}</div>
          <div data-testid="step-display">Step: {step}</div>
          
          <button onClick={() => setCount(c => c + step)} data-testid="increment">
            +{step}
          </button>
          <button onClick={() => setCount(c => c - step)} data-testid="decrement">
            -{step}
          </button>
          <button onClick={() => setCount(0)} data-testid="reset">
            Reset
          </button>
          
          <input
            type="number"
            value={step}
            onChange={(e) => setStep(Number(e.target.value))}
            data-testid="step-input"
          />
        </div>
      );
    };

    it('should initialize with default state', () => {
      render(<Counter />);
      
      expect(screen.getByTestId('count-display')).toHaveTextContent('Count: 0');
      expect(screen.getByTestId('step-display')).toHaveTextContent('Step: 1');
    });

    it('should increment and decrement by step', async () => {
      const user = userEvent.setup();
      render(<Counter />);
      
      const incrementButton = screen.getByTestId('increment');
      const decrementButton = screen.getByTestId('decrement');
      const countDisplay = screen.getByTestId('count-display');
      
      // Increment
      await user.click(incrementButton);
      expect(countDisplay).toHaveTextContent('Count: 1');
      
      await user.click(incrementButton);
      expect(countDisplay).toHaveTextContent('Count: 2');
      
      // Decrement
      await user.click(decrementButton);
      expect(countDisplay).toHaveTextContent('Count: 1');
    });

    it('should change step size', async () => {
      const user = userEvent.setup();
      render(<Counter />);
      
      const stepInput = screen.getByTestId('step-input');
      const incrementButton = screen.getByTestId('increment');
      const countDisplay = screen.getByTestId('count-display');
      
      // Change step to 5
      await user.clear(stepInput);
      await user.type(stepInput, '5');
      
      expect(screen.getByTestId('step-display')).toHaveTextContent('Step: 5');
      
      // Increment should now add 5
      await user.click(incrementButton);
      expect(countDisplay).toHaveTextContent('Count: 5');
    });

    it('should reset count to zero', async () => {
      const user = userEvent.setup();
      render(<Counter />);
      
      const incrementButton = screen.getByTestId('increment');
      const resetButton = screen.getByTestId('reset');
      const countDisplay = screen.getByTestId('count-display');
      
      // Increment a few times
      await user.click(incrementButton);
      await user.click(incrementButton);
      await user.click(incrementButton);
      expect(countDisplay).toHaveTextContent('Count: 3');
      
      // Reset
      await user.click(resetButton);
      expect(countDisplay).toHaveTextContent('Count: 0');
    });
  });

  // βœ… Testing useEffect behavior
  describe('Timer Component with useEffect', () => {
    const Timer: React.FC<{ interval?: number; autoStart?: boolean }> = ({
      interval = 1000,
      autoStart = false
    }) => {
      const [seconds, setSeconds] = useState(0);
      const [isRunning, setIsRunning] = useState(autoStart);

      useEffect(() => {
        let intervalId: NodeJS.Timeout;

        if (isRunning) {
          intervalId = setInterval(() => {
            setSeconds(s => s + 1);
          }, interval);
        }

        return () => {
          if (intervalId) {
            clearInterval(intervalId);
          }
        };
      }, [isRunning, interval]);

      return (
        <div data-testid="timer">
          <div data-testid="seconds-display">{seconds}s</div>
          <button
            onClick={() => setIsRunning(!isRunning)}
            data-testid="toggle-timer"
          >
            {isRunning ? 'Pause' : 'Start'}
          </button>
          <button
            onClick={() => setSeconds(0)}
            data-testid="reset-timer"
          >
            Reset
          </button>
        </div>
      );
    };

    beforeEach(() => {
      jest.useFakeTimers();
    });

    afterEach(() => {
      jest.useRealTimers();
    });

    it('should start and stop timer', async () => {
      const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
      render(<Timer />);
      
      const toggleButton = screen.getByTestId('toggle-timer');
      const secondsDisplay = screen.getByTestId('seconds-display');
      
      // Initially stopped
      expect(secondsDisplay).toHaveTextContent('0s');
      expect(toggleButton).toHaveTextContent('Start');
      
      // Start timer
      await user.click(toggleButton);
      expect(toggleButton).toHaveTextContent('Pause');
      
      // Advance time
      act(() => {
        jest.advanceTimersByTime(3000);
      });
      
      expect(secondsDisplay).toHaveTextContent('3s');
      
      // Pause timer
      await user.click(toggleButton);
      expect(toggleButton).toHaveTextContent('Start');
      
      // Advance time - should not change
      act(() => {
        jest.advanceTimersByTime(2000);
      });
      
      expect(secondsDisplay).toHaveTextContent('3s');
    });

    it('should auto-start when autoStart is true', () => {
      render(<Timer autoStart />);
      
      const toggleButton = screen.getByTestId('toggle-timer');
      expect(toggleButton).toHaveTextContent('Pause');
      
      // Advance time
      act(() => {
        jest.advanceTimersByTime(2000);
      });
      
      expect(screen.getByTestId('seconds-display')).toHaveTextContent('2s');
    });

    it('should reset timer', async () => {
      const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
      render(<Timer autoStart />);
      
      // Let timer run
      act(() => {
        jest.advanceTimersByTime(5000);
      });
      
      expect(screen.getByTestId('seconds-display')).toHaveTextContent('5s');
      
      // Reset
      const resetButton = screen.getByTestId('reset-timer');
      await user.click(resetButton);
      
      expect(screen.getByTestId('seconds-display')).toHaveTextContent('0s');
    });

    it('should use custom interval', () => {
      render(<Timer interval={500} autoStart />);
      
      // Advance by 1 second (2 intervals of 500ms)
      act(() => {
        jest.advanceTimersByTime(1000);
      });
      
      expect(screen.getByTestId('seconds-display')).toHaveTextContent('2s');
    });
  });

  // βœ… Testing useCallback and useMemo
  describe('Optimized Component with useCallback and useMemo', () => {
    interface TodoItem {
      id: string;
      text: string;
      completed: boolean;
    }

    const TodoList: React.FC = () => {
      const [todos, setTodos] = useState<TodoItem[]>([]);
      const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
      const [renderCount, setRenderCount] = useState(0);

      // Increment render count on every render
      useEffect(() => {
        setRenderCount(c => c + 1);
      });

      const addTodo = useCallback((text: string) => {
        const newTodo: TodoItem = {
          id: Date.now().toString(),
          text,
          completed: false
        };
        setTodos(todos => [...todos, newTodo]);
      }, []);

      const toggleTodo = useCallback((id: string) => {
        setTodos(todos => 
          todos.map(todo => 
            todo.id === id ? { ...todo, completed: !todo.completed } : todo
          )
        );
      }, []);

      const filteredTodos = useMemo(() => {
        return todos.filter(todo => {
          if (filter === 'active') return !todo.completed;
          if (filter === 'completed') return todo.completed;
          return true;
        });
      }, [todos, filter]);

      const todoStats = useMemo(() => {
        const total = todos.length;
        const completed = todos.filter(t => t.completed).length;
        const active = total - completed;
        return { total, completed, active };
      }, [todos]);

      return (
        <div data-testid="todo-list">
          <div data-testid="render-count">Renders: {renderCount}</div>
          <div data-testid="stats">
            Total: {todoStats.total}, Active: {todoStats.active}, Completed: {todoStats.completed}
          </div>
          
          <div>
            <button onClick={() => addTodo('New Todo')} data-testid="add-todo">
              Add Todo
            </button>
          </div>
          
          <div>
            <button
              onClick={() => setFilter('all')}
              data-active={filter === 'all'}
              data-testid="filter-all"
            >
              All
            </button>
            <button
              onClick={() => setFilter('active')}
              data-active={filter === 'active'}
              data-testid="filter-active"
            >
              Active
            </button>
            <button
              onClick={() => setFilter('completed')}
              data-active={filter === 'completed'}
              data-testid="filter-completed"
            >
              Completed
            </button>
          </div>
          
          <div data-testid="filtered-todos">
            {filteredTodos.map(todo => (
              <div key={todo.id} data-testid={`todo-${todo.id}`}>
                <span>{todo.text}</span>
                <button
                  onClick={() => toggleTodo(todo.id)}
                  data-testid={`toggle-${todo.id}`}
                >
                  {todo.completed ? 'Undo' : 'Complete'}
                </button>
              </div>
            ))}
          </div>
        </div>
      );
    };

    it('should add and display todos', async () => {
      const user = userEvent.setup();
      render(<TodoList />);
      
      const addButton = screen.getByTestId('add-todo');
      const stats = screen.getByTestId('stats');
      
      // Initial state
      expect(stats).toHaveTextContent('Total: 0, Active: 0, Completed: 0');
      
      // Add todos
      await user.click(addButton);
      await user.click(addButton);
      await user.click(addButton);
      
      expect(stats).toHaveTextContent('Total: 3, Active: 3, Completed: 0');
      
      // Check todos are displayed
      expect(screen.getAllByText('New Todo')).toHaveLength(3);
    });

    it('should toggle todo completion', async () => {
      const user = userEvent.setup();
      render(<TodoList />);
      
      // Add a todo
      await user.click(screen.getByTestId('add-todo'));
      
      const stats = screen.getByTestId('stats');
      expect(stats).toHaveTextContent('Total: 1, Active: 1, Completed: 0');
      
      // Find and toggle the todo
      const toggleButton = screen.getByText('Complete');
      await user.click(toggleButton);
      
      expect(stats).toHaveTextContent('Total: 1, Active: 0, Completed: 1');
      expect(screen.getByText('Undo')).toBeInTheDocument();
      
      // Toggle back
      await user.click(screen.getByText('Undo'));
      expect(stats).toHaveTextContent('Total: 1, Active: 1, Completed: 0');
    });

    it('should filter todos correctly', async () => {
      const user = userEvent.setup();
      render(<TodoList />);
      
      // Add 3 todos
      await user.click(screen.getByTestId('add-todo'));
      await user.click(screen.getByTestId('add-todo'));
      await user.click(screen.getByTestId('add-todo'));
      
      // Complete first todo
      const toggleButtons = screen.getAllByText('Complete');
      await user.click(toggleButtons[0]);
      
      // Test filtering
      const filteredTodos = screen.getByTestId('filtered-todos');
      
      // All filter (default)
      expect(filteredTodos.children).toHaveLength(3);
      
      // Active filter
      await user.click(screen.getByTestId('filter-active'));
      expect(filteredTodos.children).toHaveLength(2);
      
      // Completed filter
      await user.click(screen.getByTestId('filter-completed'));
      expect(filteredTodos.children).toHaveLength(1);
      
      // Back to all
      await user.click(screen.getByTestId('filter-all'));
      expect(filteredTodos.children).toHaveLength(3);
    });
  });
});

🌐 Testing Context and Async Operations

πŸ”„ Advanced Context and Async Testing

Testing components that use Context and async operations requires proper setup and waiting for async updates.

// 🌟 Comprehensive context and async testing

describe('Context and Async Testing', () => {
  // βœ… Testing React Context
  describe('App Context Provider and Consumers', () => {
    const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
      const [user, setUser] = useState<User | null>(null);
      const [cart, setCart] = useState<CartItem[]>([]);

      const addToCart = useCallback((product: Product, quantity: number) => {
        setCart(prevCart => {
          const existingItem = prevCart.find(item => item.product.id === product.id);
          
          if (existingItem) {
            return prevCart.map(item =>
              item.product.id === product.id
                ? { ...item, quantity: item.quantity + quantity }
                : item
            );
          }
          
          return [...prevCart, { product, quantity }];
        });
      }, []);

      const removeFromCart = useCallback((productId: string) => {
        setCart(prevCart => prevCart.filter(item => item.product.id !== productId));
      }, []);

      const updateCartQuantity = useCallback((productId: string, quantity: number) => {
        if (quantity <= 0) {
          removeFromCart(productId);
          return;
        }
        
        setCart(prevCart =>
          prevCart.map(item =>
            item.product.id === productId
              ? { ...item, quantity }
              : item
          )
        );
      }, [removeFromCart]);

      const login = useCallback((userData: User) => {
        setUser(userData);
      }, []);

      const logout = useCallback(() => {
        setUser(null);
        setCart([]);
      }, []);

      const contextValue: AppContextType = {
        user,
        cart,
        addToCart,
        removeFromCart,
        updateCartQuantity,
        login,
        logout
      };

      return (
        <AppContext.Provider value={contextValue}>
          {children}
        </AppContext.Provider>
      );
    };

    const UserProfile: React.FC = () => {
      const { user, login, logout } = useAppContext();

      const handleLogin = () => {
        const mockUser: User = {
          id: 'user-123',
          name: 'John Doe',
          email: '[email protected]',
          isActive: true,
          role: 'user'
        };
        login(mockUser);
      };

      if (!user) {
        return (
          <div>
            <div data-testid="login-prompt">Please log in</div>
            <button onClick={handleLogin} data-testid="login-button">
              Login
            </button>
          </div>
        );
      }

      return (
        <div data-testid="user-profile">
          <div data-testid="user-name">Welcome, {user.name}!</div>
          <div data-testid="user-email">{user.email}</div>
          <button onClick={logout} data-testid="logout-button">
            Logout
          </button>
        </div>
      );
    };

    const CartSummary: React.FC = () => {
      const { cart, updateCartQuantity, removeFromCart } = useAppContext();

      const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
      const totalPrice = cart.reduce((sum, item) => sum + (item.product.price * item.quantity), 0);

      return (
        <div data-testid="cart-summary">
          <div data-testid="total-items">Items: {totalItems}</div>
          <div data-testid="total-price">Total: ${totalPrice.toFixed(2)}</div>
          
          <div data-testid="cart-items">
            {cart.map(item => (
              <div key={item.product.id} data-testid={`cart-item-${item.product.id}`}>
                <span>{item.product.name}</span>
                <span>Qty: {item.quantity}</span>
                <span>${(item.product.price * item.quantity).toFixed(2)}</span>
                <button
                  onClick={() => updateCartQuantity(item.product.id, item.quantity + 1)}
                  data-testid={`increase-${item.product.id}`}
                >
                  +
                </button>
                <button
                  onClick={() => updateCartQuantity(item.product.id, item.quantity - 1)}
                  data-testid={`decrease-${item.product.id}`}
                >
                  -
                </button>
                <button
                  onClick={() => removeFromCart(item.product.id)}
                  data-testid={`remove-${item.product.id}`}
                >
                  Remove
                </button>
              </div>
            ))}
          </div>
        </div>
      );
    };

    const TestApp: React.FC = () => {
      const { addToCart } = useAppContext();

      const mockProduct: Product = {
        id: 'prod-123',
        name: 'Test Product',
        price: 19.99,
        description: 'A test product',
        category: 'Test',
        inStock: true
      };

      return (
        <div>
          <UserProfile />
          <CartSummary />
          <button
            onClick={() => addToCart(mockProduct, 2)}
            data-testid="add-product-button"
          >
            Add Test Product
          </button>
        </div>
      );
    };

    it('should provide context to child components', async () => {
      const user = userEvent.setup();
      
      render(
        <AppProvider>
          <TestApp />
        </AppProvider>
      );

      // Initially should show login prompt
      expect(screen.getByTestId('login-prompt')).toBeInTheDocument();
      expect(screen.getByTestId('total-items')).toHaveTextContent('Items: 0');

      // Login
      await user.click(screen.getByTestId('login-button'));

      // Should show user profile
      expect(screen.getByTestId('user-profile')).toBeInTheDocument();
      expect(screen.getByTestId('user-name')).toHaveTextContent('Welcome, John Doe!');
    });

    it('should manage cart state across components', async () => {
      const user = userEvent.setup();
      
      render(
        <AppProvider>
          <TestApp />
        </AppProvider>
      );

      // Add product to cart
      await user.click(screen.getByTestId('add-product-button'));

      // Check cart summary
      expect(screen.getByTestId('total-items')).toHaveTextContent('Items: 2');
      expect(screen.getByTestId('total-price')).toHaveTextContent('Total: $39.98');

      // Check cart item
      const cartItem = screen.getByTestId('cart-item-prod-123');
      expect(cartItem).toHaveTextContent('Test Product');
      expect(cartItem).toHaveTextContent('Qty: 2');
    });

    it('should update cart quantities', async () => {
      const user = userEvent.setup();
      
      render(
        <AppProvider>
          <TestApp />
        </AppProvider>
      );

      // Add product to cart
      await user.click(screen.getByTestId('add-product-button'));

      // Increase quantity
      await user.click(screen.getByTestId('increase-prod-123'));
      expect(screen.getByTestId('total-items')).toHaveTextContent('Items: 3');

      // Decrease quantity
      await user.click(screen.getByTestId('decrease-prod-123'));
      expect(screen.getByTestId('total-items')).toHaveTextContent('Items: 2');

      // Decrease to zero (should remove item)
      await user.click(screen.getByTestId('decrease-prod-123'));
      await user.click(screen.getByTestId('decrease-prod-123'));
      expect(screen.getByTestId('total-items')).toHaveTextContent('Items: 0');
    });

    it('should clear cart on logout', async () => {
      const user = userEvent.setup();
      
      render(
        <AppProvider>
          <TestApp />
        </AppProvider>
      );

      // Login and add items
      await user.click(screen.getByTestId('login-button'));
      await user.click(screen.getByTestId('add-product-button'));
      
      expect(screen.getByTestId('total-items')).toHaveTextContent('Items: 2');

      // Logout
      await user.click(screen.getByTestId('logout-button'));

      // Cart should be cleared
      expect(screen.getByTestId('total-items')).toHaveTextContent('Items: 0');
      expect(screen.getByTestId('login-prompt')).toBeInTheDocument();
    });

    it('should throw error when context is used outside provider', () => {
      // Mock console.error to avoid cluttering test output
      const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

      expect(() => {
        render(<UserProfile />);
      }).toThrow('useAppContext must be used within AppProvider');

      consoleSpy.mockRestore();
    });
  });

  // βœ… Testing async operations
  describe('Async Data Fetching Component', () => {
    interface ApiUser {
      id: string;
      name: string;
      email: string;
    }

    const UserList: React.FC = () => {
      const [users, setUsers] = useState<ApiUser[]>([]);
      const [loading, setLoading] = useState(false);
      const [error, setError] = useState<string | null>(null);

      const fetchUsers = useCallback(async () => {
        setLoading(true);
        setError(null);
        
        try {
          const response = await fetch('/api/users');
          
          if (!response.ok) {
            throw new Error('Failed to fetch users');
          }
          
          const userData = await response.json();
          setUsers(userData);
        } catch (err) {
          setError(err instanceof Error ? err.message : 'Unknown error');
        } finally {
          setLoading(false);
        }
      }, []);

      useEffect(() => {
        fetchUsers();
      }, [fetchUsers]);

      if (loading) {
        return <div data-testid="loading">Loading users...</div>;
      }

      if (error) {
        return (
          <div>
            <div data-testid="error">Error: {error}</div>
            <button onClick={fetchUsers} data-testid="retry-button">
              Retry
            </button>
          </div>
        );
      }

      return (
        <div data-testid="user-list">
          <button onClick={fetchUsers} data-testid="refresh-button">
            Refresh
          </button>
          
          {users.length === 0 ? (
            <div data-testid="no-users">No users found</div>
          ) : (
            <div data-testid="users">
              {users.map(user => (
                <div key={user.id} data-testid={`user-${user.id}`}>
                  <span>{user.name}</span>
                  <span>{user.email}</span>
                </div>
              ))}
            </div>
          )}
        </div>
      );
    };

    beforeEach(() => {
      global.fetch = jest.fn();
    });

    afterEach(() => {
      jest.resetAllMocks();
    });

    it('should show loading state initially', () => {
      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      mockFetch.mockImplementation(() => new Promise(() => {})); // Never resolves

      render(<UserList />);

      expect(screen.getByTestId('loading')).toBeInTheDocument();
    });

    it('should display users after successful fetch', async () => {
      const mockUsers: ApiUser[] = [
        { id: '1', name: 'John Doe', email: '[email protected]' },
        { id: '2', name: 'Jane Smith', email: '[email protected]' }
      ];

      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      mockFetch.mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(mockUsers)
      } as Response);

      render(<UserList />);

      // Should show loading initially
      expect(screen.getByTestId('loading')).toBeInTheDocument();

      // Wait for users to load
      await waitFor(() => {
        expect(screen.getByTestId('user-list')).toBeInTheDocument();
      });

      // Should display users
      expect(screen.getByTestId('user-1')).toHaveTextContent('John Doe');
      expect(screen.getByTestId('user-2')).toHaveTextContent('Jane Smith');
      expect(screen.queryByTestId('loading')).not.toBeInTheDocument();
    });

    it('should display error message on fetch failure', async () => {
      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      mockFetch.mockResolvedValue({
        ok: false,
        status: 500
      } as Response);

      render(<UserList />);

      await waitFor(() => {
        expect(screen.getByTestId('error')).toBeInTheDocument();
      });

      expect(screen.getByTestId('error')).toHaveTextContent('Error: Failed to fetch users');
      expect(screen.getByTestId('retry-button')).toBeInTheDocument();
    });

    it('should handle network errors', async () => {
      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      mockFetch.mockRejectedValue(new Error('Network error'));

      render(<UserList />);

      await waitFor(() => {
        expect(screen.getByTestId('error')).toBeInTheDocument();
      });

      expect(screen.getByTestId('error')).toHaveTextContent('Error: Network error');
    });

    it('should retry on retry button click', async () => {
      const user = userEvent.setup();
      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      
      // First call fails
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500
      } as Response);

      // Second call succeeds
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve([{ id: '1', name: 'John', email: '[email protected]' }])
      } as Response);

      render(<UserList />);

      // Wait for error
      await waitFor(() => {
        expect(screen.getByTestId('error')).toBeInTheDocument();
      });

      // Click retry
      await user.click(screen.getByTestId('retry-button'));

      // Should show loading again
      expect(screen.getByTestId('loading')).toBeInTheDocument();

      // Wait for success
      await waitFor(() => {
        expect(screen.getByTestId('user-list')).toBeInTheDocument();
      });

      expect(screen.getByTestId('user-1')).toBeInTheDocument();
      expect(mockFetch).toHaveBeenCalledTimes(2);
    });

    it('should refresh data on refresh button click', async () => {
      const user = userEvent.setup();
      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      
      const initialUsers = [{ id: '1', name: 'John', email: '[email protected]' }];
      const updatedUsers = [
        { id: '1', name: 'John Updated', email: '[email protected]' },
        { id: '2', name: 'Jane', email: '[email protected]' }
      ];

      // Initial load
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(initialUsers)
      } as Response);

      render(<UserList />);

      await waitFor(() => {
        expect(screen.getByTestId('user-1')).toHaveTextContent('John');
      });

      // Mock updated data
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(updatedUsers)
      } as Response);

      // Click refresh
      await user.click(screen.getByTestId('refresh-button'));

      // Should show loading during refresh
      expect(screen.getByTestId('loading')).toBeInTheDocument();

      // Wait for updated data
      await waitFor(() => {
        expect(screen.getByTestId('user-1')).toHaveTextContent('John Updated');
      });

      expect(screen.getByTestId('user-2')).toBeInTheDocument();
      expect(mockFetch).toHaveBeenCalledTimes(2);
    });

    it('should display no users message when list is empty', async () => {
      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      mockFetch.mockResolvedValue({
        ok: true,
        json: () => Promise.resolve([])
      } as Response);

      render(<UserList />);

      await waitFor(() => {
        expect(screen.getByTestId('no-users')).toBeInTheDocument();
      });

      expect(screen.getByTestId('no-users')).toHaveTextContent('No users found');
    });
  });
});

🎯 Advanced Testing Patterns and Best Practices

πŸ† Production-Ready Component Testing

Here are advanced patterns for comprehensive component testing in production applications.

// 🌟 Advanced testing patterns and best practices

describe('Advanced Testing Patterns', () => {
  // βœ… Testing component composition
  describe('Component Composition Testing', () => {
    interface ModalProps {
      isOpen: boolean;
      onClose: () => void;
      title: string;
      children: React.ReactNode;
    }

    const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children }) => {
      useEffect(() => {
        const handleEscape = (e: KeyboardEvent) => {
          if (e.key === 'Escape') {
            onClose();
          }
        };

        if (isOpen) {
          document.addEventListener('keydown', handleEscape);
          document.body.style.overflow = 'hidden';
        }

        return () => {
          document.removeEventListener('keydown', handleEscape);
          document.body.style.overflow = 'unset';
        };
      }, [isOpen, onClose]);

      if (!isOpen) return null;

      return (
        <div className="modal-overlay" data-testid="modal-overlay" onClick={onClose}>
          <div
            className="modal-content"
            data-testid="modal-content"
            onClick={(e) => e.stopPropagation()}
          >
            <header className="modal-header">
              <h2>{title}</h2>
              <button onClick={onClose} data-testid="modal-close">
                Γ—
              </button>
            </header>
            <div className="modal-body">{children}</div>
          </div>
        </div>
      );
    };

    const ConfirmDialog: React.FC<{
      isOpen: boolean;
      onClose: () => void;
      onConfirm: () => void;
      title: string;
      message: string;
    }> = ({ isOpen, onClose, onConfirm, title, message }) => {
      const handleConfirm = () => {
        onConfirm();
        onClose();
      };

      return (
        <Modal isOpen={isOpen} onClose={onClose} title={title}>
          <p data-testid="confirm-message">{message}</p>
          <div className="confirm-actions">
            <button onClick={onClose} data-testid="cancel-button">
              Cancel
            </button>
            <button onClick={handleConfirm} data-testid="confirm-button">
              Confirm
            </button>
          </div>
        </Modal>
      );
    };

    const AppWithModal: React.FC = () => {
      const [isModalOpen, setIsModalOpen] = useState(false);
      const [isConfirmOpen, setIsConfirmOpen] = useState(false);
      const [actionPerformed, setActionPerformed] = useState(false);

      return (
        <div>
          <button onClick={() => setIsModalOpen(true)} data-testid="open-modal">
            Open Modal
          </button>
          <button onClick={() => setIsConfirmOpen(true)} data-testid="open-confirm">
            Open Confirm
          </button>
          
          {actionPerformed && <div data-testid="action-result">Action was performed!</div>}

          <Modal
            isOpen={isModalOpen}
            onClose={() => setIsModalOpen(false)}
            title="Test Modal"
          >
            <p>This is modal content</p>
            <button
              onClick={() => setIsModalOpen(false)}
              data-testid="modal-action"
            >
              Close from inside
            </button>
          </Modal>

          <ConfirmDialog
            isOpen={isConfirmOpen}
            onClose={() => setIsConfirmOpen(false)}
            onConfirm={() => setActionPerformed(true)}
            title="Confirm Action"
            message="Are you sure you want to perform this action?"
          />
        </div>
      );
    };

    beforeEach(() => {
      // Reset body overflow style before each test
      document.body.style.overflow = 'unset';
    });

    it('should render modal when open', async () => {
      const user = userEvent.setup();
      render(<AppWithModal />);

      // Modal should not be visible initially
      expect(screen.queryByTestId('modal-overlay')).not.toBeInTheDocument();

      // Open modal
      await user.click(screen.getByTestId('open-modal'));

      // Modal should be visible
      expect(screen.getByTestId('modal-overlay')).toBeInTheDocument();
      expect(screen.getByText('Test Modal')).toBeInTheDocument();
      expect(screen.getByText('This is modal content')).toBeInTheDocument();
    });

    it('should close modal when clicking overlay', async () => {
      const user = userEvent.setup();
      render(<AppWithModal />);

      // Open modal
      await user.click(screen.getByTestId('open-modal'));
      expect(screen.getByTestId('modal-overlay')).toBeInTheDocument();

      // Click overlay
      await user.click(screen.getByTestId('modal-overlay'));

      // Modal should be closed
      expect(screen.queryByTestId('modal-overlay')).not.toBeInTheDocument();
    });

    it('should not close modal when clicking content', async () => {
      const user = userEvent.setup();
      render(<AppWithModal />);

      // Open modal
      await user.click(screen.getByTestId('open-modal'));
      expect(screen.getByTestId('modal-overlay')).toBeInTheDocument();

      // Click modal content
      await user.click(screen.getByTestId('modal-content'));

      // Modal should still be open
      expect(screen.getByTestId('modal-overlay')).toBeInTheDocument();
    });

    it('should close modal with escape key', async () => {
      const user = userEvent.setup();
      render(<AppWithModal />);

      // Open modal
      await user.click(screen.getByTestId('open-modal'));
      expect(screen.getByTestId('modal-overlay')).toBeInTheDocument();

      // Press escape
      await user.keyboard('{Escape}');

      // Modal should be closed
      expect(screen.queryByTestId('modal-overlay')).not.toBeInTheDocument();
    });

    it('should prevent body scroll when modal is open', async () => {
      const user = userEvent.setup();
      render(<AppWithModal />);

      // Initially body should be scrollable
      expect(document.body.style.overflow).toBe('unset');

      // Open modal
      await user.click(screen.getByTestId('open-modal'));

      // Body scroll should be disabled
      expect(document.body.style.overflow).toBe('hidden');

      // Close modal
      await user.click(screen.getByTestId('modal-close'));

      // Body scroll should be restored
      expect(document.body.style.overflow).toBe('unset');
    });

    it('should handle confirm dialog workflow', async () => {
      const user = userEvent.setup();
      render(<AppWithModal />);

      // Open confirm dialog
      await user.click(screen.getByTestId('open-confirm'));

      // Should show confirm dialog
      expect(screen.getByText('Confirm Action')).toBeInTheDocument();
      expect(screen.getByTestId('confirm-message')).toHaveTextContent(
        'Are you sure you want to perform this action?'
      );

      // Click confirm
      await user.click(screen.getByTestId('confirm-button'));

      // Dialog should close and action should be performed
      expect(screen.queryByTestId('modal-overlay')).not.toBeInTheDocument();
      expect(screen.getByTestId('action-result')).toBeInTheDocument();
    });

    it('should handle confirm dialog cancellation', async () => {
      const user = userEvent.setup();
      render(<AppWithModal />);

      // Open confirm dialog
      await user.click(screen.getByTestId('open-confirm'));

      // Click cancel
      await user.click(screen.getByTestId('cancel-button'));

      // Dialog should close and action should not be performed
      expect(screen.queryByTestId('modal-overlay')).not.toBeInTheDocument();
      expect(screen.queryByTestId('action-result')).not.toBeInTheDocument();
    });
  });

  // βœ… Testing custom hooks
  describe('Custom Hook Testing', () => {
    const useLocalStorage = <T,>(key: string, initialValue: T) => {
      const [storedValue, setStoredValue] = useState<T>(() => {
        try {
          const item = window.localStorage.getItem(key);
          return item ? JSON.parse(item) : initialValue;
        } catch (error) {
          return initialValue;
        }
      });

      const setValue = useCallback((value: T | ((val: T) => T)) => {
        try {
          const valueToStore = value instanceof Function ? value(storedValue) : value;
          setStoredValue(valueToStore);
          window.localStorage.setItem(key, JSON.stringify(valueToStore));
        } catch (error) {
          console.error('Error saving to localStorage:', error);
        }
      }, [key, storedValue]);

      const removeValue = useCallback(() => {
        try {
          window.localStorage.removeItem(key);
          setStoredValue(initialValue);
        } catch (error) {
          console.error('Error removing from localStorage:', error);
        }
      }, [key, initialValue]);

      return [storedValue, setValue, removeValue] as const;
    };

    const TestComponent: React.FC<{ storageKey: string; initialValue: string }> = ({
      storageKey,
      initialValue
    }) => {
      const [value, setValue, removeValue] = useLocalStorage(storageKey, initialValue);

      return (
        <div>
          <div data-testid="current-value">{value}</div>
          <button onClick={() => setValue('new value')} data-testid="set-value">
            Set Value
          </button>
          <button onClick={() => setValue(prev => prev + '!')} data-testid="append-value">
            Append !
          </button>
          <button onClick={removeValue} data-testid="remove-value">
            Remove Value
          </button>
        </div>
      );
    };

    beforeEach(() => {
      // Clear localStorage before each test
      localStorage.clear();
    });

    it('should initialize with default value', () => {
      render(<TestComponent storageKey="test-key" initialValue="default" />);

      expect(screen.getByTestId('current-value')).toHaveTextContent('default');
    });

    it('should read existing value from localStorage', () => {
      // Pre-populate localStorage
      localStorage.setItem('test-key', JSON.stringify('existing value'));

      render(<TestComponent storageKey="test-key" initialValue="default" />);

      expect(screen.getByTestId('current-value')).toHaveTextContent('existing value');
    });

    it('should update localStorage when value changes', async () => {
      const user = userEvent.setup();
      render(<TestComponent storageKey="test-key" initialValue="default" />);

      await user.click(screen.getByTestId('set-value'));

      expect(screen.getByTestId('current-value')).toHaveTextContent('new value');
      expect(localStorage.getItem('test-key')).toBe('"new value"');
    });

    it('should handle functional updates', async () => {
      const user = userEvent.setup();
      render(<TestComponent storageKey="test-key" initialValue="hello" />);

      await user.click(screen.getByTestId('append-value'));

      expect(screen.getByTestId('current-value')).toHaveTextContent('hello!');
      expect(localStorage.getItem('test-key')).toBe('"hello!"');
    });

    it('should remove value from localStorage', async () => {
      const user = userEvent.setup();
      localStorage.setItem('test-key', JSON.stringify('existing'));

      render(<TestComponent storageKey="test-key" initialValue="default" />);

      expect(screen.getByTestId('current-value')).toHaveTextContent('existing');

      await user.click(screen.getByTestId('remove-value'));

      expect(screen.getByTestId('current-value')).toHaveTextContent('default');
      expect(localStorage.getItem('test-key')).toBeNull();
    });

    it('should handle localStorage errors gracefully', () => {
      // Mock localStorage to throw error
      const originalSetItem = localStorage.setItem;
      localStorage.setItem = jest.fn(() => {
        throw new Error('Storage quota exceeded');
      });

      const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

      render(<TestComponent storageKey="test-key" initialValue="default" />);

      // Component should still render
      expect(screen.getByTestId('current-value')).toHaveTextContent('default');

      // Restore original methods
      localStorage.setItem = originalSetItem;
      consoleSpy.mockRestore();
    });
  });

  // βœ… Testing error boundaries
  describe('Error Boundary Testing', () => {
    interface ErrorBoundaryState {
      hasError: boolean;
      error?: Error;
    }

    class ErrorBoundary extends React.Component<
      { children: React.ReactNode; fallback?: React.ComponentType<{ error: Error }> },
      ErrorBoundaryState
    > {
      constructor(props: any) {
        super(props);
        this.state = { hasError: false };
      }

      static getDerivedStateFromError(error: Error): ErrorBoundaryState {
        return { hasError: true, error };
      }

      componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
        console.error('Error caught by boundary:', error, errorInfo);
      }

      render() {
        if (this.state.hasError) {
          const FallbackComponent = this.props.fallback;
          
          if (FallbackComponent && this.state.error) {
            return <FallbackComponent error={this.state.error} />;
          }
          
          return (
            <div data-testid="error-fallback">
              <h2>Something went wrong.</h2>
              <p data-testid="error-message">
                {this.state.error?.message || 'Unknown error'}
              </p>
            </div>
          );
        }

        return this.props.children;
      }
    }

    const ThrowError: React.FC<{ shouldThrow: boolean; errorMessage?: string }> = ({
      shouldThrow,
      errorMessage = 'Test error'
    }) => {
      if (shouldThrow) {
        throw new Error(errorMessage);
      }

      return <div data-testid="no-error">Component rendered successfully</div>;
    };

    const CustomErrorFallback: React.FC<{ error: Error }> = ({ error }) => (
      <div data-testid="custom-error-fallback">
        <h3>Custom Error Handler</h3>
        <p data-testid="custom-error-message">{error.message}</p>
        <button onClick={() => window.location.reload()} data-testid="reload-button">
          Reload
        </button>
      </div>
    );

    beforeEach(() => {
      // Suppress console.error for these tests
      jest.spyOn(console, 'error').mockImplementation(() => {});
    });

    afterEach(() => {
      jest.restoreAllMocks();
    });

    it('should render children when no error occurs', () => {
      render(
        <ErrorBoundary>
          <ThrowError shouldThrow={false} />
        </ErrorBoundary>
      );

      expect(screen.getByTestId('no-error')).toBeInTheDocument();
      expect(screen.queryByTestId('error-fallback')).not.toBeInTheDocument();
    });

    it('should catch and display error', () => {
      render(
        <ErrorBoundary>
          <ThrowError shouldThrow={true} errorMessage="Custom test error" />
        </ErrorBoundary>
      );

      expect(screen.getByTestId('error-fallback')).toBeInTheDocument();
      expect(screen.getByTestId('error-message')).toHaveTextContent('Custom test error');
      expect(screen.queryByTestId('no-error')).not.toBeInTheDocument();
    });

    it('should use custom fallback component', () => {
      render(
        <ErrorBoundary fallback={CustomErrorFallback}>
          <ThrowError shouldThrow={true} errorMessage="Custom fallback test" />
        </ErrorBoundary>
      );

      expect(screen.getByTestId('custom-error-fallback')).toBeInTheDocument();
      expect(screen.getByTestId('custom-error-message')).toHaveTextContent('Custom fallback test');
      expect(screen.getByTestId('reload-button')).toBeInTheDocument();
    });

    it('should handle errors in event handlers', async () => {
      const user = userEvent.setup();
      
      const ComponentWithHandler: React.FC = () => {
        const [shouldThrow, setShouldThrow] = useState(false);

        const handleClick = () => {
          setShouldThrow(true);
        };

        if (shouldThrow) {
          throw new Error('Error in event handler');
        }

        return (
          <button onClick={handleClick} data-testid="trigger-error">
            Trigger Error
          </button>
        );
      };

      render(
        <ErrorBoundary>
          <ComponentWithHandler />
        </ErrorBoundary>
      );

      // Component should render initially
      expect(screen.getByTestId('trigger-error')).toBeInTheDocument();

      // Click to trigger error
      await user.click(screen.getByTestId('trigger-error'));

      // Error boundary should catch the error
      expect(screen.getByTestId('error-fallback')).toBeInTheDocument();
      expect(screen.getByTestId('error-message')).toHaveTextContent('Error in event handler');
    });
  });
});

πŸŽ‰ Conclusion

Congratulations! You’ve mastered the art of testing React components with TypeScript and React Testing Library! 🎯

πŸ”‘ Key Takeaways

  1. User-Centric Testing: Test components as users would interact with them
  2. Proper Queries: Use semantic queries (getByRole, getByLabelText) over test IDs when possible
  3. Event Simulation: Use userEvent for realistic user interactions
  4. Async Testing: Handle async operations with waitFor and proper assertions
  5. Context Testing: Test components within their context providers
  6. State Management: Verify state changes and their effects on the UI
  7. Error Handling: Test error states and edge cases thoroughly
  8. Accessibility: Ensure components are accessible and keyboard navigable

πŸš€ Next Steps

  • Testing Hooks: Learn advanced custom hook testing patterns
  • Integration Testing: Build comprehensive integration test suites
  • Visual Testing: Add visual regression testing with tools like Storybook
  • Performance Testing: Test component performance and optimization
  • E2E Testing: Complete user workflows with Cypress or Playwright

You now have the skills to test any React component with confidence, ensuring your applications provide excellent user experiences that are reliable, accessible, and bug-free! 🌟