Skip to content

Configuration Laravel 12 + React + Tests

Vue d'ensemble

Template complet pour applications d'entreprise avec Laravel 12 backend, React frontend TypeScript, et stack de tests complète incluant tests unitaires, d'intégration et E2E.

Stack technologique

  • Backend : Laravel 12 + Pest (PHP)
  • Frontend : React + TypeScript + Vitest + shadcn/ui
  • Tests E2E : Laravel Dusk
  • UI : Tailwind CSS + shadcn/ui components
  • API : Laravel API + React Query

1. Structure du projet

mon-app/
├── app/                    # Laravel backend
├── resources/
│   └── js/                # React frontend
│       ├── components/
│       ├── pages/
│       ├── hooks/
│       ├── services/
│       ├── types/
│       ├── __tests__/     # Tests unitaires React (Vitest)
│       └── test-setup.ts
├── tests/                 # Tests Laravel (Pest)
│   ├── Feature/           # Tests d'intégration Laravel
│   ├── Unit/              # Tests unitaires Laravel
│   ├── Browser/           # Tests E2E avec Laravel Dusk
│   └── TestCase.php
├── integration/           # Tests d'intégration frontend-backend
├── database/
└── public/

Types de tests par emplacement

  • Backend : /tests/Unit/ (Pest/PHP)
  • Frontend : /resources/js/__tests__/ (Vitest/JS)
  • Laravel : /tests/Feature/ (API + DB avec Pest)
  • Frontend-Backend : /integration/ (Vitest avec vraies API)
  • Laravel Dusk : /tests/Browser/ (navigateur complet)

2. Installation rapide

Backend Laravel + Dusk

# Créer le projet
composer create-project laravel/laravel mon-app
cd mon-app

# Installer Pest
composer require pestphp/pest --dev
composer require pestphp/pest-plugin-laravel --dev
./vendor/bin/pest --init

# Laravel Dusk pour E2E
composer require --dev laravel/dusk
php artisan dusk:install

# Configuration API
php artisan install:api

Frontend React + shadcn/ui

# Dépendances React + TypeScript
npm install react react-dom @types/react @types/react-dom
npm install -D @vitejs/plugin-react typescript

# Tests frontend
npm install -D vitest @testing-library/react @testing-library/jest-dom
npm install -D @testing-library/user-event jsdom

# Gestion des données
npm install @tanstack/react-query axios

# shadcn/ui setup
npm install tailwindcss postcss autoprefixer
npm install class-variance-authority clsx tailwind-merge
npm install @radix-ui/react-slot lucide-react
npx tailwindcss init -p
npx shadcn@latest init

# Composants essentiels
npx shadcn@latest add button input label table data-table form dialog select card badge skeleton toast

3. Configuration Vite

vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.tsx'],
            refresh: true,
        }),
        react(),
    ],
    test: {
        globals: true,
        environment: 'jsdom',
        setupFiles: ['./resources/js/test-setup.ts'],
        include: ['resources/js/**/*.{test,spec}.{js,ts,tsx}'],
        exclude: ['tests/**/*'], // Exclure les tests Laravel
    },
    resolve: {
        alias: {
            '@': resolve(__dirname, './resources/js'),
        },
    },
});

4. Composant DataTable avec shadcn/ui

resources/js/components/DataTable.tsx
import React from 'react';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';

interface DataTableProps {
  data: any[];
  columns: Array<{
    key: string;
    label: string;
    render?: (value: any, row: any) => React.ReactNode;
  }>;
  isLoading?: boolean;
  onSearch?: (term: string) => void;
}

export default function DataTable({ data, columns, isLoading, onSearch }: DataTableProps) {
  const [searchTerm, setSearchTerm] = React.useState('');

  const handleSearch = (value: string) => {
    setSearchTerm(value);
    onSearch?.(value);
  };

  if (isLoading) {
    return (
      <div className=\"space-y-4\">
        <Skeleton className=\"h-10 w-full\" />
        <div className=\"space-y-2\">
          {Array.from({ length: 5 }).map((_, i) => (
            <Skeleton key={i} className=\"h-16 w-full\" />
          ))}
        </div>
      </div>
    );
  }

  return (
    <div className=\"space-y-4\">
      <div className=\"flex justify-between items-center\">
        <Input
          placeholder=\"Rechercher...\"
          value={searchTerm}
          onChange={(e) => handleSearch(e.target.value)}
          className=\"max-w-sm\"
        />
        <Badge variant=\"secondary\">
          {data.length} résultat{data.length > 1 ? 's' : ''}
        </Badge>
      </div>

      <div className=\"rounded-md border\">
        <Table>
          <TableHeader>
            <TableRow>
              {columns.map((column) => (
                <TableHead key={column.key}>{column.label}</TableHead>
              ))}
            </TableRow>
          </TableHeader>
          <TableBody>
            {data.length === 0 ? (
              <TableRow>
                <TableCell colSpan={columns.length} className=\"h-24 text-center\">
                  Aucune donnée trouvée
                </TableCell>
              </TableRow>
            ) : (
              data.map((row, index) => (
                <TableRow key={index}>
                  {columns.map((column) => (
                    <TableCell key={column.key}>
                      {column.render 
                        ? column.render(row[column.key], row)
                        : row[column.key]
                      }
                    </TableCell>
                  ))}
                </TableRow>
              ))
            )}
          </TableBody>
        </Table>
      </div>
    </div>
  );
}

