---
name: testing-patterns
description: Write comprehensive tests for Mawidi including unit tests, integration tests, E2E tests, and API tests. Use when adding new features, fixing bugs, improving coverage, or validating functionality. Covers React Testing Library, Jest, and Playwright patterns.
---

# Testing Patterns Skill

## Overview

Write comprehensive tests for Mawidi following established patterns. This skill covers unit tests, integration tests, E2E tests, and API tests using the project's testing stack.

**Testing Stack:**

- **Jest** - Unit and integration tests
- **React Testing Library** - Component tests
- **Playwright** - E2E browser tests
- **MSW** - API mocking

**Coverage Target:** 70%+ for critical paths

## When to Use

Use this skill when:

- Adding new features (TDD approach)
- Fixing bugs (regression tests)
- Improving test coverage
- Validating API endpoints
- Testing authentication flows
- Writing E2E user journey tests

## Test File Structure

```
mawidi-site/
├── __tests__/
│   ├── api/              # API route tests
│   │   ├── auth/
│   │   ├── bookings/
│   │   └── stripe/
│   ├── components/       # Component unit tests
│   │   ├── forms/
│   │   └── ui/
│   ├── integration/      # Integration tests
│   ├── lib/              # Utility function tests
│   └── setup.ts          # Test setup
├── e2e/                  # Playwright E2E tests
│   ├── auth.spec.ts
│   ├── booking.spec.ts
│   └── signup.spec.ts
└── jest.config.js
```

## Unit Testing Patterns

### Testing React Components

```tsx
// __tests__/components/BookingForm.test.tsx

import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { BookingForm } from "@/components/BookingForm";

// Mock dependencies
jest.mock("@/lib/api", () => ({
  createBooking: jest.fn(),
}));

describe("BookingForm", () => {
  const mockOnSubmit = jest.fn();

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

  it("renders all form fields", () => {
    render(<BookingForm onSubmit={mockOnSubmit} />);

    expect(screen.getByLabelText(/date/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/time/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/service/i)).toBeInTheDocument();
    expect(screen.getByRole("button", { name: /book/i })).toBeInTheDocument();
  });

  it("validates required fields", async () => {
    render(<BookingForm onSubmit={mockOnSubmit} />);

    const submitButton = screen.getByRole("button", { name: /book/i });
    await userEvent.click(submitButton);

    expect(screen.getByText(/date is required/i)).toBeInTheDocument();
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });

  it("submits form with valid data", async () => {
    render(<BookingForm onSubmit={mockOnSubmit} />);

    // Fill form
    await userEvent.type(screen.getByLabelText(/date/i), "2025-02-01");
    await userEvent.selectOptions(screen.getByLabelText(/service/i), "dental");
    await userEvent.click(screen.getByRole("button", { name: /book/i }));

    await waitFor(() => {
      expect(mockOnSubmit).toHaveBeenCalledWith({
        date: "2025-02-01",
        service: "dental",
      });
    });
  });

  it("shows loading state during submission", async () => {
    mockOnSubmit.mockImplementation(
      () => new Promise((r) => setTimeout(r, 100)),
    );
    render(<BookingForm onSubmit={mockOnSubmit} />);

    await userEvent.type(screen.getByLabelText(/date/i), "2025-02-01");
    await userEvent.click(screen.getByRole("button", { name: /book/i }));

    expect(screen.getByRole("button", { name: /loading/i })).toBeDisabled();
  });
});
```

### Testing Hooks

```tsx
// __tests__/hooks/useBookings.test.tsx

import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useBookings } from "@/hooks/useBookings";

const createWrapper = () => {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

describe("useBookings", () => {
  it("fetches bookings for user", async () => {
    global.fetch = jest.fn().mockResolvedValue({
      ok: true,
      json: () =>
        Promise.resolve({
          data: [{ id: "1", date: "2025-02-01" }],
        }),
    });

    const { result } = renderHook(() => useBookings("user-123"), {
      wrapper: createWrapper(),
    });

    await waitFor(() => expect(result.current.isSuccess).toBe(true));
    expect(result.current.data).toHaveLength(1);
  });

  it("handles error state", async () => {
    global.fetch = jest.fn().mockRejectedValue(new Error("Network error"));

    const { result } = renderHook(() => useBookings("user-123"), {
      wrapper: createWrapper(),
    });

    await waitFor(() => expect(result.current.isError).toBe(true));
  });
});
```

