# 🧪 TESTS - BTPXPRESS FRONTEND

## 📋 Table des matières

- [Vue d'ensemble](#vue-densemble)
- [Configuration](#configuration)
- [Tests unitaires](#tests-unitaires)
- [Tests de composants](#tests-de-composants)
- [Tests d'intégration](#tests-dintégration)
- [Tests E2E](#tests-e2e)
- [Couverture de code](#couverture-de-code)
- [Bonnes pratiques](#bonnes-pratiques)

---

## 🎯 Vue d'ensemble

### **Framework de tests**

- **Jest** : Framework de tests
- **React Testing Library** : Tests de composants React
- **MSW (Mock Service Worker)** : Mock des API
- **Playwright** : Tests end-to-end (optionnel)

### **Objectifs de couverture**

| Type | Objectif |
|------|----------|
| **Ligne** | 80% |
| **Branche** | 70% |
| **Fonction** | 85% |

---

## ⚙️ Configuration

### **jest.config.js**

```javascript
const nextJest = require('next/jest')

const createJestConfig = nextJest({
  dir: './',
})

const customJestConfig = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jest-environment-jsdom',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
  },
  collectCoverageFrom: [
    'app/**/*.{js,jsx,ts,tsx}',
    'components/**/*.{js,jsx,ts,tsx}',
    'services/**/*.{js,jsx,ts,tsx}',
    '!**/*.d.ts',
    '!**/node_modules/**',
  ],
}

module.exports = createJestConfig(customJestConfig)
```

### **jest.setup.js**

```javascript
import '@testing-library/jest-dom'

// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(),
    removeListener: jest.fn(),
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
})
```

### **package.json**

```json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  },
  "devDependencies": {
    "@testing-library/react": "^14.0.0",
    "@testing-library/jest-dom": "^6.1.4",
    "@testing-library/user-event": "^14.5.1",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0"
  }
}
```

---

## 🔬 Tests unitaires

### **Test d'un service**

**Fichier** : `services/api/__tests__/chantierService.test.ts`

```typescript
import { chantierService } from '../chantierService';
import apiClient from '../apiClient';

jest.mock('../apiClient');

describe('chantierService', () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('getAll', () => {
    it('devrait retourner la liste des chantiers', async () => {
      // Arrange
      const mockChantiers = [
        { id: '1', nom: 'Chantier 1' },
        { id: '2', nom: 'Chantier 2' },
      ];
      (apiClient.get as jest.Mock).mockResolvedValue({ data: mockChantiers });

      // Act
      const result = await chantierService.getAll();

      // Assert
      expect(apiClient.get).toHaveBeenCalledWith('/chantiers');
      expect(result).toEqual(mockChantiers);
    });

    it('devrait gérer les erreurs', async () => {
      // Arrange
      const error = new Error('Network error');
      (apiClient.get as jest.Mock).mockRejectedValue(error);

      // Act & Assert
      await expect(chantierService.getAll()).rejects.toThrow('Network error');
    });
  });

  describe('create', () => {
    it('devrait créer un chantier', async () => {
      // Arrange
      const newChantier = { nom: 'Nouveau Chantier', code: 'NC-001' };
      const createdChantier = { id: '1', ...newChantier };
      (apiClient.post as jest.Mock).mockResolvedValue({ data: createdChantier });

      // Act
      const result = await chantierService.create(newChantier);

      // Assert
      expect(apiClient.post).toHaveBeenCalledWith('/chantiers', newChantier);
      expect(result).toEqual(createdChantier);
    });
  });
});
```

---

## 🧩 Tests de composants

### **Test d'un composant simple**

**Fichier** : `components/common/__tests__/LoadingSpinner.test.tsx`

```typescript
import { render, screen } from '@testing-library/react';
import { LoadingSpinner } from '../LoadingSpinner';

describe('LoadingSpinner', () => {
  it('devrait afficher le spinner', () => {
    render(<LoadingSpinner />);
    
    const spinner = screen.getByRole('progressbar');
    expect(spinner).toBeInTheDocument();
  });
});
```

### **Test d'un formulaire**

**Fichier** : `components/forms/__tests__/ChantierForm.test.tsx`

```typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ChantierForm } from '../ChantierForm';

describe('ChantierForm', () => {
  const mockOnSubmit = jest.fn();
  const mockOnCancel = jest.fn();

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

  it('devrait afficher le formulaire vide', () => {
    render(
      <ChantierForm
        onSubmit={mockOnSubmit}
        onCancel={mockOnCancel}
      />
    );

    expect(screen.getByLabelText(/nom/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/statut/i)).toBeInTheDocument();
  });

  it('devrait afficher les données du chantier en mode édition', () => {
    const chantier = {
      id: '1',
      nom: 'Villa Moderne',
      code: 'VM-001',
      statut: 'EN_COURS',
    };

    render(
      <ChantierForm
        chantier={chantier}
        onSubmit={mockOnSubmit}
        onCancel={mockOnCancel}
      />
    );

    expect(screen.getByDisplayValue('Villa Moderne')).toBeInTheDocument();
  });

  it('devrait valider les champs obligatoires', async () => {
    render(
      <ChantierForm
        onSubmit={mockOnSubmit}
        onCancel={mockOnCancel}
      />
    );

    const submitButton = screen.getByRole('button', { name: /enregistrer/i });
    fireEvent.click(submitButton);

    await waitFor(() => {
      expect(screen.getByText(/le nom est obligatoire/i)).toBeInTheDocument();
    });

    expect(mockOnSubmit).not.toHaveBeenCalled();
  });

  it('devrait soumettre le formulaire avec des données valides', async () => {
    const user = userEvent.setup();

    render(
      <ChantierForm
        onSubmit={mockOnSubmit}
        onCancel={mockOnCancel}
      />
    );

    // Remplir le formulaire
    const nomInput = screen.getByLabelText(/nom/i);
    await user.type(nomInput, 'Villa Moderne');

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

    await waitFor(() => {
      expect(mockOnSubmit).toHaveBeenCalledWith(
        expect.objectContaining({
          nom: 'Villa Moderne',
        })
      );
    });
  });

  it('devrait appeler onCancel lors du clic sur Annuler', async () => {
    const user = userEvent.setup();

    render(
      <ChantierForm
        onSubmit={mockOnSubmit}
        onCancel={mockOnCancel}
      />
    );

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

    expect(mockOnCancel).toHaveBeenCalled();
  });
});
```

### **Test d'un tableau**

**Fichier** : `components/tables/__tests__/DataTableWrapper.test.tsx`

```typescript
import { render, screen, fireEvent } from '@testing-library/react';
import { DataTableWrapper } from '../DataTableWrapper';

describe('DataTableWrapper', () => {
  const mockData = [
    { id: '1', nom: 'Chantier 1', code: 'CH-001' },
    { id: '2', nom: 'Chantier 2', code: 'CH-002' },
  ];

  const columns = [
    { field: 'nom', header: 'Nom', sortable: true },
    { field: 'code', header: 'Code', sortable: true },
  ];

  it('devrait afficher les données', () => {
    render(
      <DataTableWrapper
        data={mockData}
        columns={columns}
      />
    );

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

  it('devrait afficher un message si aucune donnée', () => {
    render(
      <DataTableWrapper
        data={[]}
        columns={columns}
      />
    );

    expect(screen.getByText(/aucune donnée disponible/i)).toBeInTheDocument();
  });

  it('devrait appeler onEdit lors du clic sur éditer', () => {
    const mockOnEdit = jest.fn();

    render(
      <DataTableWrapper
        data={mockData}
        columns={columns}
        onEdit={mockOnEdit}
      />
    );

    const editButtons = screen.getAllByRole('button', { name: /edit/i });
    fireEvent.click(editButtons[0]);

    expect(mockOnEdit).toHaveBeenCalledWith(mockData[0]);
  });
});
```

---

## 🔗 Tests d'intégration

### **Test d'une page**

**Fichier** : `app/(auth)/chantiers/__tests__/page.test.tsx`

```typescript
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import ChantiersPage from '../page';

const server = setupServer(
  rest.get('/api/v1/chantiers', (req, res, ctx) => {
    return res(
      ctx.json([
        { id: '1', nom: 'Chantier 1', statut: 'EN_COURS' },
        { id: '2', nom: 'Chantier 2', statut: 'PLANIFIE' },
      ])
    );
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('ChantiersPage', () => {
  it('devrait charger et afficher les chantiers', async () => {
    render(<ChantiersPage />);

    expect(screen.getByText(/chargement/i)).toBeInTheDocument();

    await waitFor(() => {
      expect(screen.getByText('Chantier 1')).toBeInTheDocument();
      expect(screen.getByText('Chantier 2')).toBeInTheDocument();
    });
  });

  it('devrait afficher une erreur en cas d\'échec', async () => {
    server.use(
      rest.get('/api/v1/chantiers', (req, res, ctx) => {
        return res(ctx.status(500));
      })
    );

    render(<ChantiersPage />);

    await waitFor(() => {
      expect(screen.getByText(/erreur/i)).toBeInTheDocument();
    });
  });
});
```

---

## 🎭 Tests E2E

### **Playwright (optionnel)**

**Installation** :
```bash
npm install -D @playwright/test
npx playwright install
```

**Fichier** : `e2e/chantiers.spec.ts`

```typescript
import { test, expect } from '@playwright/test';

test.describe('Chantiers', () => {
  test('devrait afficher la liste des chantiers', async ({ page }) => {
    await page.goto('http://localhost:3000/chantiers');

    await expect(page.locator('h1')).toContainText('Chantiers');
    await expect(page.locator('table')).toBeVisible();
  });

  test('devrait créer un nouveau chantier', async ({ page }) => {
    await page.goto('http://localhost:3000/chantiers/nouveau');

    await page.fill('input[name="nom"]', 'Villa Test');
    await page.fill('input[name="code"]', 'VT-001');
    await page.click('button[type="submit"]');

    await expect(page).toHaveURL(/\/chantiers\/\w+/);
    await expect(page.locator('text=Villa Test')).toBeVisible();
  });
});
```

---

## 📊 Couverture de code

### **Générer le rapport**

```bash
npm run test:coverage
```

### **Rapport HTML**

Le rapport est généré dans `coverage/lcov-report/index.html`

```bash
# Ouvrir le rapport
open coverage/lcov-report/index.html
```

---

## ✅ Bonnes pratiques

### **1. Nommage des tests**

```typescript
// ❌ Mauvais
it('test 1', () => { });

// ✅ Bon
it('devrait afficher le formulaire vide', () => { });
```

### **2. Pattern AAA**

```typescript
it('devrait créer un chantier', async () => {
  // Arrange
  const data = { nom: 'Test' };

  // Act
  const result = await service.create(data);

  // Assert
  expect(result).toBeDefined();
});
```

### **3. Tests isolés**

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

### **4. Utiliser screen queries**

```typescript
// ✅ Bon
const button = screen.getByRole('button', { name: /enregistrer/i });

// ❌ Éviter
const button = container.querySelector('button');
```

---

**Dernière mise à jour** : 2025-09-30  
**Version** : 1.0  
**Auteur** : Équipe BTPXpress

