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
Virtualisation pour grandes listes
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