`src/lib/demo` n'existe plus. Aucun écran de PlanFlow ne lit désormais autre chose que la base. Un indicateur doit être explicable Chaque tuile est un lien vers ses lignes sources : profils incomplets, fins de période d'essai, titres de séjour, entrées, sorties, avenants, journal des absences. Un chiffre qu'on ne peut pas ouvrir ne se corrige pas — il se conteste. Et chaque ligne mène à la fiche du salarié. Les manques sont **nommés**, pas comptés : « 6 profils incomplets » n'aide personne à agir, « il manque l'IBAN de trois salariés » se règle en un message. Le NIR et l'IBAN sont contrôlés par la présence de leur colonne chiffrée, jamais déchiffrés — savoir qu'une valeur existe n'exige pas de la lire. Des chiffres qui refusent de mentir - La rotation moyenne entrées et sorties : compter seulement les départs sous-estime la rotation d'une équipe qui recrute autant qu'elle perd. - Sur un effectif nul, elle rend `—` et non « 0 % ». Zéro pour cent de rotation sur un établissement vide est une affirmation fausse, pas une absence de mouvement. - L'absentéisme se rapporte aux jours **théoriquement travaillés**, pas aux jours calendaires : rapporter à 30 jours ferait passer un problème réel pour du bruit. - Un taux horaire absent vaut zéro dans le coût, jamais une estimation : afficher un coût inventé serait pire qu'un coût partiel. Les échéances remontent avant de tomber Périodes d'essai à 45 jours, titres de séjour à 90. Les échéances **dépassées** sont conservées et placées en tête : une période d'essai qu'on a laissé filer est plus urgente qu'une échéance à venir, et la masquer parce qu'elle est passée est précisément ce qui la rend coûteuse. Périmètre et confidentialité Le filtrage par établissement s'applique **avant** l'agrégation : les mouvements d'un autre établissement ne transparaissent pas, même fondus dans un total. Le journal des absences ne porte jamais le motif médical — un tableau de bord n'en a aucun besoin. Un salarié, qui n'a pas accès à l'annuaire, reçoit un accueil adapté plutôt qu'une erreur d'autorisation ou un tableau de bord vide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
68 lines
2.8 KiB
TypeScript
68 lines
2.8 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
||
|
||
test('les six écrans se chargent et affichent leur contenu', async ({ page }) => {
|
||
await page.goto('/');
|
||
await expect(page.getByRole('heading', { name: 'Aperçu RH' })).toBeVisible();
|
||
|
||
await page.getByRole('link', { name: 'Plannings' }).click();
|
||
await expect(
|
||
page.getByRole('heading', { name: /Planning · semaine \d+/ }),
|
||
).toBeVisible();
|
||
// La grille doit porter des créneaux venus de la base, pas seulement son
|
||
// ossature : le seed plante deux semaines sur trois équipes.
|
||
await expect(page.getByRole('heading', { name: 'Vente' })).toBeVisible();
|
||
await expect(page.getByText('09:00–17:00').first()).toBeVisible();
|
||
|
||
await page.getByRole('link', { name: 'Vue jour' }).click();
|
||
await expect(
|
||
page.getByRole('heading', { name: /^Planning · / }),
|
||
).toBeVisible();
|
||
|
||
await page.getByRole('link', { name: 'Équipe', exact: true }).click();
|
||
await expect(page.getByRole('heading', { name: 'Équipe' })).toBeVisible();
|
||
|
||
// La fiche est atteinte depuis l'annuaire réel, plus depuis un lien codé
|
||
// en dur : l'identifiant est celui de la base.
|
||
await page.getByRole('link', { name: /Camille Ferrand/ }).first().click();
|
||
await expect(
|
||
page.getByRole('heading', { name: /Camille Ferrand/ }),
|
||
).toBeVisible();
|
||
|
||
await page.getByRole('link', { name: 'Congés' }).click();
|
||
await expect(
|
||
page.getByRole('heading', { name: 'Calendrier des absences' }),
|
||
).toBeVisible();
|
||
});
|
||
|
||
test('le thème bascule et survit à un rechargement', async ({ page }) => {
|
||
await page.goto('/');
|
||
const html = page.locator('html');
|
||
await expect(html).toHaveAttribute('data-theme', 'light');
|
||
|
||
await page.getByRole('button', { name: /^Thème/ }).click();
|
||
await expect(html).toHaveAttribute('data-theme', 'dark');
|
||
|
||
// Le script inline doit reposer le thème avant le premier rendu : sans lui,
|
||
// la page reviendrait en clair puis basculerait — un flash blanc.
|
||
await page.reload();
|
||
await expect(html).toHaveAttribute('data-theme', 'dark');
|
||
});
|
||
|
||
test('une entrée de navigation non construite mène à un écran explicite', async ({
|
||
page,
|
||
}) => {
|
||
// La barre latérale n'affiche que la section courante : on l'ouvre d'abord.
|
||
await page.goto('/conges');
|
||
// « Politiques de congés » fait partie du périmètre visé mais n'est pas
|
||
// encore construit : l'entrée reste, et mène à un écran qui le dit.
|
||
await page.getByRole('link', { name: 'Politiques de congés' }).click();
|
||
await expect(
|
||
page.getByRole('heading', { name: 'Politiques de congés' }),
|
||
).toBeVisible();
|
||
// `exact` évite de heurter l'annonceur de route de Next, qui répète le titre
|
||
// du document — « Écran à venir · PlanFlow ».
|
||
await expect(
|
||
page.getByText('Écran à venir', { exact: true }),
|
||
).toBeVisible();
|
||
});
|