5. Tests d'intégration Frontend-Backend

integration/user-api.test.ts
import { beforeAll, afterAll, describe, it, expect } from 'vitest';
import axios from 'axios';

const api = axios.create({
  baseURL: 'http://localhost:8000/api',
  timeout: 5000,
});

describe('User API Integration', () => {
  beforeAll(async () => {
    await api.post('/test/setup');
  });

  afterAll(async () => {
    await api.post('/test/cleanup');
  });

  it('should create and retrieve user via API', async () => {
    const userData = {
      name: 'Integration Test User',
      email: 'integration@test.com',
      password: 'password123'
    };

    const createResponse = await api.post('/users', userData);
    expect(createResponse.status).toBe(201);
    expect(createResponse.data.data.name).toBe(userData.name);

    const userId = createResponse.data.data.id;
    const getResponse = await api.get(`/users/${userId}`);
    expect(getResponse.status).toBe(200);
    expect(getResponse.data.data.email).toBe(userData.email);
  });

  it('should handle pagination correctly', async () => {
    const response = await api.get('/users?page=1&per_page=10');

    expect(response.status).toBe(200);
    expect(response.data).toHaveProperty('data');
    expect(response.data).toHaveProperty('meta');
    expect(response.data.meta).toHaveProperty('current_page', 1);
  });
});

6. Tests E2E avec Laravel Dusk

Page Object Pattern

tests/Browser/Pages/UserPage.php
<?php

namespace Tests\\Browser\\Pages;

use Laravel\\Dusk\\Browser;
use Laravel\\Dusk\\Page;

class UserPage extends Page
{
    public function url()
    {
        return '/users';
    }

    public function assert(Browser $browser)
    {
        $browser->assertPathIs($this->url())
                ->assertSee('Utilisateurs');
    }

    public function elements()
    {
        return [
            '@add-user-button' => 'button:contains(\"Ajouter un utilisateur\")',
            '@search-input' => 'input[placeholder=\"Rechercher...\"]',
            '@users-table' => 'table',
            '@name-input' => 'input[name=\"name\"]',
            '@email-input' => 'input[name=\"email\"]',
            '@submit-button' => 'button[type=\"submit\"]',
        ];
    }

    public function createUser(Browser $browser, string $name, string $email, string $password)
    {
        $browser->click('@add-user-button')
                ->waitFor('@name-input')
                ->type('@name-input', $name)
                ->type('@email-input', $email)
                ->type('@password-input', $password)
                ->click('@submit-button');
    }

    public function searchUser(Browser $browser, string $searchTerm)
    {
        $browser->type('@search-input', $searchTerm)
                ->pause(500);
    }
}

Tests E2E complets

tests/Browser/UserCrudTest.php
<?php

use Laravel\\Dusk\\Browser;
use Tests\\Browser\\Pages\\UserPage;
use App\\Models\\User;

uses(Tests\\DuskTestCase::class);

it('can display users table', function () {
    User::factory()->count(3)->create();

    $this->browse(function (Browser $browser) {
        $browser->visit(new UserPage)
                ->assertSee('Utilisateurs')
                ->assertPresent('@users-table')
                ->assertSeeIn('@users-table', 'Nom')
                ->assertSeeIn('@users-table', 'Email');
    });
});

it('can create new user via form', function () {
    $this->browse(function (Browser $browser) {
        $browser->visit(new UserPage)
                ->createUser('Nouvel Utilisateur', 'nouveau@test.com', 'password123')
                ->waitForText('Nouvel Utilisateur')
                ->assertSeeIn('@users-table', 'Nouvel Utilisateur');
    });
});

it('can search users', function () {
    User::factory()->create(['name' => 'John Doe']);
    User::factory()->create(['name' => 'Jane Smith']);

    $this->browse(function (Browser $browser) {
        $browser->visit(new UserPage)
                ->searchUser('John')
                ->waitFor('@users-table')
                ->assertSeeIn('@users-table', 'John Doe')
                ->assertDontSee('Jane Smith');
    });
});

