mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
test(r0.12): ESLint monorepo + parcours démo Playwright — R0.12 clos
- ESLint 10 flat config racine (typescript-eslint, react-hooks) ; règle ADR-001 codée : no-restricted-imports interdit « minio » partout sauf minio-storage.service.ts (violation vérifiée) ; jobs lint par paquet - Playwright apps/web/e2e : redirection sans jeton, connexion démo 1 clic, coquille (rail actif, chip DÉMO), bascule de rôle chronométrée < 3 s (ADR-002), /design, déconnexion avec purge ; webServer api dist + vite, seed idempotent en globalSetup, PW_CHANNEL=chrome en local - CI : jobs lint + e2e (services PostgreSQL/Redis, artefact playwright-report sur échec) ; vitest restreint à src/ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
11
apps/web/e2e/global-setup.ts
Normal file
11
apps/web/e2e/global-setup.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
/** Seed idempotent avant le parcours : mêmes comptes en local et en CI. */
|
||||
export default function globalSetup() {
|
||||
execSync('pnpm --filter @siop/api seed', {
|
||||
cwd: fileURLToPath(new URL('../../..', import.meta.url)),
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
});
|
||||
}
|
||||
65
apps/web/e2e/parcours-demo.spec.ts
Normal file
65
apps/web/e2e/parcours-demo.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* LE parcours de recette R0 : connexion démo 1 clic, coquille fidèle,
|
||||
* bascule de rôle < 3 s (critère d'acceptation ADR-002), /design accessible.
|
||||
*/
|
||||
|
||||
test('sans jeton, l’application renvoie sur la connexion (fermée par défaut)', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveURL(/\/connexion$/);
|
||||
});
|
||||
|
||||
test('connexion démo → coquille → bascule de rôle < 3 s → /design', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/connexion');
|
||||
|
||||
// Mode démonstration : 7 comptes seedés, chip d'avertissement
|
||||
await expect(page.locator('.sep')).toHaveText('Mode démonstration');
|
||||
await expect(page.locator('.demo-liste button')).toHaveCount(7);
|
||||
await expect(page.locator('.chip-demo')).toContainText('DEMO_MODE=true');
|
||||
|
||||
// Connexion 1 clic (Dispatcher)
|
||||
await page.getByRole('button', { name: /Salma Idrissi/ }).click();
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Tableau de bord' }),
|
||||
).toBeVisible();
|
||||
await expect(page.locator('.topbar')).toContainText('Salma Idrissi');
|
||||
await expect(page.locator('.topbar .chip-demo')).toHaveText('DÉMO');
|
||||
// Rail safran : l'entrée active de la sidebar
|
||||
await expect(page.locator('.nav a.actif')).toHaveText(/Tableau de bord/);
|
||||
|
||||
// Bascule de rôle via la topbar — critère ADR-002 : < 3 s
|
||||
const debut = Date.now();
|
||||
await page.locator('.compte').click();
|
||||
await page.getByRole('menuitem', { name: /Ahmed Benali/ }).click();
|
||||
await expect(page.locator('.topbar')).toContainText('Ahmed Benali');
|
||||
await expect(page.locator('.topbar')).toContainText('Technicien');
|
||||
expect(Date.now() - debut).toBeLessThan(3000);
|
||||
|
||||
// Référence vivante du design
|
||||
await page.goto('/design');
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /Design — référence vivante/ }),
|
||||
).toBeVisible();
|
||||
await expect(page.locator('.puce-c').first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('la déconnexion ramène à la connexion et ferme la session', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/connexion');
|
||||
await page.getByRole('button', { name: /Yasmine Alaoui/ }).click();
|
||||
await expect(page.locator('.topbar')).toContainText('Yasmine Alaoui');
|
||||
|
||||
await page.locator('.compte').click();
|
||||
await page.getByRole('menuitem', { name: 'Se déconnecter' }).click();
|
||||
await expect(page).toHaveURL(/\/connexion$/);
|
||||
|
||||
// Retour direct sur / : toujours redirigé (le jeton est bien purgé)
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveURL(/\/connexion$/);
|
||||
});
|
||||
@@ -9,7 +9,9 @@
|
||||
"build": "tsc -p tsconfig.json --noEmit && vite build",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"test": "vitest run",
|
||||
"generate:client": "openapi-typescript ../../docs/openapi.json -o src/api/schema.d.ts"
|
||||
"generate:client": "openapi-typescript ../../docs/openapi.json -o src/api/schema.d.ts",
|
||||
"e2e": "playwright test",
|
||||
"lint": "eslint src e2e"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/manrope": "^5.1.0",
|
||||
@@ -26,6 +28,7 @@
|
||||
"tailwind-merge": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@tailwindcss/vite": "^4.1.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
|
||||
46
apps/web/playwright.config.ts
Normal file
46
apps/web/playwright.config.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Parcours e2e (R0.12) — démarre l'API construite (dist) et Vite, puis joue
|
||||
* le parcours démo. Localement : PW_CHANNEL=chrome évite le téléchargement
|
||||
* de Chromium (utilise le Chrome installé) ; en CI, Chromium Playwright.
|
||||
* Prérequis : PostgreSQL/Redis joignables, `pnpm --filter @siop/api build`.
|
||||
*/
|
||||
const API_ENV = {
|
||||
NODE_ENV: 'test',
|
||||
PORT: '3000',
|
||||
DEMO_MODE: 'true',
|
||||
JWT_SECRET: process.env.JWT_SECRET ?? 'e2e-secret-0123456789abcdef',
|
||||
DATABASE_URL:
|
||||
process.env.DATABASE_URL ?? 'postgresql://siop:siop@localhost:5432/siop',
|
||||
REDIS_URL: process.env.REDIS_URL ?? 'redis://localhost:6379',
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
globalSetup: './e2e/global-setup.ts',
|
||||
timeout: 30_000,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
use: {
|
||||
baseURL: 'http://localhost:5173',
|
||||
trace: 'retain-on-failure',
|
||||
...(process.env.PW_CHANNEL ? { channel: process.env.PW_CHANNEL } : {}),
|
||||
},
|
||||
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||
webServer: [
|
||||
{
|
||||
command: 'node ../api/dist/main.js',
|
||||
url: 'http://localhost:3000/health',
|
||||
env: API_ENV,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 30_000,
|
||||
},
|
||||
{
|
||||
command: 'pnpm dev',
|
||||
url: 'http://localhost:5173',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 60_000,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -14,5 +14,5 @@
|
||||
"baseUrl": ".",
|
||||
"paths": { "@/*": ["src/*"] }
|
||||
},
|
||||
"include": ["src", "vite.config.ts"]
|
||||
"include": ["src", "e2e", "vite.config.ts", "playwright.config.ts"]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// <reference types="vitest/config" />
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'node:path';
|
||||
@@ -5,6 +6,10 @@ import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
test: {
|
||||
// e2e/ appartient à Playwright — vitest ne joue que les tests unitaires
|
||||
include: ['src/**/*.test.{ts,tsx}'],
|
||||
},
|
||||
resolve: {
|
||||
alias: { '@': path.resolve(__dirname, 'src') },
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user