Le planning quitte le module de démonstration : la grille lit `WeeklySchedule` et `Shift`, et les quatre derniers fichiers de `src/lib/demo/` qui portaient des salariés fictifs disparaissent. Modèle - `TeamMember` : rattachement d'un salarié à une équipe, distinct de `MembershipScope` (qui dit ce qu'un manager a le droit de voir). Sans lui, un salarié sans créneau n'apparaîtrait pas dans la grille — c'est-à-dire l'état de départ de toute semaine en construction. - RLS sur `WeeklySchedule`, `Shift`, `Rest`, `DailyNote` et `TeamMember`. Temps - `src/domain/planning/week.ts` : repérage par couple année ISO + semaine ISO, jamais par date de début. Le lundi 29 décembre 2025 appartient à la semaine 1 de 2026 ; une clé fondée sur la date ferait apparaître deux semaines 1. - `zonedInstant` / `zonedMidnight` corrigent le décalage mesuré **à l'instant visé**. Le 29 mars 2026, minuit est en UTC+1 et 09 h en UTC+2 : ajouter neuf heures à minuit donnerait 10 h locales. - La semaine du retour à l'heure d'hiver dure 169 h, celle du passage à l'heure d'été 167 — vérifié par test. Écritures - Création, modification, suppression de créneau ; publication et dépublication **par équipe**, avec verrou optimiste sur `version` : deux managers sur la même grille est le cas normal, pas l'exception. - Chevauchement refusé en transaction, pas seulement dans le formulaire. - Modifier une semaine publiée exige `planning.edit_published`, capacité que le rôle manager n'a pas : un salarié a organisé sa semaine sur ce qu'il a lu. - Toute mutation laisse une entrée d'audit ; la suppression écrit sa trace **avant** l'effacement, sinon l'état supprimé serait perdu. Lecture - `planning.view_unpublished` filtre en base : sans cette capacité, les brouillons ne sont pas chargés du tout. Un test vérifie que les horaires n'apparaissent pas dans le HTML servi — un masquage CSS les y laisserait. - Vue jour reconstruite sur les mêmes données, amplitude déduite de la journée réelle plutôt que figée à 06 h–21 h. Vérification - 26 tests unitaires sur le repérage des semaines et la mise en grille. - Parcours e2e : poser un créneau, refus de chevauchement, publier, dépublier ; et ce que voient un salarié et un manager sur la même semaine. - `scripts/dev-db.sh` : la base de développement est éphémère dans cet environnement, la remonter ne doit pas être une redécouverte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
65 lines
2.5 KiB
TypeScript
65 lines
2.5 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,
|
||
}) => {
|
||
await page.goto('/');
|
||
await page.getByRole('link', { name: 'Profils incomplets' }).click();
|
||
await expect(
|
||
page.getByRole('heading', { name: 'Profils incomplets' }),
|
||
).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();
|
||
});
|