### Testing Utility Functions

```typescript
// __tests__/lib/formatters.test.ts

import { formatCurrency, formatDate, formatPhone } from "@/lib/formatters";

describe("formatCurrency", () => {
  it("formats QAR correctly", () => {
    expect(formatCurrency(99.99, "QAR", "en")).toBe("QAR 99.99");
    expect(formatCurrency(99.99, "QAR", "ar")).toMatch(/٩٩٫٩٩/);
  });

  it("handles zero", () => {
    expect(formatCurrency(0, "QAR", "en")).toBe("QAR 0.00");
  });

  it("handles large numbers", () => {
    expect(formatCurrency(1000000, "QAR", "en")).toBe("QAR 1,000,000.00");
  });
});

describe("formatDate", () => {
  const testDate = new Date("2025-01-15T10:00:00Z");

  it("formats in English", () => {
    expect(formatDate(testDate, "en")).toBe("January 15, 2025");
  });

  it("formats in Arabic", () => {
    expect(formatDate(testDate, "ar")).toMatch(/يناير/);
  });
});

describe("formatPhone", () => {
  it("formats Qatar numbers", () => {
    expect(formatPhone("+97412345678")).toBe("+974 1234 5678");
  });

  it("handles invalid input", () => {
    expect(formatPhone("")).toBe("");
    expect(formatPhone("invalid")).toBe("invalid");
  });
});
```

## API Testing Patterns

### Testing API Routes

```typescript
// __tests__/api/bookings/route.test.ts

import { NextRequest } from "next/server";
import { GET, POST } from "@/app/api/bookings/route";
import { prisma } from "@/lib/db";

// Mock Prisma
jest.mock("@/lib/db", () => ({
  prisma: {
    booking: {
      findMany: jest.fn(),
      create: jest.fn(),
    },
  },
}));

// Mock NextAuth
jest.mock("next-auth", () => ({
  getServerSession: jest.fn(),
}));

describe("GET /api/bookings", () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it("returns 401 without authentication", async () => {
    const { getServerSession } = require("next-auth");
    getServerSession.mockResolvedValue(null);

    const request = new NextRequest("http://localhost/api/bookings");
    const response = await GET(request);

    expect(response.status).toBe(401);
  });

  it("returns user bookings", async () => {
    const { getServerSession } = require("next-auth");
    getServerSession.mockResolvedValue({
      user: { id: "user-123" },
    });

    (prisma.booking.findMany as jest.Mock).mockResolvedValue([
      { id: "1", date: new Date(), userId: "user-123" },
    ]);

    const request = new NextRequest("http://localhost/api/bookings");
    const response = await GET(request);
    const data = await response.json();

    expect(response.status).toBe(200);
    expect(data.data).toHaveLength(1);
  });
});

describe("POST /api/bookings", () => {
  it("validates input", async () => {
    const { getServerSession } = require("next-auth");
    getServerSession.mockResolvedValue({
      user: { id: "user-123" },
    });

    const request = new NextRequest("http://localhost/api/bookings", {
      method: "POST",
      body: JSON.stringify({ date: "invalid" }), // Invalid data
    });

    const response = await POST(request);
    expect(response.status).toBe(400);
  });

  it("creates booking with valid data", async () => {
    const { getServerSession } = require("next-auth");
    getServerSession.mockResolvedValue({
      user: { id: "user-123" },
    });

    (prisma.booking.create as jest.Mock).mockResolvedValue({
      id: "new-booking",
      date: new Date("2025-02-01"),
      userId: "user-123",
    });

    const request = new NextRequest("http://localhost/api/bookings", {
      method: "POST",
      body: JSON.stringify({
        date: "2025-02-01",
        serviceId: "service-123",
      }),
    });

    const response = await POST(request);
    expect(response.status).toBe(201);
  });
});
```