it('can handle large datasets with pagination', function () {
    User::factory()->count(50)->create();

    $this->browse(function (Browser $browser) {
        $browser->visit(new UserPage)
                ->waitFor('@users-table')
                ->assertSee('50 résultats')
                ->whenAvailable('[data-testid=\"pagination\"]', function ($pagination) {
                    $pagination->click('[data-testid=\"next-page\"]');
                });
    });
});

7. Tests Frontend avec React Testing Library

resources/js/__tests__/DataTable.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import DataTable from '../components/DataTable';

const mockData = [
  { id: 1, name: 'John Doe', email: 'john@example.com', status: 'active' },
  { id: 2, name: 'Jane Smith', email: 'jane@example.com', status: 'inactive' },
];

const mockColumns = [
  { key: 'name', label: 'Nom' },
  { key: 'email', label: 'Email' },
  { 
    key: 'status', 
    label: 'Statut',
    render: (value: string) => <span data-testid=\"status\">{value}</span>
  },
];

describe('DataTable', () => {
  it('renders data correctly', () => {
    render(<DataTable data={mockData} columns={mockColumns} />);

    expect(screen.getByText('John Doe')).toBeInTheDocument();
    expect(screen.getByText('jane@example.com')).toBeInTheDocument();
    expect(screen.getByText('2 résultats')).toBeInTheDocument();
  });

  it('shows loading skeleton when loading', () => {
    render(<DataTable data={[]} columns={mockColumns} isLoading={true} />);

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

  it('handles search functionality', () => {
    const mockOnSearch = vi.fn();
    render(
      <DataTable 
        data={mockData} 
        columns={mockColumns} 
        onSearch={mockOnSearch}
      />
    );

    const searchInput = screen.getByPlaceholderText('Rechercher...');
    fireEvent.change(searchInput, { target: { value: 'John' } });

    expect(mockOnSearch).toHaveBeenCalledWith('John');
  });
});

8. Scripts package.json

package.json (scripts)
{
  \"scripts\": {
    \"dev\": \"vite\",
    \"build\": \"tsc && vite build\",

    \"test\": \"vitest\",
    \"test:ui\": \"vitest --ui\",
    \"test:coverage\": \"vitest --coverage\",
    \"test:unit\": \"vitest\",
    \"test:integration\": \"vitest --config vitest.integration.config.ts\",

    \"test:frontend\": \"npm run test:unit\",
    \"test:backend\": \"./vendor/bin/pest\",
    \"test:dusk\": \"php artisan dusk\",
    \"test:e2e\": \"npm run test:dusk\",

    \"test:all\": \"npm run test:backend && npm run test:frontend && npm run test:integration\",
    \"test:full\": \"npm run test:all && npm run test:dusk\"
  }
}

9. Commandes de test

!!! tip \"Lancer les tests\"

# Tests unitaires
npm run test:unit              # React components
./vendor/bin/pest tests/Unit   # Laravel models

# Tests d'intégration
npm run test:integration       # Frontend-Backend
./vendor/bin/pest tests/Feature # Laravel API + DB

# Tests E2E
php artisan dusk              # Tous les tests Dusk
php artisan dusk tests/Browser/UserCrudTest.php # Spécifique

# Tous les tests
npm run test:full

10. Optimisations pour gros volumes

Table performante avec React Table

npm install @tanstack/react-table
npx shadcn@latest add pagination

Virtualisation pour grandes listes

npm install react-window react-window-infinite-loader
npm install -D @types/react-window

Avantages de cette stack

Design System cohérent

  • shadcn/ui components prêts à l'emploi
  • Accessibilité intégrée par défaut
  • Customisation facile via CSS variables

Tests robustes

  • Unitaires : Pest (PHP) + Vitest (JS)
  • Intégration : Feature tests + API réelles
  • E2E : Laravel Dusk avec Page Objects

Développement efficace

  • Hot reload frontend/backend
  • Type safety complet avec TypeScript
  • Configuration minimale

Performance

  • DataTable avec pagination/tri/recherche
  • React Query pour cache intelligent
  • Lazy loading et virtualisation

Prochaines étapes

  • [ ] Créer le template de base complet
  • [ ] Ajouter des exemples d'API REST complètes
  • [ ] Intégrer l'authentification Laravel Sanctum
  • [ ] Documentation des composants shadcn/ui customs
  • [ ] Pipeline CI/CD avec tous les tests

Status : 🟡 Template en développement
Dernière mise à jour : 31/07/2025
Repo : À créer