@ai-qa/workflow 2.0.8 → 2.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/install.js ADDED
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+
7
+ const TEMPLATE_DIR = __dirname;
8
+ const YES = process.argv.includes('--yes') || process.argv.includes('-y');
9
+ const IS_UPDATE = process.argv.includes('--update') || process.argv.includes('-u');
10
+ const IS_SELF = process.argv.includes('--self') || process.argv.includes('-s');
11
+
12
+ // Files that belong to the user — NEVER overwritten
13
+ const USER_FILES = new Set([
14
+ '.qa-workflow.json',
15
+ 'opencode.json',
16
+ 'docs/application-context.md',
17
+ ]);
18
+
19
+ // Directories that are pure user data — NEVER touched
20
+ const USER_DIRS = new Set([
21
+ 'user-story',
22
+ 'specs',
23
+ 'tests',
24
+ 'test-results',
25
+ 'agents',
26
+
27
+
28
+
29
+
30
+ ]);
31
+
32
+ // Template files to copy/update
33
+ const QA_ITEMS = [
34
+ { src: 'ai-qa-workflow.js', dest: 'ai-qa-workflow.js' },
35
+ { src: 'scripts', dest: 'scripts', dir: true },
36
+ { src: 'opencode.json', dest: 'opencode.json' },
37
+ { src: '.qa-workflow.json', dest: '.qa-workflow.json' },
38
+ { src: 'prompts', dest: 'prompts', dir: true },
39
+ { src: '.github/agents', dest: '.github/agents', dir: true },
40
+ { src: '.opencode', dest: '.opencode', dir: true },
41
+ { src: 'README.md', dest: 'README.md' },
42
+ { src: 'PROJECT_GUIDE.md', dest: 'PROJECT_GUIDE.md' },
43
+ { src: '.cursorrules', dest: '.cursorrules' },
44
+ { src: '.geminirules', dest: '.geminirules' },
45
+ { src: '.github/copilot-instructions.md', dest: '.github/copilot-instructions.md' },
46
+ { src: 'router.md', dest: 'agents/router.md' },
47
+ { src: 'user-story', dest: 'user-story', dir: true },
48
+ { src: 'specs', dest: 'specs', dir: true },
49
+ { src: 'tests', dest: 'tests', dir: true },
50
+
51
+ ];
52
+
53
+ // Files to copy even during update (excludes user config files)
54
+ const UPDATE_ITEMS = QA_ITEMS.filter(item => {
55
+ if (item.dir) return !USER_DIRS.has(item.dest);
56
+ return !USER_FILES.has(item.dest);
57
+ });
58
+
59
+ const DIRS_TO_CREATE = ['user-story', 'specs', 'tests', 'test-results'];
60
+
61
+ const NPM_SCRIPTS = {
62
+ 'qa': 'node ai-qa-workflow.js',
63
+ 'qa:init': 'node ai-qa-workflow.js init',
64
+ 'qa:plan': 'node ai-qa-workflow.js plan',
65
+ 'qa:generate': 'node ai-qa-workflow.js generate',
66
+ 'qa:execute': 'node ai-qa-workflow.js execute',
67
+ 'qa:heal': 'node ai-qa-workflow.js heal',
68
+ 'qa:report': 'node ai-qa-workflow.js report',
69
+ 'qa:report:allure': 'node ai-qa-workflow.js report:allure',
70
+ 'qa:run': 'node ai-qa-workflow.js run',
71
+ 'qa:status': 'node ai-qa-workflow.js status',
72
+ 'qa:list': 'node ai-qa-workflow.js list',
73
+ 'dashboard': 'cd qa-dashboard && npm start',
74
+ 'dashboard:dev': 'cd qa-dashboard && npx nodemon app.js',
75
+ 'dashboard:stop': 'npx kill-port 4000',
76
+ };
77
+
78
+ const BANNER = `
79
+ ╔══════════════════════════════════════════╗
80
+ ║ AI QA Pipeline Installer v2.0 ║
81
+ ╚══════════════════════════════════════════╝
82
+ `;
83
+
84
+ function copyRecursive(src, dest, skipDirs) {
85
+ skipDirs = skipDirs || new Set();
86
+ const stat = fs.statSync(src);
87
+ if (stat.isDirectory()) {
88
+ const base = path.basename(src);
89
+ if (skipDirs.has(base)) return;
90
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
91
+ fs.readdirSync(src).forEach(item => copyRecursive(path.join(src, item), path.join(dest, item), skipDirs));
92
+ } else {
93
+ if (!fs.existsSync(dest) || fs.readFileSync(src).length !== fs.readFileSync(dest).length) {
94
+ fs.copyFileSync(src, dest);
95
+ return true;
96
+ }
97
+ }
98
+ return false;
99
+ }
100
+
101
+ function countFiles(dir) {
102
+ let count = 0;
103
+ if (!fs.existsSync(dir)) return 0;
104
+ fs.readdirSync(dir).forEach(item => {
105
+ try {
106
+ const full = path.join(dir, item);
107
+ if (fs.statSync(full).isDirectory()) count += countFiles(full);
108
+ else count++;
109
+ } catch (e) {}
110
+ });
111
+ return count;
112
+ }
113
+
114
+ function addNpmScripts(pkgPath, overwrite) {
115
+ if (!fs.existsSync(pkgPath)) return;
116
+ let pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
117
+ if (!pkg.scripts) pkg.scripts = {};
118
+ let changed = 0;
119
+ for (const [key, val] of Object.entries(NPM_SCRIPTS)) {
120
+ if (!pkg.scripts[key] || overwrite) {
121
+ pkg.scripts[key] = val;
122
+ changed++;
123
+ }
124
+ }
125
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
126
+ return changed;
127
+ }
128
+
129
+ function ask(query) {
130
+ const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
131
+ return new Promise(resolve => rl.question(query, a => { rl.close(); resolve(a.toLowerCase()); }));
132
+ }
133
+
134
+ async function install(targetPath, mode) {
135
+ const isUpdate = mode === 'update';
136
+
137
+ console.log(BANNER);
138
+ console.log(` Mode: ${isUpdate ? 'UPDATE (preserves config)' : 'FRESH INSTALL'}`);
139
+ console.log(` Target: ${targetPath}\n`);
140
+
141
+ // 1. Copy template files
142
+ const items = isUpdate ? UPDATE_ITEMS : QA_ITEMS;
143
+ console.log(` ── Step 1: QA Pipeline Files ──`);
144
+ for (const item of items) {
145
+ const srcPath = path.join(TEMPLATE_DIR, item.src);
146
+ const destPath = path.join(targetPath, item.dest);
147
+ if (!fs.existsSync(srcPath)) { console.log(` ⚠ ${item.src} not found, skipping`); continue; }
148
+ if (item.dir) {
149
+ if (!fs.existsSync(destPath)) fs.mkdirSync(destPath, { recursive: true });
150
+ const skipDirs = isUpdate ? new Set(['node_modules', 'data']) : new Set();
151
+ copyRecursive(srcPath, destPath, skipDirs);
152
+ const count = countFiles(srcPath);
153
+ console.log(` ✓ ${item.dest}/ (${count} files)`);
154
+ } else {
155
+ fs.copyFileSync(srcPath, destPath);
156
+ console.log(` ✓ ${item.dest}`);
157
+ }
158
+ }
159
+
160
+ // 2. Create directories (fresh install only)
161
+ if (!isUpdate) {
162
+ console.log(`\n ── Step 2: Project Directories ──`);
163
+ for (const dir of DIRS_TO_CREATE) {
164
+ const dirPath = path.join(targetPath, dir);
165
+ if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); console.log(` ✓ ${dir}/`); }
166
+ else console.log(` • ${dir}/ (exists)`);
167
+ }
168
+ }
169
+
170
+ // 3. Add/update npm scripts
171
+ console.log(`\n ── Step 3: NPM Scripts ──`);
172
+ const pkgPath = path.join(targetPath, 'package.json');
173
+ const changed = addNpmScripts(pkgPath, isUpdate);
174
+ if (changed > 0) console.log(` ✓ ${isUpdate ? 'Updated' : 'Added'} ${changed} npm scripts (qa:*, dashboard)`);
175
+ else console.log(` • Scripts already configured`);
176
+
177
+ // 4. Dashboard
178
+ const dashboardSrc = path.join(TEMPLATE_DIR, 'qa-dashboard');
179
+ const dashboardDest = path.join(targetPath, 'qa-dashboard');
180
+ if (fs.existsSync(dashboardSrc)) {
181
+ console.log(`\n ── Step 4: QA Dashboard ──`);
182
+ if (!fs.existsSync(dashboardDest)) {
183
+ fs.mkdirSync(dashboardDest, { recursive: true });
184
+ copyRecursive(dashboardSrc, dashboardDest);
185
+ console.log(` ✓ Dashboard copied to qa-dashboard/`);
186
+ console.log(` → Installing dashboard dependencies...`);
187
+ try { execSync('npm install', { cwd: dashboardDest, stdio: 'pipe', timeout: 120000 }); console.log(` ✓ Dashboard deps installed`); } catch (e) { console.log(` ⚠ cd qa-dashboard && npm install`); }
188
+ } else {
189
+ const skipDirs = new Set(['node_modules', 'data']);
190
+ copyRecursive(dashboardSrc, dashboardDest, skipDirs);
191
+ console.log(` ✓ Dashboard updated (preserved data/, node_modules/)`);
192
+ if (!isUpdate) {
193
+ console.log(` → Installing dashboard dependencies...`);
194
+ try { execSync('npm install', { cwd: dashboardDest, stdio: 'pipe', timeout: 120000 }); console.log(` ✓ Dashboard deps installed`); } catch (e) { console.log(` ⚠ cd qa-dashboard && npm install`); }
195
+ }
196
+ }
197
+ }
198
+
199
+ // 5. Register project in dashboard (fresh install only)
200
+ if (!isUpdate && fs.existsSync(dashboardDest)) {
201
+ console.log(`\n ── Step 5: Link Dashboard → Project ──`);
202
+ const dashDataDir = path.join(dashboardDest, 'data');
203
+ const dashProjectsFile = path.join(dashDataDir, 'projects.json');
204
+ if (fs.existsSync(dashDataDir)) {
205
+ let projects = [];
206
+ if (fs.existsSync(dashProjectsFile)) {
207
+ try { projects = JSON.parse(fs.readFileSync(dashProjectsFile, 'utf-8')); } catch(e) {}
208
+ }
209
+ const projectName = path.basename(targetPath);
210
+ const projectId = projectName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
211
+ if (!projects.find(p => p.id === projectId)) {
212
+ projects.push({ id: projectId, name: projectName, path: targetPath, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() });
213
+ fs.writeFileSync(dashProjectsFile, JSON.stringify(projects, null, 2));
214
+ console.log(` ✓ Project "${projectName}" registered in dashboard`);
215
+ }
216
+ }
217
+ }
218
+
219
+ // 6. Install Playwright (fresh only)
220
+ if (!isUpdate) {
221
+ console.log(`\n ── Step 6: Dependencies ──`);
222
+ if (!fs.existsSync(path.join(targetPath, 'node_modules', '@playwright'))) {
223
+ console.log(` → Installing @playwright/test...`);
224
+ try { execSync('npm install @playwright/test', { cwd: targetPath, stdio: 'pipe', timeout: 120000 }); console.log(` ✓ Playwright installed`); } catch (e) { console.log(` ⚠ npm install failed: npm install @playwright/test`); }
225
+ } else console.log(` • Playwright already installed`);
226
+ }
227
+
228
+ // Summary
229
+ const totalFiles = countFiles(path.join(targetPath, 'scripts'));
230
+ const dashboardCount = countFiles(path.join(targetPath, 'qa-dashboard'));
231
+
232
+ console.log(`\n ╔══════════════════════════════════════════╗`);
233
+ console.log(` ║ ✓ ${isUpdate ? 'UPDATE COMPLETE' : 'INSTALLATION COMPLETE'}${' '.repeat(isUpdate ? 13 : 7)}║`);
234
+ console.log(` ╚══════════════════════════════════════════╝\n`);
235
+
236
+ if (isUpdate) {
237
+ console.log(` Pipeline files updated. Your config and data are preserved.`);
238
+ console.log(` Restart the dashboard if it was running.\n`);
239
+ } else {
240
+ console.log(` Files: ~${totalFiles} scripts + ${dashboardCount} dashboard files`);
241
+ console.log(` Next:\n`);
242
+ console.log(` npm run qa:init Initialize pipeline (auto-detect config)`);
243
+ console.log(` npm run qa:run Run a user story through full pipeline`);
244
+ console.log(` npm run qa:status Check pipeline state`);
245
+ console.log(` npm run dashboard Start dashboard (port 4000)\n`);
246
+ }
247
+ }
248
+
249
+ // ---- CLI entry ----
250
+ const args = process.argv.slice(2);
251
+ const isHelp = !args.length || args.includes('--help') || args.includes('-h');
252
+
253
+ if (isHelp) {
254
+ console.log(`
255
+ AI QA Pipeline Installer
256
+
257
+ USAGE:
258
+
259
+ npx ai-qa-workflow init [--yes] Install into current directory
260
+ npx ai-qa-workflow update [--yes] Update pipeline files (preserves config)
261
+
262
+ Or from the template directory:
263
+
264
+ node install.js <target> [--yes] Fresh install into target project
265
+ node install.js <target> --update Update existing installation
266
+ node install.js . --yes Install into current directory
267
+
268
+ FLAGS:
269
+ --yes, -y Skip all prompts
270
+ --update,-u Update mode (preserves config, stories, results)
271
+
272
+ EXAMPLES:
273
+ npx ai-qa-workflow init --yes
274
+ node install.js ../my-project --yes
275
+ node install.js ../my-project --update
276
+ `);
277
+ process.exit(0);
278
+ }
279
+
280
+ async function main() {
281
+ let targetPath;
282
+ let mode = 'install';
283
+
284
+ // Parse flags and target
285
+ const nonFlagArgs = args.filter(a => !a.startsWith('-'));
286
+
287
+ if (IS_SELF || args.includes('init')) {
288
+ targetPath = process.cwd();
289
+ mode = 'install';
290
+ } else if (args.includes('update')) {
291
+ targetPath = process.cwd();
292
+ mode = 'update';
293
+ } else if (IS_UPDATE) {
294
+ targetPath = path.resolve(nonFlagArgs[0] || process.cwd());
295
+ mode = 'update';
296
+ } else if (nonFlagArgs.length > 0) {
297
+ targetPath = path.resolve(nonFlagArgs[0]);
298
+ mode = 'install';
299
+ } else {
300
+ targetPath = process.cwd();
301
+ mode = 'install';
302
+ }
303
+
304
+ if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
305
+ console.error(` ✗ Target not found: ${targetPath}`);
306
+ process.exit(1);
307
+ }
308
+
309
+ // Confirm unless --yes
310
+ if (!YES) {
311
+ const modeLabel = mode === 'update' ? 'UPDATE pipeline files' : 'INSTALL pipeline';
312
+ const answer = await ask(` ${modeLabel} in "${path.basename(targetPath)}"? (Y/n) `);
313
+ if (answer.startsWith('n')) { console.log(' Aborted.'); process.exit(0); }
314
+ }
315
+
316
+ await install(targetPath, mode);
317
+ }
318
+
319
+ main().catch(e => { console.error(e); process.exit(1); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-qa/workflow",
3
- "version": "2.0.8",
3
+ "version": "2.0.10",
4
4
  "description": "One-command AI QA Pipeline — User Story to Test Report. Auto-detects project config, generates Playwright tests, self-heals failures, dashboard UI.",
5
5
  "keywords": [
6
6
  "qa",
@@ -25,12 +25,14 @@
25
25
  "templates/",
26
26
  "ai-qa-workflow.js",
27
27
  "cli.js",
28
+ "install.js",
28
29
  "README.md",
29
30
  "PROJECT_GUIDE.md",
30
31
  ".cursorrules",
31
32
  ".geminirules",
32
33
  ".opencode/",
33
34
  ".qa-workflow.json",
35
+ "router.md",
34
36
  ".github/"
35
37
  ],
36
38
  "license": "MIT",
@@ -1,29 +1,29 @@
1
- # 🧠 AI QA Workflow - Agent Router (Core Engine)
2
-
3
- Tu es l'Intelligence Artificielle (Copilot, RooCode, Cursor,opencode, Antigravity, Gemini, etc.) assignée à ce projet d'automatisation QA.
4
- Ce projet contient plusieurs profils d'agents (dans `.github/agents/` ou `agents/`) et plusieurs instructions (dans `prompts/`).
5
-
6
- **RÈGLE STRICTE :** Tu DOIS analyser l'intention de l'utilisateur et lire automatiquement le fichier correspondant avant de générer ta réponse ou d'exécuter du code.
7
-
8
- ## 🧭 Routage Automatique du Contexte
9
-
10
- ### 1. Planification et Exploration (Intention : "Teste ce lien", "Explore cette app", "Fais un plan pour cette story")
11
- * **Action obligatoire :** Lis le fichier `.github/agents/playwright-test-planner.agent.md`
12
- * **Comportement attendu :** Utilise tes outils MCP Playwright pour naviguer sur l'application, faire des snapshots du DOM, identifier les éléments, et rédiger un plan de test clair.
13
-
14
- ### 2. Génération de Code Playwright (Intention : "Génère le script", "Code le test pour cette story")
15
- * **Action obligatoire 1 :** Lis le fichier `.github/agents/playwright-test-generator.agent.md`
16
- * **Action obligatoire 2 :** Lis le fichier `prompts/QAe2eprompt.md` pour respecter les conventions du framework.
17
- * **Comportement attendu :** Rédige les fichiers `.spec.ts` dans le dossier `tests/`. Tu as l'interdiction d'halluciner (deviner) des sélecteurs CSS. Utilise tes outils MCP pour trouver les locators sémantiques exacts (`getByRole`, `getByText`) sur la page réelle.
18
-
19
- ### 3. Auto-Guérison et Débogage (Intention : "Le test a planté", "Corrige cette erreur Playwright")
20
- * **Action obligatoire :** Lis le fichier `.github/agents/playwright-test-healer.agent.md`
21
- * **Comportement attendu :** Analyse la trace d'erreur, ouvre le navigateur avec MCP pour voir l'état réel de l'UI, et corrige le sélecteur défectueux dans le fichier source.
22
-
23
- ### 4. Requêtes QA Générales (Intention globale)
24
- * **Action obligatoire :** Lis le fichier `prompts/general_prompt.md`
25
-
26
- ## ⚠️ Directives Techniques
27
- * Ne demande jamais la permission de lire ces fichiers, fais-le silencieusement en arrière-plan.
28
- * Utilise toujours des sélecteurs Playwright robustes et sémantiques.
29
- * Si le serveur local n'est pas lancé, préviens l'utilisateur avant d'essayer de t'y connecter.
1
+ # 🧠 AI QA Workflow - Agent Router (Core Engine)
2
+
3
+ Tu es l'Intelligence Artificielle (Copilot, RooCode, Cursor,opencode, Antigravity, Gemini, etc.) assignée à ce projet d'automatisation QA.
4
+ Ce projet contient plusieurs profils d'agents (dans `.github/agents/` ou `agents/`) et plusieurs instructions (dans `prompts/`).
5
+
6
+ **RÈGLE STRICTE :** Tu DOIS analyser l'intention de l'utilisateur et lire automatiquement le fichier correspondant avant de générer ta réponse ou d'exécuter du code.
7
+
8
+ ## 🧭 Routage Automatique du Contexte
9
+
10
+ ### 1. Planification et Exploration (Intention : "Teste ce lien", "Explore cette app", "Fais un plan pour cette story")
11
+ * **Action obligatoire :** Lis le fichier `.github/agents/playwright-test-planner.agent.md`
12
+ * **Comportement attendu :** Utilise tes outils MCP Playwright pour naviguer sur l'application, faire des snapshots du DOM, identifier les éléments, et rédiger un plan de test clair.
13
+
14
+ ### 2. Génération de Code Playwright (Intention : "Génère le script", "Code le test pour cette story")
15
+ * **Action obligatoire 1 :** Lis le fichier `.github/agents/playwright-test-generator.agent.md`
16
+ * **Action obligatoire 2 :** Lis le fichier `prompts/QAe2eprompt.md` pour respecter les conventions du framework.
17
+ * **Comportement attendu :** Rédige les fichiers `.spec.ts` dans le dossier `tests/`. Tu as l'interdiction d'halluciner (deviner) des sélecteurs CSS. Utilise tes outils MCP pour trouver les locators sémantiques exacts (`getByRole`, `getByText`) sur la page réelle.
18
+
19
+ ### 3. Auto-Guérison et Débogage (Intention : "Le test a planté", "Corrige cette erreur Playwright")
20
+ * **Action obligatoire :** Lis le fichier `.github/agents/playwright-test-healer.agent.md`
21
+ * **Comportement attendu :** Analyse la trace d'erreur, ouvre le navigateur avec MCP pour voir l'état réel de l'UI, et corrige le sélecteur défectueux dans le fichier source.
22
+
23
+ ### 4. Requêtes QA Générales (Intention globale)
24
+ * **Action obligatoire :** Lis le fichier `prompts/general_prompt.md`
25
+
26
+ ## ⚠️ Directives Techniques
27
+ * Ne demande jamais la permission de lire ces fichiers, fais-le silencieusement en arrière-plan.
28
+ * Utilise toujours des sélecteurs Playwright robustes et sémantiques.
29
+ * Si le serveur local n'est pas lancé, préviens l'utilisateur avant d'essayer de t'y connecter.
@@ -1,30 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
- import { chromium } from '@playwright/test';
3
-
4
- test.describe('Soumission de Demande d\'Alimentation de Compte Professionnel', () => {
5
-
6
- test.beforeEach(async () => {
7
- // const browser = await chromium.launch({ headless: !process.env.HEADED });
8
- // const page = await browser.newPage();
9
- // global.page = page;
10
- });
11
-
12
- test.afterEach(async () => {
13
- // Cleanup if needed
14
- });
15
-
16
- test('Scenario 1', async () => {
17
- // TODO: Implement test steps from plan
18
- // Reference: specs/us-expense-01-test-plan.md (Scenario 3.1)
19
- //
20
- // Step 1: *Montant demandé* (valeur numérique positive)
21
- // Step 2: *Motif de la demande* (texte, minimum 15 caractères)
22
- // Step 3: *Projet associé* (sélection dans une liste déroulante active)
23
- // Step 4: Le montant demandé doit être compris entre 10 EUR et 5 000 EUR.
24
- // Step 5: Les messages d'erreur de validation doivent s'afficher en temps réel sous les champs incorrects.
25
- //
26
- // Expected: Workflow completes successfully
27
-
28
- });
29
-
30
- });
@@ -1,24 +0,0 @@
1
- # Soumission de Demande d'Alimentation de Compte Professionnel
2
-
3
- **Story ID**: US-EXPENSE-01
4
- **Title**: Demande d'Alimentation de Compte
5
- **Author**: QA Automation Team
6
-
7
- ## Description
8
- En tant que collaborateur d'entreprise, je souhaite soumettre une demande d'alimentation pour mon compte de dépenses professionnel afin de couvrir les frais liés à mes déplacements clients sans avancer de fonds personnels.
9
-
10
- ## Preconditions
11
- - L'utilisateur est authentifié sur le portail collaborateur.
12
- - Le compte de l'utilisateur est actif et rattaché à un centre de coût valide.
13
- - Le solde actuel du compte de dépenses est inférieur à 500 EUR.
14
-
15
- ## Acceptance Criteria
16
- 1. **Accès au formulaire** : Un bouton "Nouvelle Demande d'Alimentation" doit être visible depuis le tableau de bord principal.
17
- 2. **Saisie des informations** : Le formulaire doit comporter les champs obligatoires suivants :
18
- - *Montant demandé* (valeur numérique positive)
19
- - *Motif de la demande* (texte, minimum 15 caractères)
20
- - *Projet associé* (sélection dans une liste déroulante active)
21
- 3. **Contrôles de validation de surface** :
22
- - Le montant demandé doit être compris entre 10 EUR et 5 000 EUR.
23
- - Les messages d'erreur de validation doivent s'afficher en temps réel sous les champs incorrects.
24
- 4. **Soumission et Historique** : Après soumission réussie, un spinner de chargement doit s'afficher temporairement, suivi d'un toast de succès. L'utilisateur doit être redirigé vers l'historique où la demande doit apparaître avec le statut "En attente de validation".