mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
Arbitrage du référent sur la revue pixel : tout corriger, activer la recherche. - Stock : filtre fournisseur, sous-seuil en tête, « Entrée de stock » depuis la liste ; fiche pièce : fournisseur → lien Tiers. - Statistiques : période 3/6/12 mois (paramètre months au contrat). - Tiers : rattachements syndic→site (migration r3_recette_fixes, Location.partnerId gardé CLIENT), éditable sur la fiche site, seedé. - Bibliothèque : filtre « Rattaché à » + glisser-déposer (modale préremplie, rattachement toujours requis). - Recherche globale : GET /search (73 opérations) — familles OT/ ascenseurs/sites filtrées par la matrice, « voir autre » respecté ; topbar ⌘K, debounce, résultats groupés, navigation clavier. 74 tests API (8 nouveaux sur le scoping de la recherche), 14/14 Playwright dont un parcours « recette corrigée », 18/18 contrôles en navigateur réel, zéro erreur console. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
210 lines
11 KiB
TypeScript
210 lines
11 KiB
TypeScript
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();
|
||
});
|