### Testing with MSW (Mock Service Worker)

```typescript
// __tests__/setup/msw-handlers.ts

import { rest } from "msw";

export const handlers = [
  rest.get("/api/bookings", (req, res, ctx) => {
    return res(
      ctx.json({
        data: [{ id: "1", date: "2025-02-01", status: "confirmed" }],
      }),
    );
  }),

  rest.post("/api/bookings", async (req, res, ctx) => {
    const body = await req.json();

    if (!body.date) {
      return res(ctx.status(400), ctx.json({ error: "Date is required" }));
    }

    return res(ctx.status(201), ctx.json({ data: { id: "new", ...body } }));
  }),

  rest.post("/api/auth/login", async (req, res, ctx) => {
    const { email, password } = await req.json();

    if (email === "test@example.com" && password === "password123") {
      return res(ctx.json({ user: { id: "1", email } }));
    }

    return res(ctx.status(401), ctx.json({ error: "Invalid credentials" }));
  }),
];
```

## Integration Testing Patterns

### Database Integration Tests

```typescript
// __tests__/integration/booking-flow.test.ts

import { prisma } from "@/lib/db";
import { createBooking, cancelBooking } from "@/lib/services/booking.service";

describe("Booking Service Integration", () => {
  let testUserId: string;
  let testServiceId: string;

  beforeAll(async () => {
    // Create test data
    const user = await prisma.user.create({
      data: { email: "test@example.com", name: "Test User" },
    });
    testUserId = user.id;

    const service = await prisma.service.create({
      data: { name: "Test Service", duration: 60 },
    });
    testServiceId = service.id;
  });

  afterAll(async () => {
    // Cleanup
    await prisma.booking.deleteMany({ where: { userId: testUserId } });
    await prisma.service.delete({ where: { id: testServiceId } });
    await prisma.user.delete({ where: { id: testUserId } });
    await prisma.$disconnect();
  });

  it("creates and cancels booking", async () => {
    // Create booking
    const booking = await createBooking({
      userId: testUserId,
      serviceId: testServiceId,
      date: new Date("2025-02-01"),
    });

    expect(booking.status).toBe("PENDING");

    // Cancel booking
    const cancelled = await cancelBooking(booking.id, "Changed plans");

    expect(cancelled.status).toBe("CANCELLED");
    expect(cancelled.cancelReason).toBe("Changed plans");
  });
});
```

## E2E Testing Patterns (Playwright)

### Authentication Flow

```typescript
// e2e/auth.spec.ts

import { test, expect } from "@playwright/test";

test.describe("Authentication", () => {
  test("user can log in with OTP", async ({ page }) => {
    // Navigate to login
    await page.goto("/en/login");

    // Enter email
    await page.fill('[data-testid="email-input"]', "test@example.com");
    await page.click('[data-testid="send-otp-button"]');

    // Wait for OTP sent message
    await expect(page.locator("text=OTP sent")).toBeVisible();

    // In test environment, get OTP from MailHog or mock
    const otp = "123456"; // From test setup
    await page.fill('[data-testid="otp-input"]', otp);
    await page.click('[data-testid="verify-otp-button"]');

    // Should redirect to dashboard
    await expect(page).toHaveURL(/.*dashboard/);
    await expect(page.locator("text=Welcome")).toBeVisible();
  });

  test("shows error for invalid OTP", async ({ page }) => {
    await page.goto("/en/login");
    await page.fill('[data-testid="email-input"]', "test@example.com");
    await page.click('[data-testid="send-otp-button"]');

    await page.fill('[data-testid="otp-input"]', "000000");
    await page.click('[data-testid="verify-otp-button"]');

    await expect(page.locator("text=Invalid OTP")).toBeVisible();
  });
});
```

### Booking Flow E2E

