+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 150 of 355

🌐 Testing API Calls: MSW Integration

Master API testing in TypeScript with Mock Service Worker (MSW), covering HTTP mocking, error scenarios, and integration testing πŸš€

πŸš€Intermediate
30 min read

Prerequisites

  • Understanding of REST APIs and HTTP protocols πŸ“
  • Knowledge of Jest testing framework and async testing ⚑
  • Familiarity with React Testing Library and TypeScript πŸ’»

What you'll learn

  • Test API calls with Mock Service Worker for realistic HTTP mocking 🎯
  • Master testing error scenarios, network failures, and retry logic πŸ—οΈ
  • Handle complex API interactions and integration testing patterns πŸ›
  • Create maintainable test suites for API-driven applications ✨

🎯 Introduction

Welcome to the API testing command center! 🌐 If testing regular functions were like checking individual components in a factory, then testing API calls would be like testing the entire supply chain - complete with network requests, data transformation, error handling, and complex async workflows that need to work reliably even when external services are unavailable or behaving unpredictably!

API testing with Mock Service Worker (MSW) provides the most realistic testing environment by intercepting actual HTTP requests at the network level. Unlike simple mocking, MSW allows you to test your API layer exactly as it behaves in production, including error handling, request/response transformations, and complex async patterns.

By the end of this tutorial, you’ll be a master of API testing, capable of thoroughly testing everything from simple GET requests to complex multi-step workflows with authentication, error recovery, and optimistic updates. You’ll learn to test API interactions with confidence, ensuring your applications handle all network scenarios gracefully. Let’s build some bulletproof API tests! 🌟

πŸ“š Understanding API Testing with MSW

πŸ€” Why Mock Service Worker?

MSW provides network-level request interception, allowing you to test API interactions exactly as they occur in real applications without depending on external services.

// 🌟 Setting up comprehensive MSW testing 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 { rest } from 'msw';
import { setupServer } from 'msw/node';
import '@testing-library/jest-dom';

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

interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
  category: string;
  inStock: boolean;
  stockCount: number;
  imageUrl?: string;
  tags: string[];
  createdAt: string;
}

interface Order {
  id: string;
  userId: string;
  items: OrderItem[];
  total: number;
  status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
  shippingAddress: Address;
  createdAt: string;
  updatedAt: string;
}

interface OrderItem {
  productId: string;
  quantity: number;
  price: number;
  product?: Product;
}

interface Address {
  street: string;
  city: string;
  state: string;
  zipCode: string;
  country: string;
}

interface ApiError {
  message: string;
  code: string;
  field?: string;
  details?: Record<string, any>;
}

interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}

interface LoginRequest {
  email: string;
  password: string;
}

interface LoginResponse {
  user: User;
  token: string;
  refreshToken: string;
  expiresIn: number;
}

interface CreateProductRequest {
  name: string;
  price: number;
  description: string;
  category: string;
  stockCount: number;
  tags: string[];
}

interface UpdateProductRequest extends Partial<CreateProductRequest> {
  id: string;
}

// API service with comprehensive error handling
class ApiService {
  private baseUrl: string;
  private token: string | null = null;

  constructor(baseUrl: string = 'https://api.example.com') {
    this.baseUrl = baseUrl;
  }

  setToken(token: string | null): void {
    this.token = token;
  }

