import { expect, test } from '@playwright/test'; /** * Recette R3 (plan de releases) : pièce sous le seuil → BC prérempli → * réception (le stock remonte, l'alerte s'éteint) ; coûts réels sur OT * (prix figé + taux figé) ; document rattaché téléversé puis téléchargé ; * statistiques lisibles par la Vue seule. * Chaque test crée SES données (marquées E2E, purgées au global-setup). */ const suffix = Date.now().toString(36); /** PNG 1×1 valide — assez pour tester upload/download réels via MinIO. */ const PNG_1PX = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64', ); async function connexionDemo(page: import('@playwright/test').Page, nom: RegExp) { await page.goto('/connexion'); await page.getByRole('button', { name: nom }).click(); await expect(page.locator('.topbar')).toBeVisible(); } async function changerDeCompte(page: import('@playwright/test').Page, nom: RegExp) { await page.locator('.compte').click(); await page.getByRole('menuitem', { name: nom }).click(); } test('recette R3 : tiers → pièce sous seuil → BC prérempli → réception → alerte éteinte', async ({ page, }) => { const fournisseur = `E2E Fournisseur ${suffix}`; const piece = `E2E Pièce ${suffix}`; // 1 · Nadia (gestionnaire) crée le fournisseur await connexionDemo(page, /Nadia Berrada/); await page.getByRole('link', { name: 'Tiers' }).click(); await page.getByRole('button', { name: '+ Nouveau tiers' }).click(); await page.getByLabel('Nom *').fill(fournisseur); await page.getByLabel('Type *').selectOption('SUPPLIER'); await page.getByRole('button', { name: 'Créer', exact: true }).click(); await expect(page.getByRole('cell', { name: fournisseur })).toBeVisible(); // 2 · Nouvelle pièce (seuil 5, prix initial 100) → fiche await page.getByRole('link', { name: 'Stock & achats' }).click(); await page.getByRole('button', { name: '+ Nouvelle pièce' }).click(); await page.getByLabel('Désignation *').fill(piece); await page.getByLabel("Seuil d'alerte").fill('5'); await page.getByLabel('Prix initial (MAD)').fill('100'); await page.getByLabel('Fournisseur', { exact: true }).selectOption({ label: fournisseur }); await page.getByRole('button', { name: 'Créer la pièce' }).click(); await expect(page.getByRole('heading', { name: piece })).toBeVisible(); // 3 · Entrée de 2 → stock 2 < seuil 5 : alerte await page.getByRole('button', { name: 'Entrée de stock' }).click(); await page.getByLabel(/Quantité/).fill('2'); await page.getByRole('button', { name: 'Tracer le mouvement' }).click(); await expect(page.locator('.entete-page .stock-bas')).toHaveText('Sous le seuil'); await expect(page.locator('dd', { hasText: 'seuil : 5' })).toContainText('2'); // 4 · « Préparer le BC » → formulaire prérempli (fournisseur, qté 8, PU 100) await page.getByRole('button', { name: 'Préparer le BC' }).click(); await expect(page.getByRole('heading', { name: 'Nouveau bon de commande' })).toBeVisible(); await expect(page.getByLabel('Fournisseur *')).toHaveValue(/.+/); await expect(page.getByLabel('Quantité de la ligne 1')).toHaveValue('8'); // seuil − stock + 5 await expect(page.getByLabel('Prix unitaire de la ligne 1')).toHaveValue('100'); await page.getByRole('button', { name: 'Créer le BC (brouillon)' }).click(); // 5 · Envoyer puis réceptionner — le panneau suit le BC const ligneBC = page.locator('tr', { hasText: fournisseur }).first(); await ligneBC.click(); await expect(page.locator('.panneau .st')).toHaveText('Brouillon'); await page.getByRole('button', { name: 'Envoyer au fournisseur' }).click(); await expect(page.locator('.panneau .st')).toHaveText('Envoyé'); await page.getByRole('button', { name: /Réceptionner/ }).click(); await expect(page.locator('.panneau .st')).toHaveText('Reçu'); // 6 · Le stock est remonté (2 + 8 = 10), l'alerte est éteinte await page.getByRole('link', { name: 'Stock & achats' }).click(); const lignePiece = page.locator('tr', { hasText: piece }); await expect(lignePiece.locator('td').nth(2)).toHaveText('10'); await expect(lignePiece.locator('.stock-bas')).toHaveCount(0); // 7 · La fiche pièce raconte tout : entrée manuelle + réception à PU figé await lignePiece.click(); await expect(page.locator('.mvt.entree')).toHaveCount(2); await expect(page.getByText('100 MAD/u')).toBeVisible(); }); test('recette R3 : coûts réels sur OT (prix + taux figés) + document + statistiques', async ({ page, }) => { const piece = `E2E PièceOT ${suffix}`; const titreOT = `E2E ${suffix} — coûts`; // 1 · Nadia crée une pièce avec du stock (10 à 100 MAD) await connexionDemo(page, /Nadia Berrada/); await page.getByRole('link', { name: 'Stock & achats' }).click(); await page.getByRole('button', { name: '+ Nouvelle pièce' }).click(); await page.getByLabel('Désignation *').fill(piece); await page.getByLabel('Prix initial (MAD)').fill('100'); await page.getByRole('button', { name: 'Créer la pièce' }).click(); await expect(page.getByRole('heading', { name: piece })).toBeVisible(); await page.getByRole('button', { name: 'Entrée de stock' }).click(); await page.getByLabel(/Quantité/).fill('10'); await page.getByRole('button', { name: 'Tracer le mouvement' }).click(); await expect(page.locator('dd', { hasText: 'seuil : 0' })).toContainText('10'); // 2 · Salma (dispatcher) crée l'OT et l'assigne à Ahmed await changerDeCompte(page, /Salma Idrissi/); await page.getByRole('link', { name: /Ordres de travail/ }).click(); await page.getByRole('button', { name: '+ Nouvel OT' }).click(); await page.getByLabel('Objet *').fill(titreOT); await page.getByLabel('Équipement *').selectOption({ index: 1 }); await page.getByLabel('Assigner à').selectOption({ label: 'Ahmed Benali' }); await page.getByRole('button', { name: "Créer l'OT (statut Ouvert)" }).click(); await expect(page.getByRole('heading', { name: titreOT })).toBeVisible(); // 3 · Ahmed (technicien) démarre, consomme 2 pièces, saisit 1 h 30 await changerDeCompte(page, /Ahmed Benali/); await page.getByRole('link', { name: /Ordres de travail/ }).click(); await page.getByRole('cell', { name: titreOT }).click(); await page.getByRole('button', { name: 'Démarrer' }).click(); await expect(page.locator('.entete-page .st')).toHaveText('En cours'); await page.getByRole('button', { name: '+ Consommer une pièce' }).click(); await page.getByLabel('Pièce *').selectOption({ label: `${piece} — stock : 10` }); await expect(page.getByText('Prix figé à la consommation')).toBeVisible(); await page.getByLabel('Quantité *').fill('2'); await page.getByRole('button', { name: /^Consommer/ }).click(); await expect(page.locator('.item-liste', { hasText: `${piece} × 2` })).toBeVisible(); await page.getByRole('button', { name: '+ Saisir du temps' }).click(); await page.getByLabel('Heures').fill('1'); await page.getByLabel('Minutes').fill('30'); await page.getByRole('button', { name: 'Enregistrer' }).click(); // 2 × 100 + 1,5 h × 120 MAD/h (taux seedé d'Ahmed) = 380 MAD await expect(page.getByText('1 h 30 × 120 MAD/h')).toBeVisible(); await expect(page.locator('.total .num')).toHaveText('380 MAD'); // 4 · Ahmed joint une photo — puis la retélécharge (aller-retour MinIO réel) await page.getByLabel('Choisir un fichier').setInputFiles({ name: `photo-e2e-${suffix}.png`, mimeType: 'image/png', buffer: PNG_1PX, }); await expect(page.getByText(`photo-e2e-${suffix}.png`)).toBeVisible(); const attenteTelechargement = page.waitForEvent('download'); await page.getByRole('button', { name: 'Télécharger' }).click(); expect((await attenteTelechargement).suggestedFilename()).toBe(`photo-e2e-${suffix}.png`); // 5 · Rachid (vue seule) lit les statistiques await changerDeCompte(page, /Rachid Bennis/); await page.getByRole('link', { name: 'Statistiques', exact: true }).click(); await expect(page.getByText('OT clôturés · 12 mois')).toBeVisible(); await expect(page.getByText('Pannes par organe · 12 mois')).toBeVisible(); await expect(page.getByText('Top équipements en coût · 12 mois')).toBeVisible(); }); test('recette R3 corrigée : recherche ⌘K, tri/filtre stock, période stats, rattachements, dépôt', async ({ page, }) => { await connexionDemo(page, /Yasmine Alaoui/); // Recherche globale : ⌘K → site (insensible à la casse), clic → fiche site await page.keyboard.press('ControlOrMeta+k'); await page.keyboard.type('atlas'); await page.locator('.resultat', { hasText: 'Tour Atlas' }).first().click(); await expect(page).toHaveURL(/\/sites\//); // le site porte son syndic (rattachement seedé, éditable par l'admin) await expect(page.getByLabel('Client / syndic du site')).toHaveValue(/.+/); // Recherche OT au clavier seul (Entrée sur le premier résultat) await page.keyboard.press('ControlOrMeta+k'); await page.keyboard.type('bruit'); await page.locator('.resultat', { hasText: 'Bruit anormal' }).first().waitFor(); await page.keyboard.press('Enter'); await expect(page).toHaveURL(/\/ot\//); // Stock : les pièces sous le seuil remontent en tête, filtre fournisseur, // « Entrée de stock » accessible depuis la liste (choix de la pièce) await page.getByRole('link', { name: 'Stock & achats' }).click(); await expect(page.locator('tbody tr').first().locator('.stock-bas')).toHaveText(/Sous le seuil/); await page.getByLabel('Filtrer par fournisseur').selectOption({ label: 'Lubmaroc' }); await expect(page.locator('tbody tr', { hasText: 'P-0042' })).toHaveCount(1); await page.getByLabel('Filtrer par fournisseur').selectOption({ index: 0 }); await page.getByRole('button', { name: 'Entrée de stock' }).click(); await expect(page.getByLabel('Pièce *')).toBeVisible(); await page.getByRole('button', { name: 'Annuler' }).click(); // Fiche pièce : le fournisseur est un lien vers les tiers await page.getByRole('cell', { name: 'P-0113' }).click(); await page.getByRole('link', { name: 'Ascentech Maroc' }).click(); await expect(page).toHaveURL(/\/tiers/); // Tiers : les syndics affichent leurs sites await expect(page.getByRole('cell', { name: 'Tour Atlas' })).toBeVisible(); await expect(page.getByRole('cell', { name: 'Résidence Al Manar' })).toBeVisible(); // Statistiques : la période est un vrai sélecteur await page.getByRole('link', { name: 'Statistiques', exact: true }).click(); await page.getByLabel('Période').selectOption('3'); await expect(page.getByText('OT clôturés · 3 mois')).toBeVisible(); // Bibliothèque : filtre « Rattaché à » + zone de glisser-déposer await page.getByRole('link', { name: 'Fichiers' }).click(); await expect(page.getByLabel('Filtrer par appareil')).toBeVisible(); await expect(page.locator('.zone-depot')).toBeVisible(); });