```typescript
// e2e/booking.spec.ts

import { test, expect } from "@playwright/test";

test.describe("Booking Flow", () => {
  test.beforeEach(async ({ page }) => {
    // Login first
    await page.goto("/en/login");
    // ... login steps
  });

  test("user can create a booking", async ({ page }) => {
    await page.goto("/en/dashboard");
    await page.click('[data-testid="new-booking-button"]');

    // Select service
    await page.selectOption('[data-testid="service-select"]', "dental-checkup");

    // Select date
    await page.click('[data-testid="date-picker"]');
    await page.click("text=15"); // Select 15th

    // Select time
    await page.click('[data-testid="time-slot-10:00"]');

    // Submit
    await page.click('[data-testid="confirm-booking-button"]');

    // Verify success
    await expect(page.locator("text=Booking confirmed")).toBeVisible();
    await expect(page.locator('[data-testid="booking-card"]')).toContainText(
      "dental-checkup",
    );
  });
});
```

### RTL Testing

```typescript
// e2e/rtl.spec.ts

import { test, expect } from "@playwright/test";

test.describe("RTL Support", () => {
  test("Arabic pages render in RTL", async ({ page }) => {
    await page.goto("/ar");

    // Check direction
    const html = page.locator("html");
    await expect(html).toHaveAttribute("dir", "rtl");
    await expect(html).toHaveAttribute("lang", "ar");

    // Check text alignment
    const heading = page.locator("h1").first();
    const textAlign = await heading.evaluate(
      (el) => window.getComputedStyle(el).textAlign,
    );
    expect(textAlign).toBe("right");
  });

  test("language switcher works", async ({ page }) => {
    await page.goto("/en");
    await expect(page.locator("html")).toHaveAttribute("dir", "ltr");

    // Switch to Arabic
    await page.click('[data-testid="lang-switcher"]');
    await page.click("text=العربية");

    await expect(page).toHaveURL(/\/ar\//);
    await expect(page.locator("html")).toHaveAttribute("dir", "rtl");
  });
});
```

## Test Commands

```bash
# Run all tests
npm run test

# Run unit tests only
npm run test:unit

# Run with coverage
npm run test:coverage

# Run specific file
npm run test -- BookingForm.test.tsx

# Run E2E tests
npm run test:e2e

# Run E2E with UI
npm run test:e2e -- --ui

# Run specific E2E test
npm run test:e2e -- auth.spec.ts
```

## Test Data Helpers

```typescript
// __tests__/helpers/factories.ts

import { faker } from "@faker-js/faker";

export const createMockUser = (overrides = {}) => ({
  id: faker.string.uuid(),
  email: faker.internet.email(),
  name: faker.person.fullName(),
  createdAt: new Date(),
  ...overrides,
});

export const createMockBooking = (overrides = {}) => ({
  id: faker.string.uuid(),
  date: faker.date.future(),
  status: "PENDING",
  userId: faker.string.uuid(),
  serviceId: faker.string.uuid(),
  ...overrides,
});

export const createMockService = (overrides = {}) => ({
  id: faker.string.uuid(),
  name: faker.commerce.productName(),
  duration: 60,
  price: parseFloat(faker.commerce.price()),
  ...overrides,
});
```

## Quick Reference

### Test Naming Convention

```typescript
// Component tests
describe("ComponentName", () => {
  it("renders correctly", () => {});
  it("handles user interaction", () => {});
  it("shows error state", () => {});
});

// API tests
describe("GET /api/resource", () => {
  it("returns 401 without auth", () => {});
  it("returns data for valid request", () => {});
});

// E2E tests
test("user can complete [action]", () => {});
```

### Coverage Requirements

| Category       | Minimum Coverage |
| -------------- | ---------------- |
| Critical paths | 90%              |
| API routes     | 80%              |
| Components     | 70%              |
| Utilities      | 80%              |

## See Also

- [run-full-test-suite.ts](../run-full-test-suite.ts) - Comprehensive test runner
- [api-development](../api-development/SKILL.md) - API patterns to test
- `README_TESTING_SYSTEM.md` - Project testing documentation