  private async makeRequest<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`;
    
    const headers: HeadersInit = {
      'Content-Type': 'application/json',
      ...options.headers,
    };

    if (this.token) {
      headers.Authorization = `Bearer ${this.token}`;
    }

    const config: RequestInit = {
      ...options,
      headers,
    };

    try {
      const response = await fetch(url, config);
      
      if (!response.ok) {
        const errorData = await response.json().catch(() => ({
          message: 'An error occurred',
          code: 'UNKNOWN_ERROR'
        }));
        
        throw new ApiError(
          errorData.message || 'Request failed',
          response.status,
          errorData.code || 'HTTP_ERROR',
          errorData
        );
      }

      const contentType = response.headers.get('Content-Type');
      if (contentType && contentType.includes('application/json')) {
        return await response.json();
      }

      return response.text() as unknown as T;
    } catch (error) {
      if (error instanceof ApiError) {
        throw error;
      }
      
      if (error instanceof TypeError && error.message.includes('fetch')) {
        throw new ApiError('Network error', 0, 'NETWORK_ERROR');
      }
      
      throw new ApiError('Unknown error', 0, 'UNKNOWN_ERROR');
    }
  }

  // Authentication endpoints
  async login(credentials: LoginRequest): Promise<LoginResponse> {
    return this.makeRequest<LoginResponse>('/auth/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
    });
  }

  async logout(): Promise<void> {
    await this.makeRequest<void>('/auth/logout', {
      method: 'POST',
    });
    this.token = null;
  }

  async refreshToken(refreshToken: string): Promise<LoginResponse> {
    return this.makeRequest<LoginResponse>('/auth/refresh', {
      method: 'POST',
      body: JSON.stringify({ refreshToken }),
    });
  }

  // User endpoints
  async getCurrentUser(): Promise<User> {
    return this.makeRequest<User>('/users/me');
  }

  async getUsers(page = 1, limit = 10): Promise<PaginatedResponse<User>> {
    return this.makeRequest<PaginatedResponse<User>>(
      `/users?page=${page}&limit=${limit}`
    );
  }

  async getUserById(id: string): Promise<User> {
    return this.makeRequest<User>(`/users/${id}`);
  }

  async updateUser(id: string, updates: Partial<User>): Promise<User> {
    return this.makeRequest<User>(`/users/${id}`, {
      method: 'PATCH',
      body: JSON.stringify(updates),
    });
  }

  async deleteUser(id: string): Promise<void> {
    return this.makeRequest<void>(`/users/${id}`, {
      method: 'DELETE',
    });
  }

  // Product endpoints
  async getProducts(
    page = 1,
    limit = 10,
    category?: string,
    search?: string
  ): Promise<PaginatedResponse<Product>> {
    const params = new URLSearchParams({
      page: page.toString(),
      limit: limit.toString(),
    });

    if (category) params.append('category', category);
    if (search) params.append('search', search);

    return this.makeRequest<PaginatedResponse<Product>>(`/products?${params}`);
  }

  async getProductById(id: string): Promise<Product> {
    return this.makeRequest<Product>(`/products/${id}`);
  }

  async createProduct(product: CreateProductRequest): Promise<Product> {
    return this.makeRequest<Product>('/products', {
      method: 'POST',
      body: JSON.stringify(product),
    });
  }

  async updateProduct(product: UpdateProductRequest): Promise<Product> {
    const { id, ...updates } = product;
    return this.makeRequest<Product>(`/products/${id}`, {
      method: 'PATCH',
      body: JSON.stringify(updates),
    });
  }

  async deleteProduct(id: string): Promise<void> {
    return this.makeRequest<void>(`/products/${id}`, {
      method: 'DELETE',
    });
  }

  // Order endpoints
  async getOrders(page = 1, limit = 10): Promise<PaginatedResponse<Order>> {
    return this.makeRequest<PaginatedResponse<Order>>(
      `/orders?page=${page}&limit=${limit}`
    );
  }

  async getOrderById(id: string): Promise<Order> {
    return this.makeRequest<Order>(`/orders/${id}`);
  }

  async createOrder(orderData: {
    items: Array<{ productId: string; quantity: number }>;
    shippingAddress: Address;
  }): Promise<Order> {
    return this.makeRequest<Order>('/orders', {
      method: 'POST',
      body: JSON.stringify(orderData),
    });
  }

  async updateOrderStatus(
    id: string,
    status: Order['status']
  ): Promise<Order> {
    return this.makeRequest<Order>(`/orders/${id}/status`, {
      method: 'PATCH',
      body: JSON.stringify({ status }),
    });
  }

  async cancelOrder(id: string): Promise<Order> {
    return this.makeRequest<Order>(`/orders/${id}/cancel`, {
      method: 'POST',
    });
  }
}

// Custom ApiError class
class ApiError extends Error {
  constructor(
    message: string,
    public statusCode: number,
    public code: string,
    public details?: any
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

// Hook for API operations with comprehensive error handling
interface UseApiOptions {
  immediate?: boolean;
  retries?: number;
  retryDelay?: number;
  onSuccess?: (data: any) => void;
  onError?: (error: ApiError) => void;
}

interface UseApiResult<T> {
  data: T | null;
  loading: boolean;
  error: ApiError | null;
  execute: (...args: any[]) => Promise<T>;
  reset: () => void;
}

const useApi = <T>(
  apiCall: (...args: any[]) => Promise<T>,
  options: UseApiOptions = {}
): UseApiResult<T> => {
  const {
    immediate = false,
    retries = 0,
    retryDelay = 1000,
    onSuccess,
    onError
  } = options;

  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(immediate);
  const [error, setError] = useState<ApiError | null>(null);

  const execute = useCallback(
    async (...args: any[]): Promise<T> => {
      setLoading(true);
      setError(null);

      let lastError: ApiError;
      
      for (let attempt = 0; attempt <= retries; attempt++) {
        try {
          const result = await apiCall(...args);
          setData(result);
          setLoading(false);
          
          if (onSuccess) {
            onSuccess(result);
          }
          
          return result;
        } catch (err) {
          lastError = err instanceof ApiError ? err : new ApiError(
            'Unknown error',
            0,
            'UNKNOWN_ERROR'
          );

          if (attempt < retries) {
            await new Promise(resolve => setTimeout(resolve, retryDelay));
          }
        }
      }

      setError(lastError!);
      setLoading(false);
      
      if (onError) {
        onError(lastError!);
      }
      
      throw lastError!;
    },
    [apiCall, retries, retryDelay, onSuccess, onError]
  );

  const reset = useCallback(() => {
    setData(null);
    setLoading(false);
    setError(null);
  }, []);

  useEffect(() => {
    if (immediate) {
      execute();
    }
  }, [immediate, execute]);

  return {
    data,
    loading,
    error,
    execute,
    reset
  };
};

// Mock data for testing
const mockUsers: User[] = [
  {
    id: 'user-1',
    name: 'John Doe',
    email: '[email protected]',
    isActive: true,
    role: 'user',
    createdAt: '2023-01-01T00:00:00Z',
    updatedAt: '2023-01-01T00:00:00Z'
  },
  {
    id: 'user-2',
    name: 'Jane Smith',
    email: '[email protected]',
    avatar: 'https://example.com/avatar2.jpg',
    isActive: true,
    role: 'admin',
    createdAt: '2023-01-02T00:00:00Z',
    updatedAt: '2023-01-02T00:00:00Z'
  },
  {
    id: 'user-3',
    name: 'Bob Johnson',
    email: '[email protected]',
    isActive: false,
    role: 'moderator',
    createdAt: '2023-01-03T00:00:00Z',
    updatedAt: '2023-01-03T00:00:00Z'
  }
];

const mockProducts: Product[] = [
  {
    id: 'product-1',
    name: 'Wireless Headphones',
    price: 99.99,
    description: 'High-quality wireless headphones with noise cancellation',
    category: 'electronics',
    inStock: true,
    stockCount: 50,
    imageUrl: 'https://example.com/headphones.jpg',
    tags: ['audio', 'wireless', 'electronics'],
    createdAt: '2023-01-01T00:00:00Z'
  },
  {
    id: 'product-2',
    name: 'Smartphone',
    price: 699.99,
    description: 'Latest smartphone with advanced features',
    category: 'electronics',
    inStock: true,
    stockCount: 25,
    imageUrl: 'https://example.com/smartphone.jpg',
    tags: ['mobile', 'electronics', 'communication'],
    createdAt: '2023-01-02T00:00:00Z'
  },
  {
    id: 'product-3',
    name: 'Coffee Mug',
    price: 15.99,
    description: 'Ceramic coffee mug with ergonomic handle',
    category: 'home',
    inStock: false,
    stockCount: 0,
    imageUrl: 'https://example.com/mug.jpg',
    tags: ['kitchen', 'ceramic', 'home'],
    createdAt: '2023-01-03T00:00:00Z'
  }
];

const mockOrders: Order[] = [
  {
    id: 'order-1',
    userId: 'user-1',
    items: [
      {
        productId: 'product-1',
        quantity: 2,
        price: 99.99
      }
    ],
    total: 199.98,
    status: 'delivered',
    shippingAddress: {
      street: '123 Main St',
      city: 'New York',
      state: 'NY',
      zipCode: '10001',
      country: 'USA'
    },
    createdAt: '2023-01-01T00:00:00Z',
    updatedAt: '2023-01-05T00:00:00Z'
  }
];

// MSW request handlers
const handlers = [
  // Authentication endpoints
  rest.post('https://api.example.com/auth/login', (req, res, ctx) => {
    const { email, password } = req.body as LoginRequest;
    
    if (email === '[email protected]' && password === 'password123') {
      const user = mockUsers[0];
      return res(
        ctx.status(200),
        ctx.json<LoginResponse>({
          user,
          token: 'mock-jwt-token',
          refreshToken: 'mock-refresh-token',
          expiresIn: 3600
        })
      );
    }
    
    if (email === '[email protected]') {
      return res(
        ctx.status(401),
        ctx.json({
          message: 'Invalid credentials',
          code: 'INVALID_CREDENTIALS'
        })
      );
    }
    
    if (email === '[email protected]') {
      return res(
        ctx.status(423),
        ctx.json({
          message: 'Account locked',
          code: 'ACCOUNT_LOCKED'
        })
      );
    }
    
    return res(
      ctx.status(400),
      ctx.json({
        message: 'Invalid request data',
        code: 'VALIDATION_ERROR',
        field: 'email'
      })
    );
  }),

  rest.post('https://api.example.com/auth/logout', (req, res, ctx) => {
    const authHeader = req.headers.get('Authorization');
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res(
        ctx.status(401),
        ctx.json({
          message: 'Unauthorized',
          code: 'UNAUTHORIZED'
        })
      );
    }
    
    return res(ctx.status(200));
  }),

  rest.post('https://api.example.com/auth/refresh', (req, res, ctx) => {
    const { refreshToken } = req.body as { refreshToken: string };
    
    if (refreshToken === 'mock-refresh-token') {
      const user = mockUsers[0];
      return res(
        ctx.status(200),
        ctx.json<LoginResponse>({
          user,
          token: 'new-mock-jwt-token',
          refreshToken: 'new-mock-refresh-token',
          expiresIn: 3600
        })
      );
    }
    
    return res(
      ctx.status(401),
      ctx.json({
        message: 'Invalid refresh token',
        code: 'INVALID_REFRESH_TOKEN'
      })
    );
  }),

  // User endpoints
  rest.get('https://api.example.com/users/me', (req, res, ctx) => {
    const authHeader = req.headers.get('Authorization');
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res(
        ctx.status(401),
        ctx.json({
          message: 'Unauthorized',
          code: 'UNAUTHORIZED'
        })
      );
    }
    
    return res(
      ctx.status(200),
      ctx.json<User>(mockUsers[0])
    );
  }),

  rest.get('https://api.example.com/users', (req, res, ctx) => {
    const page = Number(req.url.searchParams.get('page')) || 1;
    const limit = Number(req.url.searchParams.get('limit')) || 10;
    
    const startIndex = (page - 1) * limit;
    const endIndex = startIndex + limit;
    const paginatedUsers = mockUsers.slice(startIndex, endIndex);
    
    return res(
      ctx.status(200),
      ctx.json<PaginatedResponse<User>>({
        data: paginatedUsers,
        pagination: {
          page,
          limit,
          total: mockUsers.length,
          totalPages: Math.ceil(mockUsers.length / limit)
        }
      })
    );
  }),

  rest.get('https://api.example.com/users/:id', (req, res, ctx) => {
    const { id } = req.params;
    const user = mockUsers.find(u => u.id === id);
    
    if (!user) {
      return res(
        ctx.status(404),
        ctx.json({
          message: 'User not found',
          code: 'USER_NOT_FOUND'
        })
      );
    }
    
    return res(
      ctx.status(200),
      ctx.json<User>(user)
    );
  }),

  rest.patch('https://api.example.com/users/:id', (req, res, ctx) => {
    const { id } = req.params;
    const updates = req.body as Partial<User>;
    const userIndex = mockUsers.findIndex(u => u.id === id);
    
    if (userIndex === -1) {
      return res(
        ctx.status(404),
        ctx.json({
          message: 'User not found',
          code: 'USER_NOT_FOUND'
        })
      );
    }
    
    const updatedUser = { ...mockUsers[userIndex], ...updates, updatedAt: new Date().toISOString() };
    mockUsers[userIndex] = updatedUser;
    
    return res(
      ctx.status(200),
      ctx.json<User>(updatedUser)
    );
  }),

  rest.delete('https://api.example.com/users/:id', (req, res, ctx) => {
    const { id } = req.params;
    const userIndex = mockUsers.findIndex(u => u.id === id);
    
    if (userIndex === -1) {
      return res(
        ctx.status(404),
        ctx.json({
          message: 'User not found',
          code: 'USER_NOT_FOUND'
        })
      );
    }
    
    if (id === 'user-1') {
      return res(
        ctx.status(403),
        ctx.json({
          message: 'Cannot delete admin user',
          code: 'FORBIDDEN'
        })
      );
    }
    
    mockUsers.splice(userIndex, 1);
    
    return res(ctx.status(204));
  }),

  // Product endpoints
  rest.get('https://api.example.com/products', (req, res, ctx) => {
    const page = Number(req.url.searchParams.get('page')) || 1;
    const limit = Number(req.url.searchParams.get('limit')) || 10;
    const category = req.url.searchParams.get('category');
    const search = req.url.searchParams.get('search');
    
    let filteredProducts = mockProducts;
    
    if (category) {
      filteredProducts = filteredProducts.filter(p => p.category === category);
    }
    
    if (search) {
      const searchLower = search.toLowerCase();
      filteredProducts = filteredProducts.filter(p =>
        p.name.toLowerCase().includes(searchLower) ||
        p.description.toLowerCase().includes(searchLower) ||
        p.tags.some(tag => tag.toLowerCase().includes(searchLower))
      );
    }
    
    const startIndex = (page - 1) * limit;
    const endIndex = startIndex + limit;
    const paginatedProducts = filteredProducts.slice(startIndex, endIndex);
    
    return res(
      ctx.status(200),
      ctx.json<PaginatedResponse<Product>>({
        data: paginatedProducts,
        pagination: {
          page,
          limit,
          total: filteredProducts.length,
          totalPages: Math.ceil(filteredProducts.length / limit)
        }
      })
    );
  }),

  rest.get('https://api.example.com/products/:id', (req, res, ctx) => {
    const { id } = req.params;
    
    // Simulate slow network for testing
    if (id === 'slow-product') {
      return res(
        ctx.delay(2000),
        ctx.status(200),
        ctx.json<Product>({
          ...mockProducts[0],
          id: 'slow-product',
          name: 'Slow Loading Product'
        })
      );
    }
    
    const product = mockProducts.find(p => p.id === id);
    
    if (!product) {
      return res(
        ctx.status(404),
        ctx.json({
          message: 'Product not found',
          code: 'PRODUCT_NOT_FOUND'
        })
      );
    }
    
    return res(
      ctx.status(200),
      ctx.json<Product>(product)
    );
  }),

  rest.post('https://api.example.com/products', (req, res, ctx) => {
    const productData = req.body as CreateProductRequest;
    
    if (!productData.name || productData.name.length < 3) {
      return res(
        ctx.status(400),
        ctx.json({
          message: 'Product name must be at least 3 characters',
          code: 'VALIDATION_ERROR',
          field: 'name'
        })
      );
    }
    
    if (productData.price <= 0) {
      return res(
        ctx.status(400),
        ctx.json({
          message: 'Product price must be greater than 0',
          code: 'VALIDATION_ERROR',
          field: 'price'
        })
      );
    }
    
    const newProduct: Product = {
      id: `product-${Date.now()}`,
      ...productData,
      inStock: productData.stockCount > 0,
      createdAt: new Date().toISOString()
    };
    
    mockProducts.push(newProduct);
    
    return res(
      ctx.status(201),
      ctx.json<Product>(newProduct)
    );
  }),

  rest.patch('https://api.example.com/products/:id', (req, res, ctx) => {
    const { id } = req.params;
    const updates = req.body as Partial<CreateProductRequest>;
    const productIndex = mockProducts.findIndex(p => p.id === id);
    
    if (productIndex === -1) {
      return res(
        ctx.status(404),
        ctx.json({
          message: 'Product not found',
          code: 'PRODUCT_NOT_FOUND'
        })
      );
    }
    
    if (updates.price !== undefined && updates.price <= 0) {
      return res(
        ctx.status(400),
        ctx.json({
          message: 'Product price must be greater than 0',
          code: 'VALIDATION_ERROR',
          field: 'price'
        })
      );
    }
    
    const updatedProduct = {
      ...mockProducts[productIndex],
      ...updates,
      inStock: updates.stockCount !== undefined ? updates.stockCount > 0 : mockProducts[productIndex].inStock
    };
    
    mockProducts[productIndex] = updatedProduct;
    
    return res(
      ctx.status(200),
      ctx.json<Product>(updatedProduct)
    );
  }),

  rest.delete('https://api.example.com/products/:id', (req, res, ctx) => {
    const { id } = req.params;
    const productIndex = mockProducts.findIndex(p => p.id === id);
    
    if (productIndex === -1) {
      return res(
        ctx.status(404),
        ctx.json({
          message: 'Product not found',
          code: 'PRODUCT_NOT_FOUND'
        })
      );
    }
    
    mockProducts.splice(productIndex, 1);
    
    return res(ctx.status(204));
  }),

  // Order endpoints
  rest.get('https://api.example.com/orders', (req, res, ctx) => {
    const page = Number(req.url.searchParams.get('page')) || 1;
    const limit = Number(req.url.searchParams.get('limit')) || 10;
    
    const startIndex = (page - 1) * limit;
    const endIndex = startIndex + limit;
    const paginatedOrders = mockOrders.slice(startIndex, endIndex);
    
    return res(
      ctx.status(200),
      ctx.json<PaginatedResponse<Order>>({
        data: paginatedOrders,
        pagination: {
          page,
          limit,
          total: mockOrders.length,
          totalPages: Math.ceil(mockOrders.length / limit)
        }
      })
    );
  }),

  rest.get('https://api.example.com/orders/:id', (req, res, ctx) => {
    const { id } = req.params;
    const order = mockOrders.find(o => o.id === id);
    
    if (!order) {
      return res(
        ctx.status(404),
        ctx.json({
          message: 'Order not found',
          code: 'ORDER_NOT_FOUND'
        })
      );
    }
    
    return res(
      ctx.status(200),
      ctx.json<Order>(order)
    );
  }),

  rest.post('https://api.example.com/orders', (req, res, ctx) => {
    const orderData = req.body as {
      items: Array<{ productId: string; quantity: number }>;
      shippingAddress: Address;
    };
    
    if (!orderData.items || orderData.items.length === 0) {
      return res(
        ctx.status(400),
        ctx.json({
          message: 'Order must have at least one item',
          code: 'VALIDATION_ERROR',
          field: 'items'
        })
      );
    }
    
    const invalidProduct = orderData.items.find(item => 
      !mockProducts.find(p => p.id === item.productId)
    );
    
    if (invalidProduct) {
      return res(
        ctx.status(400),
        ctx.json({
          message: 'Invalid product in order',
          code: 'INVALID_PRODUCT',
          details: { productId: invalidProduct.productId }
        })
      );
    }
    
    const orderItems: OrderItem[] = orderData.items.map(item => {
      const product = mockProducts.find(p => p.id === item.productId)!;
      return {
        productId: item.productId,
        quantity: item.quantity,
        price: product.price,
        product
      };
    });
    
    const total = orderItems.reduce(
      (sum, item) => sum + (item.price * item.quantity),
      0
    );
    
    const newOrder: Order = {
      id: `order-${Date.now()}`,
      userId: 'user-1',
      items: orderItems,
      total,
      status: 'pending',
      shippingAddress: orderData.shippingAddress,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    };
    
    mockOrders.push(newOrder);
    
    return res(
      ctx.status(201),
      ctx.json<Order>(newOrder)
    );
  }),

  rest.patch('https://api.example.com/orders/:id/status', (req, res, ctx) => {
    const { id } = req.params;
    const { status } = req.body as { status: Order['status'] };
    const orderIndex = mockOrders.findIndex(o => o.id === id);
    
    if (orderIndex === -1) {
      return res(
        ctx.status(404),
        ctx.json({
          message: 'Order not found',
          code: 'ORDER_NOT_FOUND'
        })
      );
    }
    
    const currentOrder = mockOrders[orderIndex];
    
    if (currentOrder.status === 'delivered' && status !== 'delivered') {
      return res(
        ctx.status(400),
        ctx.json({
          message: 'Cannot change status of delivered order',
          code: 'INVALID_STATUS_CHANGE'
        })
      );
    }
    
    const updatedOrder = {
      ...currentOrder,
      status,
      updatedAt: new Date().toISOString()
    };
    
    mockOrders[orderIndex] = updatedOrder;
    
    return res(
      ctx.status(200),
      ctx.json<Order>(updatedOrder)
    );
  }),

  rest.post('https://api.example.com/orders/:id/cancel', (req, res, ctx) => {
    const { id } = req.params;
    const orderIndex = mockOrders.findIndex(o => o.id === id);
    
    if (orderIndex === -1) {
      return res(
        ctx.status(404),
        ctx.json({
          message: 'Order not found',
          code: 'ORDER_NOT_FOUND'
        })
      );
    }
    
    const currentOrder = mockOrders[orderIndex];
    
    if (['shipped', 'delivered'].includes(currentOrder.status)) {
      return res(
        ctx.status(400),
        ctx.json({
          message: 'Cannot cancel shipped or delivered order',
          code: 'CANNOT_CANCEL_ORDER'
        })
      );
    }
    
    const cancelledOrder = {
      ...currentOrder,
      status: 'cancelled' as const,
      updatedAt: new Date().toISOString()
    };
    
    mockOrders[orderIndex] = cancelledOrder;
    
    return res(
      ctx.status(200),
      ctx.json<Order>(cancelledOrder)
    );
  }),

  // Network error simulation
  rest.get('https://api.example.com/network-error', (req, res, ctx) => {
    return res.networkError('Failed to connect');
  }),

  // Server error simulation
  rest.get('https://api.example.com/server-error', (req, res, ctx) => {
    return res(
      ctx.status(500),
      ctx.json({
        message: 'Internal server error',
        code: 'INTERNAL_SERVER_ERROR'
      })
    );
  }),

  // Timeout simulation
  rest.get('https://api.example.com/timeout', (req, res, ctx) => {
    return res(
      ctx.delay(5000),
      ctx.status(200),
      ctx.json({ message: 'This will timeout' })
    );
  })
];

// Test server setup
const server = setupServer(...handlers);

export { server, ApiService, useApi, mockUsers, mockProducts, mockOrders };

πŸ§ͺ Basic API Testing Patterns

πŸ”§ Setting Up MSW Test Environment

Learn to configure MSW for comprehensive API testing with proper setup and teardown.

// 🎯 Comprehensive MSW test setup and configuration

// Test setup file (setupTests.ts)
import { server } from './mocks/server';
import '@testing-library/jest-dom';

// Enable API mocking before all tests
beforeAll(() => {
  server.listen({
    onUnhandledRequest: 'error' // Fail if any unhandled requests
  });
});

// Reset any runtime request handlers between tests
afterEach(() => {
  server.resetHandlers();
});

// Clean up after all tests are finished
afterAll(() => {
  server.close();
});

// Basic API service testing
describe('ApiService', () => {
  let apiService: ApiService;

  beforeEach(() => {
    apiService = new ApiService();
  });

  describe('Authentication', () => {
    it('should login successfully with valid credentials', async () => {
      const credentials: LoginRequest = {
        email: '[email protected]',
        password: 'password123'
      };

      const result = await apiService.login(credentials);

      expect(result).toEqual({
        user: expect.objectContaining({
          id: 'user-1',
          name: 'John Doe',
          email: '[email protected]',
          isActive: true,
          role: 'user'
        }),
        token: 'mock-jwt-token',
        refreshToken: 'mock-refresh-token',
        expiresIn: 3600
      });
    });

    it('should throw error for invalid credentials', async () => {
      const credentials: LoginRequest = {
        email: '[email protected]',
        password: 'wrongpassword'
      };

      await expect(apiService.login(credentials)).rejects.toThrow(
        new ApiError('Invalid credentials', 401, 'INVALID_CREDENTIALS')
      );
    });

    it('should handle account locked error', async () => {
      const credentials: LoginRequest = {
        email: '[email protected]',
        password: 'password123'
      };

      await expect(apiService.login(credentials)).rejects.toThrow(
        new ApiError('Account locked', 423, 'ACCOUNT_LOCKED')
      );
    });

    it('should handle validation errors', async () => {
      const credentials: LoginRequest = {
        email: 'invalid-email',
        password: 'password123'
      };

      await expect(apiService.login(credentials)).rejects.toThrow(
        new ApiError('Invalid request data', 400, 'VALIDATION_ERROR')
      );
    });

    it('should logout successfully', async () => {
      apiService.setToken('valid-token');

      await expect(apiService.logout()).resolves.toBeUndefined();
      expect(apiService['token']).toBeNull();
    });

    it('should throw error when logging out without token', async () => {
      await expect(apiService.logout()).rejects.toThrow(
        new ApiError('Unauthorized', 401, 'UNAUTHORIZED')
      );
    });

    it('should refresh token successfully', async () => {
      const result = await apiService.refreshToken('mock-refresh-token');

      expect(result).toEqual({
        user: expect.objectContaining({
          id: 'user-1',
          name: 'John Doe'
        }),
        token: 'new-mock-jwt-token',
        refreshToken: 'new-mock-refresh-token',
        expiresIn: 3600
      });
    });

    it('should handle invalid refresh token', async () => {
      await expect(
        apiService.refreshToken('invalid-token')
      ).rejects.toThrow(
        new ApiError('Invalid refresh token', 401, 'INVALID_REFRESH_TOKEN')
      );
    });
  });

  describe('User Operations', () => {
    beforeEach(() => {
      apiService.setToken('valid-token');
    });

    it('should get current user', async () => {
      const user = await apiService.getCurrentUser();

      expect(user).toEqual({
        id: 'user-1',
        name: 'John Doe',
        email: '[email protected]',
        isActive: true,
        role: 'user',
        createdAt: '2023-01-01T00:00:00Z',
        updatedAt: '2023-01-01T00:00:00Z'
      });
    });

    it('should get users with pagination', async () => {
      const result = await apiService.getUsers(1, 2);

      expect(result).toEqual({
        data: expect.arrayContaining([
          expect.objectContaining({
            id: 'user-1',
            name: 'John Doe'
          }),
          expect.objectContaining({
            id: 'user-2',
            name: 'Jane Smith'
          })
        ]),
        pagination: {
          page: 1,
          limit: 2,
          total: 3,
          totalPages: 2
        }
      });
    });

    it('should get user by id', async () => {
      const user = await apiService.getUserById('user-2');

      expect(user).toEqual(
        expect.objectContaining({
          id: 'user-2',
          name: 'Jane Smith',
          email: '[email protected]',
          role: 'admin'
        })
      );
    });

    it('should throw error for non-existent user', async () => {
      await expect(
        apiService.getUserById('non-existent')
      ).rejects.toThrow(
        new ApiError('User not found', 404, 'USER_NOT_FOUND')
      );
    });

    it('should update user successfully', async () => {
      const updates = { name: 'Updated Name', isActive: false };
      const updatedUser = await apiService.updateUser('user-1', updates);

      expect(updatedUser).toEqual(
        expect.objectContaining({
          id: 'user-1',
          name: 'Updated Name',
          isActive: false,
          updatedAt: expect.any(String)
        })
      );
    });

    it('should delete user successfully', async () => {
      await expect(
        apiService.deleteUser('user-3')
      ).resolves.toBeUndefined();
    });

    it('should prevent deletion of admin user', async () => {
      await expect(
        apiService.deleteUser('user-1')
      ).rejects.toThrow(
        new ApiError('Cannot delete admin user', 403, 'FORBIDDEN')
      );
    });
  });

  describe('Product Operations', () => {
    it('should get products with filtering and pagination', async () => {
      const result = await apiService.getProducts(1, 10, 'electronics', 'wireless');

      expect(result.data).toHaveLength(1);
      expect(result.data[0]).toEqual(
        expect.objectContaining({
          name: 'Wireless Headphones',
          category: 'electronics'
        })
      );
      expect(result.pagination).toEqual({
        page: 1,
        limit: 10,
        total: 1,
        totalPages: 1
      });
    });

    it('should get product by id', async () => {
      const product = await apiService.getProductById('product-1');

      expect(product).toEqual(
        expect.objectContaining({
          id: 'product-1',
          name: 'Wireless Headphones',
          price: 99.99,
          category: 'electronics'
        })
      );
    });

    it('should handle slow loading products', async () => {
      const startTime = Date.now();
      const product = await apiService.getProductById('slow-product');
      const endTime = Date.now();

      expect(endTime - startTime).toBeGreaterThanOrEqual(2000);
      expect(product.name).toBe('Slow Loading Product');
    });

    it('should create product successfully', async () => {
      const productData: CreateProductRequest = {
        name: 'New Product',
        price: 49.99,
        description: 'A new test product',
        category: 'test',
        stockCount: 10,
        tags: ['test', 'new']
      };

      const createdProduct = await apiService.createProduct(productData);

      expect(createdProduct).toEqual(
        expect.objectContaining({
          id: expect.stringMatching(/^product-\d+$/),
          name: 'New Product',
          price: 49.99,
          inStock: true,
          createdAt: expect.any(String)
        })
      );
    });

    it('should validate product creation data', async () => {
      const invalidProductData: CreateProductRequest = {
        name: 'AB', // Too short
        price: -10, // Invalid price
        description: 'Invalid product',
        category: 'test',
        stockCount: 5,
        tags: []
      };

      await expect(
        apiService.createProduct(invalidProductData)
      ).rejects.toThrow(
        new ApiError(
          'Product name must be at least 3 characters',
          400,
          'VALIDATION_ERROR'
        )
      );
    });

    it('should update product successfully', async () => {
      const updates: UpdateProductRequest = {
        id: 'product-1',
        name: 'Updated Headphones',
        price: 129.99
      };

      const updatedProduct = await apiService.updateProduct(updates);

      expect(updatedProduct).toEqual(
        expect.objectContaining({
          id: 'product-1',
          name: 'Updated Headphones',
          price: 129.99
        })
      );
    });

    it('should validate product update data', async () => {
      const invalidUpdates: UpdateProductRequest = {
        id: 'product-1',
        price: -50 // Invalid price
      };

      await expect(
        apiService.updateProduct(invalidUpdates)
      ).rejects.toThrow(
        new ApiError(
          'Product price must be greater than 0',
          400,
          'VALIDATION_ERROR'
        )
      );
    });

    it('should delete product successfully', async () => {
      await expect(
        apiService.deleteProduct('product-3')
      ).resolves.toBeUndefined();
    });
  });

  describe('Order Operations', () => {
    it('should create order successfully', async () => {
      const orderData = {
        items: [
          { productId: 'product-1', quantity: 2 },
          { productId: 'product-2', quantity: 1 }
        ],
        shippingAddress: {
          street: '456 Oak St',
          city: 'Los Angeles',
          state: 'CA',
          zipCode: '90210',
          country: 'USA'
        }
      };

      const createdOrder = await apiService.createOrder(orderData);

      expect(createdOrder).toEqual(
        expect.objectContaining({
          id: expect.stringMatching(/^order-\d+$/),
          userId: 'user-1',
          items: expect.arrayContaining([
            expect.objectContaining({
              productId: 'product-1',
              quantity: 2,
              price: 99.99
            }),
            expect.objectContaining({
              productId: 'product-2',
              quantity: 1,
              price: 699.99
            })
          ]),
          total: 899.97, // (99.99 * 2) + 699.99
          status: 'pending'
        })
      );
    });

    it('should validate order items', async () => {
      const invalidOrderData = {
        items: [], // Empty items
        shippingAddress: {
          street: '456 Oak St',
          city: 'Los Angeles',
          state: 'CA',
          zipCode: '90210',
          country: 'USA'
        }
      };

      await expect(
        apiService.createOrder(invalidOrderData)
      ).rejects.toThrow(
        new ApiError(
          'Order must have at least one item',
          400,
          'VALIDATION_ERROR'
        )
      );
    });

    it('should validate product existence in order', async () => {
      const invalidOrderData = {
        items: [
          { productId: 'non-existent-product', quantity: 1 }
        ],
        shippingAddress: {
          street: '456 Oak St',
          city: 'Los Angeles',
          state: 'CA',
          zipCode: '90210',
          country: 'USA'
        }
      };

      await expect(
        apiService.createOrder(invalidOrderData)
      ).rejects.toThrow(
        new ApiError('Invalid product in order', 400, 'INVALID_PRODUCT')
      );
    });

    it('should update order status', async () => {
      const updatedOrder = await apiService.updateOrderStatus(
        'order-1',
        'processing'
      );

      expect(updatedOrder).toEqual(
        expect.objectContaining({
          id: 'order-1',
          status: 'processing',
          updatedAt: expect.any(String)
        })
      );
    });

    it('should prevent invalid status changes', async () => {
      // First update to delivered
      await apiService.updateOrderStatus('order-1', 'delivered');

      // Then try to change back to processing
      await expect(
        apiService.updateOrderStatus('order-1', 'processing')
      ).rejects.toThrow(
        new ApiError(
          'Cannot change status of delivered order',
          400,
          'INVALID_STATUS_CHANGE'
        )
      );
    });

    it('should cancel order successfully', async () => {
      const cancelledOrder = await apiService.cancelOrder('order-1');

      expect(cancelledOrder).toEqual(
        expect.objectContaining({
          id: 'order-1',
          status: 'cancelled'
        })
      );
    });

    it('should prevent cancelling shipped orders', async () => {
      // Update order to shipped status
      await apiService.updateOrderStatus('order-1', 'shipped');

      // Try to cancel shipped order
      await expect(
        apiService.cancelOrder('order-1')
      ).rejects.toThrow(
        new ApiError(
          'Cannot cancel shipped or delivered order',
          400,
          'CANNOT_CANCEL_ORDER'
        )
      );
    });
  });
});

πŸŽ›οΈ Testing Custom API Hooks

πŸ”„ Hook State Management Testing

Test custom hooks that manage API state with comprehensive coverage of loading, success, and error states.

// 🎯 Testing custom API hooks with comprehensive state management

describe('useApi Hook', () => {
  let apiService: ApiService;

  beforeEach(() => {
    apiService = new ApiService();
  });

  describe('Basic Functionality', () => {
    it('should initialize with correct default state', () => {
      const mockApiCall = jest.fn().mockResolvedValue({ data: 'test' });
      const { result } = renderHook(() => useApi(mockApiCall));

      expect(result.current.data).toBeNull();
      expect(result.current.loading).toBe(false);
      expect(result.current.error).toBeNull();
      expect(typeof result.current.execute).toBe('function');
      expect(typeof result.current.reset).toBe('function');
    });

    it('should execute API call when immediate is true', async () => {
      const mockApiCall = jest.fn().mockResolvedValue({ data: 'test' });
      const { result } = renderHook(() => 
        useApi(mockApiCall, { immediate: true })
      );

      // Initially loading
      expect(result.current.loading).toBe(true);
      expect(result.current.data).toBeNull();
      expect(result.current.error).toBeNull();

      // Wait for completion
      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.data).toEqual({ data: 'test' });
      expect(result.current.error).toBeNull();
      expect(mockApiCall).toHaveBeenCalledTimes(1);
    });

    it('should handle successful API execution', async () => {
      const testData = { id: '1', name: 'Test User' };
      const mockApiCall = jest.fn().mockResolvedValue(testData);
      const { result } = renderHook(() => useApi(mockApiCall));

      act(() => {
        result.current.execute();
      });

      expect(result.current.loading).toBe(true);
      expect(result.current.data).toBeNull();
      expect(result.current.error).toBeNull();

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.data).toEqual(testData);
      expect(result.current.error).toBeNull();
      expect(mockApiCall).toHaveBeenCalledTimes(1);
    });

    it('should handle API execution with parameters', async () => {
      const testData = { id: '123', name: 'Specific User' };
      const mockApiCall = jest.fn().mockResolvedValue(testData);
      const { result } = renderHook(() => useApi(mockApiCall));

      act(() => {
        result.current.execute('param1', 'param2', 123);
      });

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.data).toEqual(testData);
      expect(mockApiCall).toHaveBeenCalledWith('param1', 'param2', 123);
    });

    it('should handle API errors', async () => {
      const apiError = new ApiError('Test error', 404, 'NOT_FOUND');
      const mockApiCall = jest.fn().mockRejectedValue(apiError);
      const { result } = renderHook(() => useApi(mockApiCall));

      act(() => {
        result.current.execute();
      });

      expect(result.current.loading).toBe(true);

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.data).toBeNull();
      expect(result.current.error).toEqual(apiError);
    });

    it('should reset state correctly', async () => {
      const testData = { id: '1', name: 'Test User' };
      const mockApiCall = jest.fn().mockResolvedValue(testData);
      const { result } = renderHook(() => useApi(mockApiCall));

      // Execute API call
      act(() => {
        result.current.execute();
      });

      await waitFor(() => {
        expect(result.current.data).toEqual(testData);
      });

      // Reset state
      act(() => {
        result.current.reset();
      });

      expect(result.current.data).toBeNull();
      expect(result.current.loading).toBe(false);
      expect(result.current.error).toBeNull();
    });
  });

  describe('Error Handling and Retries', () => {
    it('should retry failed requests', async () => {
      const apiError = new ApiError('Network error', 0, 'NETWORK_ERROR');
      const mockApiCall = jest.fn()
        .mockRejectedValueOnce(apiError)
        .mockRejectedValueOnce(apiError)
        .mockResolvedValue({ data: 'success' });

      const { result } = renderHook(() => 
        useApi(mockApiCall, { retries: 2, retryDelay: 100 })
      );

      act(() => {
        result.current.execute();
      });

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.data).toEqual({ data: 'success' });
      expect(result.current.error).toBeNull();
      expect(mockApiCall).toHaveBeenCalledTimes(3);
    });

    it('should fail after exhausting retries', async () => {
      const apiError = new ApiError('Persistent error', 500, 'SERVER_ERROR');
      const mockApiCall = jest.fn().mockRejectedValue(apiError);

      const { result } = renderHook(() => 
        useApi(mockApiCall, { retries: 2, retryDelay: 50 })
      );

      act(() => {
        result.current.execute();
      });

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.data).toBeNull();
      expect(result.current.error).toEqual(apiError);
      expect(mockApiCall).toHaveBeenCalledTimes(3); // Original + 2 retries
    });

    it('should respect retry delay', async () => {
      const apiError = new ApiError('Network error', 0, 'NETWORK_ERROR');
      const mockApiCall = jest.fn()
        .mockRejectedValueOnce(apiError)
        .mockResolvedValue({ data: 'success' });

      const startTime = Date.now();
      const { result } = renderHook(() => 
        useApi(mockApiCall, { retries: 1, retryDelay: 200 })
      );

      act(() => {
        result.current.execute();
      });

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      const endTime = Date.now();
      expect(endTime - startTime).toBeGreaterThanOrEqual(200);
      expect(result.current.data).toEqual({ data: 'success' });
    });

    it('should handle non-ApiError exceptions', async () => {
      const genericError = new Error('Generic error');
      const mockApiCall = jest.fn().mockRejectedValue(genericError);

      const { result } = renderHook(() => useApi(mockApiCall));

      act(() => {
        result.current.execute();
      });

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.error).toEqual(
        new ApiError('Unknown error', 0, 'UNKNOWN_ERROR')
      );
    });
  });

  describe('Callback Integration', () => {
    it('should call onSuccess callback', async () => {
      const testData = { id: '1', name: 'Test User' };
      const mockApiCall = jest.fn().mockResolvedValue(testData);
      const onSuccess = jest.fn();

      const { result } = renderHook(() => 
        useApi(mockApiCall, { onSuccess })
      );

      act(() => {
        result.current.execute();
      });

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(onSuccess).toHaveBeenCalledWith(testData);
      expect(onSuccess).toHaveBeenCalledTimes(1);
    });

    it('should call onError callback', async () => {
      const apiError = new ApiError('Test error', 404, 'NOT_FOUND');
      const mockApiCall = jest.fn().mockRejectedValue(apiError);
      const onError = jest.fn();

      const { result } = renderHook(() => 
        useApi(mockApiCall, { onError })
      );

      act(() => {
        result.current.execute();
      });

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(onError).toHaveBeenCalledWith(apiError);
      expect(onError).toHaveBeenCalledTimes(1);
    });

    it('should not call onSuccess after retries if final attempt fails', async () => {
      const apiError = new ApiError('Persistent error', 500, 'SERVER_ERROR');
      const mockApiCall = jest.fn().mockRejectedValue(apiError);
      const onSuccess = jest.fn();
      const onError = jest.fn();

      const { result } = renderHook(() => 
        useApi(mockApiCall, { retries: 2, onSuccess, onError })
      );

      act(() => {
        result.current.execute();
      });

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(onSuccess).not.toHaveBeenCalled();
      expect(onError).toHaveBeenCalledWith(apiError);
      expect(onError).toHaveBeenCalledTimes(1);
    });
  });

  describe('Real API Integration', () => {
    it('should login successfully', async () => {
      const { result } = renderHook(() => 
        useApi(() => apiService.login({
          email: '[email protected]',
          password: 'password123'
        }))
      );

      act(() => {
        result.current.execute();
      });

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.data).toEqual({
        user: expect.objectContaining({
          id: 'user-1',
          email: '[email protected]'
        }),
        token: 'mock-jwt-token',
        refreshToken: 'mock-refresh-token',
        expiresIn: 3600
      });
      expect(result.current.error).toBeNull();
    });

    it('should handle login failure', async () => {
      const { result } = renderHook(() => 
        useApi(() => apiService.login({
          email: '[email protected]',
          password: 'wrongpassword'
        }))
      );

      act(() => {
        result.current.execute();
      });

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.data).toBeNull();
      expect(result.current.error).toEqual(
        new ApiError('Invalid credentials', 401, 'INVALID_CREDENTIALS')
      );
    });

    it('should fetch users with pagination', async () => {
      apiService.setToken('valid-token');
      
      const { result } = renderHook(() => 
        useApi((page: number, limit: number) => 
          apiService.getUsers(page, limit)
        )
      );

      act(() => {
        result.current.execute(1, 2);
      });

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.data).toEqual({
        data: expect.arrayContaining([
          expect.objectContaining({ id: 'user-1' }),
          expect.objectContaining({ id: 'user-2' })
        ]),
        pagination: {
          page: 1,
          limit: 2,
          total: 3,
          totalPages: 2
        }
      });
    });

    it('should create product successfully', async () => {
      const productData: CreateProductRequest = {
        name: 'New Test Product',
        price: 29.99,
        description: 'A product created in tests',
        category: 'test',
        stockCount: 15,
        tags: ['test', 'new']
      };

      const { result } = renderHook(() => 
        useApi((data: CreateProductRequest) => 
          apiService.createProduct(data)
        )
      );

      act(() => {
        result.current.execute(productData);
      });

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });

      expect(result.current.data).toEqual(
        expect.objectContaining({
          id: expect.stringMatching(/^product-\d+$/),
          name: 'New Test Product',
          price: 29.99,
          inStock: true
        })
      );
    });
  });
});

πŸ”„ Advanced MSW Patterns

🎭 Dynamic Response Handling

Master advanced MSW patterns for complex testing scenarios including conditional responses and stateful mocking.

// 🎯 Advanced MSW patterns for complex API testing scenarios

describe('Advanced MSW Patterns', () => {
  describe('Dynamic Response Handling', () => {
    it('should handle conditional responses based on request data', async () => {
      // Override default handler with conditional logic
      server.use(
        rest.post('https://api.example.com/auth/login', (req, res, ctx) => {
          const { email, password } = req.body as LoginRequest;
          
          if (email === '[email protected]' && password === 'premium123') {
            return res(
              ctx.status(200),
              ctx.json<LoginResponse>({
                user: {
                  ...mockUsers[0],
                  id: 'premium-user',
                  role: 'admin',
                  email: '[email protected]'
                },
                token: 'premium-jwt-token',
                refreshToken: 'premium-refresh-token',
                expiresIn: 7200 // Longer expiry for premium users
              })
            );
          }
          
          return res(
            ctx.status(401),
            ctx.json({
              message: 'Invalid credentials',
              code: 'INVALID_CREDENTIALS'
            })
          );
        })
      );

      const apiService = new ApiService();
      const result = await apiService.login({
        email: '[email protected]',
        password: 'premium123'
      });

      expect(result.user.role).toBe('admin');
      expect(result.expiresIn).toBe(7200);
      expect(result.token).toBe('premium-jwt-token');
    });

    it('should handle stateful responses with request counting', async () => {
      let requestCount = 0;

      server.use(
        rest.get('https://api.example.com/products/rate-limited', (req, res, ctx) => {
          requestCount++;
          
          if (requestCount <= 3) {
            return res(
              ctx.status(200),
              ctx.json({
                id: 'rate-limited-product',
                name: `Product Request ${requestCount}`,
                requestCount
              })
            );
          }
          
          return res(
            ctx.status(429),
            ctx.json({
              message: 'Rate limit exceeded',
              code: 'RATE_LIMIT_EXCEEDED',
              retryAfter: 60
            })
          );
        })
      );

      const apiService = new ApiService();

      // First 3 requests should succeed
      for (let i = 1; i <= 3; i++) {
        const result = await apiService.getProductById('rate-limited');
        expect(result.requestCount).toBe(i);
      }

      // 4th request should be rate limited
      await expect(
        apiService.getProductById('rate-limited')
      ).rejects.toThrow(
        new ApiError('Rate limit exceeded', 429, 'RATE_LIMIT_EXCEEDED')
      );
    });

    it('should simulate progressive loading states', async () => {
      const loadingStates = ['initializing', 'processing', 'finalizing', 'complete'];
      let stateIndex = 0;

      server.use(
        rest.get('https://api.example.com/long-process/:id', (req, res, ctx) => {
          const currentState = loadingStates[stateIndex];
          
          if (stateIndex < loadingStates.length - 1) {
            stateIndex++;
            return res(
              ctx.status(202), // Accepted but not complete
              ctx.json({
                id: req.params.id,
                status: currentState,
                progress: Math.round((stateIndex / loadingStates.length) * 100)
              })
            );
          }
          
          return res(
            ctx.status(200),
            ctx.json({
              id: req.params.id,
              status: 'complete',
              progress: 100,
              result: 'Process completed successfully'
            })
          );
        })
      );

      const apiService = new ApiService();
      
      // Simulate polling until complete
      let response;
      let attempts = 0;
      const maxAttempts = 5;
      
      do {
        response = await apiService.makeRequest('/long-process/test-123');
        attempts++;
        
        if (response.status !== 'complete' && attempts < maxAttempts) {
          await new Promise(resolve => setTimeout(resolve, 100));
        }
      } while (response.status !== 'complete' && attempts < maxAttempts);

      expect(response.status).toBe('complete');
      expect(response.progress).toBe(100);
      expect(response.result).toBe('Process completed successfully');
    });

    it('should handle request headers and authentication states', async () => {
      server.use(
        rest.get('https://api.example.com/secure-data', (req, res, ctx) => {
          const authHeader = req.headers.get('Authorization');
          
          if (!authHeader) {
            return res(
              ctx.status(401),
              ctx.json({
                message: 'No authorization header',
                code: 'NO_AUTH_HEADER'
              })
            );
          }
          
          if (authHeader === 'Bearer expired-token') {
            return res(
              ctx.status(401),
              ctx.json({
                message: 'Token expired',
                code: 'TOKEN_EXPIRED'
              })
            );
          }
          
          if (authHeader === 'Bearer admin-token') {
            return res(
              ctx.status(200),
              ctx.json({
                data: 'Top secret admin data',
                accessLevel: 'admin'
              })
            );
          }
          
          if (authHeader === 'Bearer user-token') {
            return res(
              ctx.status(200),
              ctx.json({
                data: 'Regular user data',
                accessLevel: 'user'
              })
            );
          }
          
          return res(
            ctx.status(403),
            ctx.json({
              message: 'Invalid token',
              code: 'INVALID_TOKEN'
            })
          );
        })
      );

      const apiService = new ApiService();

      // Test no auth
      await expect(
        apiService.makeRequest('/secure-data')
      ).rejects.toThrow(
        new ApiError('No authorization header', 401, 'NO_AUTH_HEADER')
      );

      // Test expired token
      apiService.setToken('expired-token');
      await expect(
        apiService.makeRequest('/secure-data')
      ).rejects.toThrow(
        new ApiError('Token expired', 401, 'TOKEN_EXPIRED')
      );

      // Test admin access
      apiService.setToken('admin-token');
      const adminResponse = await apiService.makeRequest('/secure-data');
      expect(adminResponse.accessLevel).toBe('admin');

      // Test user access
      apiService.setToken('user-token');
      const userResponse = await apiService.makeRequest('/secure-data');
      expect(userResponse.accessLevel).toBe('user');
    });
  });

  describe('Network Condition Simulation', () => {
    it('should simulate network errors', async () => {
      const apiService = new ApiService();

      await expect(
        apiService.makeRequest('/network-error')
      ).rejects.toThrow(
        new ApiError('Network error', 0, 'NETWORK_ERROR')
      );
    });

    it('should simulate server errors', async () => {
      const apiService = new ApiService();

      await expect(
        apiService.makeRequest('/server-error')
      ).rejects.toThrow(
        new ApiError('Internal server error', 500, 'INTERNAL_SERVER_ERROR')
      );
    });

    it('should simulate slow network conditions', async () => {
      server.use(
        rest.get('https://api.example.com/slow-endpoint', (req, res, ctx) => {
          return res(
            ctx.delay(1500), // 1.5 second delay
            ctx.status(200),
            ctx.json({ message: 'Slow response' })
          );
        })
      );

      const apiService = new ApiService();
      const startTime = Date.now();
      
      const response = await apiService.makeRequest('/slow-endpoint');
      const endTime = Date.now();
      
      expect(endTime - startTime).toBeGreaterThanOrEqual(1500);
      expect(response.message).toBe('Slow response');
    });

    it('should simulate intermittent connectivity', async () => {
      let requestCount = 0;

      server.use(
        rest.get('https://api.example.com/intermittent', (req, res, ctx) => {
          requestCount++;
          
          // Fail every other request
          if (requestCount % 2 === 0) {
            return res.networkError('Connection failed');
          }
          
          return res(
            ctx.status(200),
            ctx.json({ 
              message: 'Connection successful',
              requestNumber: requestCount
            })
          );
        })
      );

      const apiService = new ApiService();

      // First request should succeed
      const firstResponse = await apiService.makeRequest('/intermittent');
      expect(firstResponse.requestNumber).toBe(1);

      // Second request should fail
      await expect(
        apiService.makeRequest('/intermittent')
      ).rejects.toThrow(
        new ApiError('Network error', 0, 'NETWORK_ERROR')
      );

      // Third request should succeed again
      const thirdResponse = await apiService.makeRequest('/intermittent');
      expect(thirdResponse.requestNumber).toBe(3);
    });
  });

  describe('Complex Response Scenarios', () => {
    it('should handle pagination with different page sizes', async () => {
      server.use(
        rest.get('https://api.example.com/variable-pagination', (req, res, ctx) => {
          const page = Number(req.url.searchParams.get('page')) || 1;
          const limit = Number(req.url.searchParams.get('limit')) || 10;
          
          // Simulate a dataset of 47 items
          const totalItems = 47;
          const totalPages = Math.ceil(totalItems / limit);
          
          if (page > totalPages) {
            return res(
              ctx.status(404),
              ctx.json({
                message: 'Page not found',
                code: 'PAGE_NOT_FOUND'
              })
            );
          }
          
          const startIndex = (page - 1) * limit;
          const endIndex = Math.min(startIndex + limit, totalItems);
          const itemsOnPage = endIndex - startIndex;
          
          const items = Array.from({ length: itemsOnPage }, (_, index) => ({
            id: `item-${startIndex + index + 1}`,
            name: `Item ${startIndex + index + 1}`
          }));
          
          return res(
            ctx.status(200),
            ctx.json({
              data: items,
              pagination: {
                page,
                limit,
                total: totalItems,
                totalPages,
                hasNext: page < totalPages,
                hasPrev: page > 1
              }
            })
          );
        })
      );

      const apiService = new ApiService();

      // Test first page with default limit
      const firstPage = await apiService.makeRequest('/variable-pagination?page=1&limit=10');
      expect(firstPage.data).toHaveLength(10);
      expect(firstPage.pagination.hasNext).toBe(true);
      expect(firstPage.pagination.hasPrev).toBe(false);

      // Test last page
      const lastPage = await apiService.makeRequest('/variable-pagination?page=5&limit=10');
      expect(lastPage.data).toHaveLength(7); // 47 % 10 = 7 items on last page
      expect(lastPage.pagination.hasNext).toBe(false);
      expect(lastPage.pagination.hasPrev).toBe(true);

      // Test page beyond range
      await expect(
        apiService.makeRequest('/variable-pagination?page=10&limit=10')
      ).rejects.toThrow(
        new ApiError('Page not found', 404, 'PAGE_NOT_FOUND')
      );
    });

    it('should handle complex filtering and sorting', async () => {
      server.use(
        rest.get('https://api.example.com/advanced-search', (req, res, ctx) => {
          const search = req.url.searchParams.get('search');
          const category = req.url.searchParams.get('category');
          const minPrice = Number(req.url.searchParams.get('minPrice')) || 0;
          const maxPrice = Number(req.url.searchParams.get('maxPrice')) || Infinity;
          const sortBy = req.url.searchParams.get('sortBy') || 'name';
          const sortOrder = req.url.searchParams.get('sortOrder') || 'asc';
          
          let filteredProducts = [...mockProducts];
          
          // Apply filters
          if (search) {
            const searchLower = search.toLowerCase();
            filteredProducts = filteredProducts.filter(p =>
              p.name.toLowerCase().includes(searchLower) ||
              p.description.toLowerCase().includes(searchLower)
            );
          }
          
          if (category) {
            filteredProducts = filteredProducts.filter(p => p.category === category);
          }
          
          filteredProducts = filteredProducts.filter(p => 
            p.price >= minPrice && p.price <= maxPrice
          );
          
          // Apply sorting
          filteredProducts.sort((a, b) => {
            let valueA = a[sortBy as keyof Product];
            let valueB = b[sortBy as keyof Product];
            
            if (typeof valueA === 'string') {
              valueA = valueA.toLowerCase();
              valueB = (valueB as string).toLowerCase();
            }
            
            if (valueA < valueB) return sortOrder === 'asc' ? -1 : 1;
            if (valueA > valueB) return sortOrder === 'asc' ? 1 : -1;
            return 0;
          });
          
          return res(
            ctx.status(200),
            ctx.json({
              data: filteredProducts,
              filters: {
                search,
                category,
                minPrice,
                maxPrice,
                sortBy,
                sortOrder
              },
              count: filteredProducts.length
            })
          );
        })
      );

      const apiService = new ApiService();

      // Test search functionality
      const searchResult = await apiService.makeRequest('/advanced-search?search=wireless');
      expect(searchResult.data).toHaveLength(1);
      expect(searchResult.data[0].name).toContain('Wireless');

      // Test category filtering
      const categoryResult = await apiService.makeRequest('/advanced-search?category=electronics');
      expect(categoryResult.data).toHaveLength(2);
      expect(categoryResult.data.every((p: Product) => p.category === 'electronics')).toBe(true);

      // Test price range filtering
      const priceResult = await apiService.makeRequest('/advanced-search?minPrice=50&maxPrice=200');
      expect(priceResult.data.every((p: Product) => p.price >= 50 && p.price <= 200)).toBe(true);

      // Test sorting
      const sortedResult = await apiService.makeRequest('/advanced-search?sortBy=price&sortOrder=desc');
      const prices = sortedResult.data.map((p: Product) => p.price);
      expect(prices).toEqual([...prices].sort((a, b) => b - a));
    });
  });
});

🎯 Integration Testing Patterns

πŸ”— Component-API Integration

Test complete user workflows involving API interactions with comprehensive coverage.

// 🎯 Complete integration testing with API interactions

// UserProfile component for testing
interface UserProfileProps {
  userId: string;
  onUserUpdate?: (user: User) => void;
}

const UserProfile: React.FC<UserProfileProps> = ({ userId, onUserUpdate }) => {
  const apiService = useMemo(() => new ApiService(), []);
  
  const {
    data: user,
    loading,
    error,
    execute: fetchUser
  } = useApi((id: string) => apiService.getUserById(id));

  const {
    loading: updating,
    error: updateError,
    execute: updateUser
  } = useApi(
    (id: string, updates: Partial<User>) => apiService.updateUser(id, updates),
    {
      onSuccess: (updatedUser) => {
        if (onUserUpdate) onUserUpdate(updatedUser);
      }
    }
  );

  const [editMode, setEditMode] = useState(false);
  const [formData, setFormData] = useState<Partial<User>>({});

  useEffect(() => {
    if (userId) {
      fetchUser(userId);
    }
  }, [userId, fetchUser]);

  useEffect(() => {
    if (user) {
      setFormData({
        name: user.name,
        email: user.email,
        isActive: user.isActive
      });
    }
  }, [user]);

  const handleEdit = useCallback(() => {
    setEditMode(true);
  }, []);

  const handleCancel = useCallback(() => {
    setEditMode(false);
    if (user) {
      setFormData({
        name: user.name,
        email: user.email,
        isActive: user.isActive
      });
    }
  }, [user]);

  const handleSave = useCallback(async () => {
    if (user && formData) {
      await updateUser(user.id, formData);
      setEditMode(false);
    }
  }, [user, formData, updateUser]);

  const handleInputChange = useCallback((field: keyof User, value: any) => {
    setFormData(prev => ({ ...prev, [field]: value }));
  }, []);

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

  if (error) {
    return (
      <div data-testid="error" role="alert">
        Error: {error.message}
      </div>
    );
  }

  if (!user) {
    return <div data-testid="no-user">User not found</div>;
  }

  return (
    <div data-testid="user-profile">
      <h2>User Profile</h2>
      
      {updateError && (
        <div data-testid="update-error" role="alert">
          Update Error: {updateError.message}
        </div>
      )}
      
      {editMode ? (
        <div data-testid="edit-form">
          <div>
            <label htmlFor="name">Name:</label>
            <input
              id="name"
              data-testid="name-input"
              value={formData.name || ''}
              onChange={(e) => handleInputChange('name', e.target.value)}
              disabled={updating}
            />
          </div>
          
          <div>
            <label htmlFor="email">Email:</label>
            <input
              id="email"
              data-testid="email-input"
              type="email"
              value={formData.email || ''}
              onChange={(e) => handleInputChange('email', e.target.value)}
              disabled={updating}
            />
          </div>
          
          <div>
            <label>
              <input
                data-testid="active-checkbox"
                type="checkbox"
                checked={formData.isActive || false}
                onChange={(e) => handleInputChange('isActive', e.target.checked)}
                disabled={updating}
              />
              Active User
            </label>
          </div>
          
          <div>
            <button
              data-testid="save-button"
              onClick={handleSave}
              disabled={updating}
            >
              {updating ? 'Saving...' : 'Save'}
            </button>
            <button
              data-testid="cancel-button"
              onClick={handleCancel}
              disabled={updating}
            >
              Cancel
            </button>
          </div>
        </div>
      ) : (
        <div data-testid="view-mode">
          <p data-testid="user-name">Name: {user.name}</p>
          <p data-testid="user-email">Email: {user.email}</p>
          <p data-testid="user-role">Role: {user.role}</p>
          <p data-testid="user-status">
            Status: {user.isActive ? 'Active' : 'Inactive'}
          </p>
          
          <button data-testid="edit-button" onClick={handleEdit}>
            Edit Profile
          </button>
        </div>
      )}
    </div>
  );
};

// ProductList component for testing
interface ProductListProps {
  category?: string;
  onProductSelect?: (product: Product) => void;
}

const ProductList: React.FC<ProductListProps> = ({ category, onProductSelect }) => {
  const apiService = useMemo(() => new ApiService(), []);
  
  const [currentPage, setCurrentPage] = useState(1);
  const [searchTerm, setSearchTerm] = useState('');
  
  const {
    data: productsResponse,
    loading,
    error,
    execute: fetchProducts
  } = useApi((page: number, limit: number, cat?: string, search?: string) =>
    apiService.getProducts(page, limit, cat, search)
  );

  useEffect(() => {
    fetchProducts(currentPage, 10, category, searchTerm || undefined);
  }, [currentPage, category, searchTerm, fetchProducts]);

  const handleSearch = useCallback((search: string) => {
    setSearchTerm(search);
    setCurrentPage(1); // Reset to first page when searching
  }, []);

  const handlePageChange = useCallback((page: number) => {
    setCurrentPage(page);
  }, []);

  const handleProductClick = useCallback((product: Product) => {
    if (onProductSelect) {
      onProductSelect(product);
    }
  }, [onProductSelect]);

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

  if (error) {
    return (
      <div data-testid="products-error" role="alert">
        Error loading products: {error.message}
      </div>
    );
  }

  const products = productsResponse?.data || [];
  const pagination = productsResponse?.pagination;

  return (
    <div data-testid="product-list">
      <h2>Products {category && `- ${category}`}</h2>
      
      <div data-testid="search-section">
        <input
          data-testid="search-input"
          type="text"
          placeholder="Search products..."
          value={searchTerm}
          onChange={(e) => handleSearch(e.target.value)}
        />
      </div>
      
      {products.length === 0 ? (
        <div data-testid="no-products">No products found</div>
      ) : (
        <>
          <div data-testid="products-grid">
            {products.map((product) => (
              <div
                key={product.id}
                data-testid={`product-${product.id}`}
                className="product-card"
                onClick={() => handleProductClick(product)}
                style={{ cursor: 'pointer', border: '1px solid #ccc', margin: '8px', padding: '16px' }}
              >
                <h3 data-testid={`product-name-${product.id}`}>{product.name}</h3>
                <p data-testid={`product-price-${product.id}`}>${product.price}</p>
                <p data-testid={`product-category-${product.id}`}>{product.category}</p>
                <p data-testid={`product-stock-${product.id}`}>
                  {product.inStock ? `In Stock (${product.stockCount})` : 'Out of Stock'}
                </p>
              </div>
            ))}
          </div>
          
          {pagination && pagination.totalPages > 1 && (
            <div data-testid="pagination">
              <button
                data-testid="prev-page"
                onClick={() => handlePageChange(currentPage - 1)}
                disabled={currentPage === 1}
              >
                Previous
              </button>
              
              <span data-testid="page-info">
                Page {pagination.page} of {pagination.totalPages}
              </span>
              
              <button
                data-testid="next-page"
                onClick={() => handlePageChange(currentPage + 1)}
                disabled={currentPage === pagination.totalPages}
              >
                Next
              </button>
            </div>
          )}
        </>
      )}
    </div>
  );
};

// Integration tests
describe('API Integration Tests', () => {
  describe('UserProfile Component Integration', () => {
    it('should load and display user data', async () => {
      render(<UserProfile userId="user-1" />);

      // Initially shows loading
      expect(screen.getByTestId('loading')).toBeInTheDocument();

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

      // Check displayed user data
      expect(screen.getByTestId('user-name')).toHaveTextContent('Name: John Doe');
      expect(screen.getByTestId('user-email')).toHaveTextContent('Email: [email protected]');
      expect(screen.getByTestId('user-role')).toHaveTextContent('Role: user');
      expect(screen.getByTestId('user-status')).toHaveTextContent('Status: Active');
    });

    it('should handle user not found error', async () => {
      render(<UserProfile userId="non-existent" />);

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

      expect(screen.getByTestId('error')).toHaveTextContent('Error: User not found');
    });

    it('should enable edit mode and update user', async () => {
      const user = userEvent.setup();
      const onUserUpdate = jest.fn();
      
      render(<UserProfile userId="user-1" onUserUpdate={onUserUpdate} />);

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

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

      // Should show edit form
      expect(screen.getByTestId('edit-form')).toBeInTheDocument();
      expect(screen.getByTestId('name-input')).toHaveValue('John Doe');
      expect(screen.getByTestId('email-input')).toHaveValue('[email protected]');

      // Update name
      await user.clear(screen.getByTestId('name-input'));
      await user.type(screen.getByTestId('name-input'), 'John Updated');

      // Toggle active status
      await user.click(screen.getByTestId('active-checkbox'));

      // Save changes
      await user.click(screen.getByTestId('save-button'));

      // Wait for update to complete
      await waitFor(() => {
        expect(screen.getByTestId('view-mode')).toBeInTheDocument();
      });

      // Check updated display
      expect(screen.getByTestId('user-name')).toHaveTextContent('Name: John Updated');
      expect(screen.getByTestId('user-status')).toHaveTextContent('Status: Inactive');

      // Check callback was called
      expect(onUserUpdate).toHaveBeenCalledWith(
        expect.objectContaining({
          id: 'user-1',
          name: 'John Updated',
          isActive: false
        })
      );
    });

    it('should handle update errors gracefully', async () => {
      const user = userEvent.setup();
      
      // Mock update failure
      server.use(
        rest.patch('https://api.example.com/users/:id', (req, res, ctx) => {
          return res(
            ctx.status(400),
            ctx.json({
              message: 'Invalid email format',
              code: 'VALIDATION_ERROR',
              field: 'email'
            })
          );
        })
      );

      render(<UserProfile userId="user-1" />);

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

      await user.click(screen.getByTestId('edit-button'));

      // Enter invalid email
      await user.clear(screen.getByTestId('email-input'));
      await user.type(screen.getByTestId('email-input'), 'invalid-email');

      await user.click(screen.getByTestId('save-button'));

      // Should show update error
      await waitFor(() => {
        expect(screen.getByTestId('update-error')).toBeInTheDocument();
      });

      expect(screen.getByTestId('update-error')).toHaveTextContent(
        'Update Error: Invalid email format'
      );

      // Should remain in edit mode
      expect(screen.getByTestId('edit-form')).toBeInTheDocument();
    });

    it('should cancel edit mode and restore original data', async () => {
      const user = userEvent.setup();
      
      render(<UserProfile userId="user-1" />);

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

      await user.click(screen.getByTestId('edit-button'));

      // Make changes
      await user.clear(screen.getByTestId('name-input'));
      await user.type(screen.getByTestId('name-input'), 'Changed Name');

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

      // Should return to view mode with original data
      expect(screen.getByTestId('view-mode')).toBeInTheDocument();
      expect(screen.getByTestId('user-name')).toHaveTextContent('Name: John Doe');
    });
  });

  describe('ProductList Component Integration', () => {
    it('should load and display products', async () => {
      render(<ProductList />);

      // Initially shows loading
      expect(screen.getByTestId('products-loading')).toBeInTheDocument();

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

      // Check that products are displayed
      expect(screen.getByTestId('products-grid')).toBeInTheDocument();
      expect(screen.getByTestId('product-product-1')).toBeInTheDocument();
      expect(screen.getByTestId('product-name-product-1')).toHaveTextContent('Wireless Headphones');
      expect(screen.getByTestId('product-price-product-1')).toHaveTextContent('$99.99');
    });

    it('should filter products by category', async () => {
      render(<ProductList category="electronics" />);

      await waitFor(() => {
        expect(screen.getByText('Products - electronics')).toBeInTheDocument();
      });

      // Should only show electronics products
      const productElements = screen.getAllByTestId(/^product-product-/);
      expect(productElements).toHaveLength(2); // Only electronics products
    });

    it('should search products', async () => {
      const user = userEvent.setup();
      
      render(<ProductList />);

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

      // Search for wireless products
      await user.type(screen.getByTestId('search-input'), 'wireless');

      await waitFor(() => {
        const productElements = screen.getAllByTestId(/^product-product-/);
        expect(productElements).toHaveLength(1);
        expect(screen.getByTestId('product-name-product-1')).toHaveTextContent('Wireless Headphones');
      });
    });

    it('should handle pagination', async () => {
      const user = userEvent.setup();
      
      // Mock pagination response
      server.use(
        rest.get('https://api.example.com/products', (req, res, ctx) => {
          const page = Number(req.url.searchParams.get('page')) || 1;
          const limit = Number(req.url.searchParams.get('limit')) || 10;
          
          const allProducts = Array.from({ length: 25 }, (_, i) => ({
            id: `product-${i + 1}`,
            name: `Product ${i + 1}`,
            price: 10.99 + i,
            description: `Description for product ${i + 1}`,
            category: 'test',
            inStock: true,
            stockCount: 10,
            tags: ['test'],
            createdAt: '2023-01-01T00:00:00Z'
          }));
          
          const startIndex = (page - 1) * limit;
          const endIndex = startIndex + limit;
          const paginatedProducts = allProducts.slice(startIndex, endIndex);
          
          return res(
            ctx.status(200),
            ctx.json({
              data: paginatedProducts,
              pagination: {
                page,
                limit,
                total: allProducts.length,
                totalPages: Math.ceil(allProducts.length / limit)
              }
            })
          );
        })
      );

      render(<ProductList />);

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

      // Check initial page info
      expect(screen.getByTestId('page-info')).toHaveTextContent('Page 1 of 3');
      expect(screen.getByTestId('prev-page')).toBeDisabled();

      // Go to next page
      await user.click(screen.getByTestId('next-page'));

      await waitFor(() => {
        expect(screen.getByTestId('page-info')).toHaveTextContent('Page 2 of 3');
      });

      expect(screen.getByTestId('prev-page')).toBeEnabled();
      expect(screen.getByTestId('next-page')).toBeEnabled();
    });

    it('should handle product selection', async () => {
      const user = userEvent.setup();
      const onProductSelect = jest.fn();
      
      render(<ProductList onProductSelect={onProductSelect} />);

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

      // Click on a product
      await user.click(screen.getByTestId('product-product-1'));

      expect(onProductSelect).toHaveBeenCalledWith(
        expect.objectContaining({
          id: 'product-1',
          name: 'Wireless Headphones'
        })
      );
    });

    it('should show no products message when search yields no results', async () => {
      const user = userEvent.setup();
      
      render(<ProductList />);

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

      // Search for non-existent product
      await user.type(screen.getByTestId('search-input'), 'nonexistent');

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

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

    it('should handle API errors gracefully', async () => {
      // Mock API error
      server.use(
        rest.get('https://api.example.com/products', (req, res, ctx) => {
          return res(
            ctx.status(500),
            ctx.json({
              message: 'Internal server error',
              code: 'INTERNAL_SERVER_ERROR'
            })
          );
        })
      );

      render(<ProductList />);

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

      expect(screen.getByTestId('products-error')).toHaveTextContent(
        'Error loading products: Internal server error'
      );
    });
  });

  describe('End-to-End Workflow Tests', () => {
    it('should handle complete user management workflow', async () => {
      const user = userEvent.setup();
      
      render(<UserProfile userId="user-2" />);

      // Load user
      await waitFor(() => {
        expect(screen.getByTestId('user-name')).toHaveTextContent('Name: Jane Smith');
      });

      // Edit user
      await user.click(screen.getByTestId('edit-button'));
      await user.clear(screen.getByTestId('name-input'));
      await user.type(screen.getByTestId('name-input'), 'Jane Updated Smith');

      // Save changes
      await user.click(screen.getByTestId('save-button'));

      // Verify update
      await waitFor(() => {
        expect(screen.getByTestId('user-name')).toHaveTextContent('Name: Jane Updated Smith');
      });

      // Edit again with different changes
      await user.click(screen.getByTestId('edit-button'));
      await user.click(screen.getByTestId('active-checkbox')); // Toggle status

      // Cancel this time
      await user.click(screen.getByTestId('cancel-button'));

      // Should show previous saved state, not cancelled changes
      expect(screen.getByTestId('user-name')).toHaveTextContent('Name: Jane Updated Smith');
      expect(screen.getByTestId('user-status')).toHaveTextContent('Status: Active');
    });

    it('should handle product search and filtering workflow', async () => {
      const user = userEvent.setup();
      const onProductSelect = jest.fn();
      
      render(<ProductList onProductSelect={onProductSelect} />);

      // Load all products
      await waitFor(() => {
        expect(screen.getAllByTestId(/^product-product-/)).toHaveLength(3);
      });

      // Search for specific product
      await user.type(screen.getByTestId('search-input'), 'smartphone');

      await waitFor(() => {
        expect(screen.getAllByTestId(/^product-product-/)).toHaveLength(1);
      });

      // Select the found product
      await user.click(screen.getByTestId('product-product-2'));

      expect(onProductSelect).toHaveBeenCalledWith(
        expect.objectContaining({
          id: 'product-2',
          name: 'Smartphone'
        })
      );

      // Clear search to see all products again
      await user.clear(screen.getByTestId('search-input'));

      await waitFor(() => {
        expect(screen.getAllByTestId(/^product-product-/)).toHaveLength(3);
      });
    });
  });
});

πŸŽ‰ Conclusion and Best Practices

Congratulations! You’ve mastered the art of API testing with Mock Service Worker in TypeScript! 🌟

πŸ”‘ Key Takeaways

  • MSW provides realistic testing by intercepting actual HTTP requests at the network level
  • Test all scenarios: success, errors, network failures, and edge cases
  • Use comprehensive mocking for different response conditions and request validation
  • Test API hooks independently and in integration with components
  • Cover complex workflows with multi-step user interactions and state management

πŸš€ Best Practices Summary

  1. Setup MSW properly with beforeAll/afterEach/afterAll hooks
  2. Create reusable mock handlers for different API scenarios
  3. Test error conditions as thoroughly as success cases
  4. Use realistic test data that mirrors production responses
  5. Verify API calls with proper parameters and headers
  6. Test retry logic and timeout scenarios
  7. Cover edge cases like empty responses and validation errors
  8. Test integration flows that combine multiple API calls

🎯 Next Steps

With these API testing skills, you’re ready to:

  • Build comprehensive test suites for API-driven applications
  • Test complex async workflows with confidence
  • Handle error scenarios and network failures gracefully
  • Create maintainable tests that provide real value
  • Integrate API testing into CI/CD pipelines

Keep practicing these patterns and exploring advanced MSW features to become a true API testing expert! πŸ†