Files
siop2/apps/web/e2e/parcours-r3.spec.ts
pr-daaif c5382a5538 feat(r3.3): écrans web de la gestion — stock, achats, coûts OT, documents, statistiques
- Stock : alertes sous seuil (strie ambre), « Préparer le BC » sur la ligne ;
  fiche pièce où les mouvements SONT le stock (entrée / ajustement motivé)
- Bons de commande : liste + panneau (envoi, annulation, réception → entrées
  de stock à PU figé) ; création préremplie depuis l'alerte de seuil
- Fiche OT : carte « Pièces & main-d'œuvre » réelle (consommation à prix figé
  annoncé avant le clic, temps au taux figé) + carte Documents (upload/download) ;
  même carte Documents sur la fiche ascenseur
- Statistiques : 4 KPI, pannes par organe (bilans codés), coûts par mois,
  top équipements ; le tableau de bord allume « Pannes par organe »
- Tiers, bibliothèque (upload rattaché, filtre par type), taux horaire
  éditable dans Personnes ; nav Ressources/Pilotage activée par la matrice
- Recette R3 Playwright (2 parcours autonomes) ; cleanup-e2e étendu aux
  entités R3 ; le formulaire BC attend la fin du refetch (stock périmé en cache)

13/13 e2e verts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 22:16:56 +01:00

160 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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').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();
});