Files
planflow/tests/e2e/navigation.spec.ts
T
Claude 10a9c923e2 WP-09 : tableau de bord RH, et disparition des données de démonstration
`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
2026-08-08 12:40:19 +00:00

68 lines
2.8 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';
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();
});