Tests
Vitest pour les tests unitaires, Playwright pour les E2E.
Tests unitaires (Vitest)
Configuration
Fichier : vitest.config.ts
{
test: {
environment: "jsdom",
globals: true, // describe, it, expect, vi sans import
setupFiles: ["./src/test/setup.ts"],
coverage: {
provider: "v8",
include: ["src/lib/**", "src/modules/**"],
},
},
resolve: {
alias: {
"~": "./src",
"generated": "./generated",
},
},
}Commandes
pnpm test # Lancer tous les tests une fois
pnpm test:watch # Mode watch (relance sur modification)
pnpm test:security # Tests de sécurité (guards, permissions)
pnpm test:security:full # Audit complet (IDOR, crypto, CVE, middleware)Cibler un fichier spécifique
npx vitest run src/server/api/routers/__tests__/facturation-calculs.test.tsConventions
| Convention | Détail |
|---|---|
| Emplacement | Colocalisé : src/lib/foo/__tests__/foo.test.ts ou src/lib/foo/foo.test.ts |
| Routers | src/server/api/routers/__tests__/ |
| Sécurité | src/__tests__/security/ |
| DB | Mocker Prisma — jamais de vraie DB en tests unitaires |
| Globals | describe, it, expect, vi disponibles sans import |
Setup
Fichier : src/test/setup.ts
Polyfills pour l'environnement jsdom :
window.matchMedia(Radix UI)ResizeObserver- Testing Library (
@testing-library/jest-dom)
Écrire un nouveau test unitaire
// src/lib/mon-module/__tests__/mon-module.test.ts
import { maFonction } from "../mon-module";
describe("maFonction", () => {
it("devrait retourner le résultat attendu", () => {
const result = maFonction({ input: "test" });
expect(result).toBe("expected");
});
it("devrait lever une erreur si input invalide", () => {
expect(() => maFonction({ input: "" })).toThrow();
});
});Mocker Prisma
import { vi } from "vitest";
// Mock du client Prisma
vi.mock("~/server/db", () => ({
db: {
invoice: {
findMany: vi.fn().mockResolvedValue([]),
findFirst: vi.fn().mockResolvedValue(null),
create: vi.fn().mockResolvedValue({ id: "test-id" }),
update: vi.fn().mockResolvedValue({}),
},
},
}));Mocker le contexte tRPC
const mockCtx = {
session: {
user: {
id: "user-1",
role: "ADMIN",
organizationId: "org-1",
},
},
orgDb: {
invoice: {
findMany: vi.fn().mockResolvedValue([]),
findFirst: vi.fn().mockResolvedValue(mockInvoice),
},
},
db: {
invoice: {
update: vi.fn().mockResolvedValue({}),
},
},
};Tests E2E (Playwright)
Configuration
Fichier : playwright.config.ts
{
testDir: "e2e",
timeout: 60000,
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{ name: "setup", testMatch: "**/global-setup.ts" },
{ name: "public", testMatch: "**/public/**" },
{
name: "authenticated",
testMatch: "**/authenticated/**",
dependencies: ["setup"],
use: { storageState: "e2e/.auth/user.json" },
},
],
}Commandes
pnpm test:e2e # Lancer tous les tests E2E
pnpm test:e2e:ui # Mode UI (interface graphique)
pnpm test:e2e:headed # Mode headed (navigateur visible)Projects
| Projet | Description | Auth |
|---|---|---|
setup | Login global, sauvegarde auth state | — |
public | Pages publiques (smoke, auth, portal) | Non |
authenticated | Dashboard, facturation, banking | Oui (via setup) |
Auth state
Le projet setup se connecte une fois et sauvegarde l'état d'authentification dans e2e/.auth/user.json (gitignored). Les tests authenticated réutilisent cet état.
Écrire un test E2E
// e2e/authenticated/facturation.spec.ts
import { test, expect } from "@playwright/test";
test("devrait afficher la liste des factures", async ({ page }) => {
await page.goto("/dashboard/facturation");
await expect(page.getByRole("heading", { name: "Factures" })).toBeVisible();
});
test("devrait créer une facture", async ({ page }) => {
await page.goto("/dashboard/facturation");
await page.getByRole("button", { name: "Nouvelle facture" }).click();
// ... remplir le formulaire
await expect(page.getByText("Facture créée")).toBeVisible();
});Test de page publique (sans auth)
// e2e/public/landing.spec.ts
import { test, expect } from "@playwright/test";
test("la landing page se charge", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle(/HeartCo/);
});Tests de sécurité
Guards de permissions
pnpm test:securityVérifie que les procédures tRPC utilisent les bonnes guards :
staffProcedureminimum pour les routes internesrequirePermission()pour les actions sensiblesadminProcedurepour les paramètres critiques
Audit complet
pnpm test:security:fullTests étendus :
- IDOR : Tentative d'accès cross-organisation
- Crypto : Vérification HMAC, encryption tokens
- CVE : Protections connues (middleware bypass, etc.)
- Middleware : CSP, headers, rate limiting
Conventions de correction
| Règle | Détail |
|---|---|
| Corriger le code | JAMAIS modifier les tests pour les faire passer |
| Max tentatives | 5 par test avant de marquer "bloqué" |
| Logging | Logger les corrections dans test-reports/corrections.log |
| Échecs pré-existants | Noter, ne pas corriger (sauf si lié à la tâche) |
Tests existants
21 fichiers de tests dans src/server/api/routers/__tests__/ :
facturation-calculs.test.ts— Calculs de facturation (TTC, TVA)devis-lifecycle.test.ts— Cycle de vie des deviscrm-pipeline.test.ts— Pipeline CRMsecurity-guards.test.ts— Guards de sécuritéonboarding.test.ts— Flow d'onboardingstock.test.ts— Gestion des stockstime-logs.test.ts— Logs de tempsleave-workflow.test.ts— Workflow congésfec-export.test.ts— Export FEC comptablepagination-caps.test.ts— Limites de pagination- Et 11 autres...