Aller au contenu principal

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.ts

Conventions

ConventionDétail
EmplacementColocalisé : src/lib/foo/__tests__/foo.test.ts ou src/lib/foo/foo.test.ts
Routerssrc/server/api/routers/__tests__/
Sécuritésrc/__tests__/security/
DBMocker Prisma — jamais de vraie DB en tests unitaires
Globalsdescribe, 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

ProjetDescriptionAuth
setupLogin global, sauvegarde auth state
publicPages publiques (smoke, auth, portal)Non
authenticatedDashboard, facturation, bankingOui (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:security

Vérifie que les procédures tRPC utilisent les bonnes guards :

  • staffProcedure minimum pour les routes internes
  • requirePermission() pour les actions sensibles
  • adminProcedure pour les paramètres critiques

Audit complet

pnpm test:security:full

Tests é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ègleDétail
Corriger le codeJAMAIS modifier les tests pour les faire passer
Max tentatives5 par test avant de marquer "bloqué"
LoggingLogger les corrections dans test-reports/corrections.log
Échecs pré-existantsNoter, 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 devis
  • crm-pipeline.test.ts — Pipeline CRM
  • security-guards.test.ts — Guards de sécurité
  • onboarding.test.ts — Flow d'onboarding
  • stock.test.ts — Gestion des stocks
  • time-logs.test.ts — Logs de temps
  • leave-workflow.test.ts — Workflow congés
  • fec-export.test.ts — Export FEC comptable
  • pagination-caps.test.ts — Limites de pagination
  • Et 11 autres...