Merge branch 'claude/mfa-lever-barriere-enrolement'

This commit is contained in:
Michael committed 2026-08-10 17:05:09 +02:00
commit e27d61dd4c
13 files changed
+827 -27

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
import { expect, test } from '@playwright/test';
/**
* Registre unique du personnel — édition.
*
* Ce qui est éprouvé n'est pas la mise en page, mais qu'un fichier sorte et
* qu'il porte le bon établissement : un registre édité pour le mauvais site ne
* répond à aucune demande d'inspection.
*/
test('le registre s’édite par établissement', async ({ page }) => {
await page.goto('/equipe');
await page.getByRole('button', { name: 'Registre unique du personnel' }).click();
await expect(
page.getByRole('heading', {
name: 'Téléchargez votre registre unique du personnel',
}),
).toBeVisible();
// Sans établissement, rien à télécharger : le registre se tient par site.
const download = page.getByRole('link', { name: 'Télécharger' });
await expect(download).toHaveAttribute('aria-disabled', 'true');
const select = page.getByLabel('Établissement pour lequel éditer le registre');
const value = await select.locator('option').nth(1).getAttribute('value');
await select.selectOption(value!);
const started = page.waitForEvent('download');
await download.click();
const file = await started;
expect(file.suggestedFilename()).toMatch(/^RUP - .+\.pdf$/);
// Le contenu est bien un PDF, pas une page d'erreur renvoyée en 200.
const path = await file.path();
const { readFileSync } = await import('node:fs');
expect(readFileSync(path).subarray(0, 5).toString()).toBe('%PDF-');
});
test('l’adresse du registre refuse un établissement absent', async ({
page,
}) => {
const response = await page.goto('/equipe/registre');
expect(response?.status()).toBe(400);
});
+10
View File
@@ -0,0 +1,10 @@
/**
* Doublure de `server-only`.
*
* Le paquet est fourni par Next au moment du bundling : il n'existe pas dans
* `node_modules`, et son seul rôle est de faire échouer la compilation d'un
* module serveur importé depuis un composant client. Hors de Next, il n'a rien
* à garantir — mais son absence empêcherait de tester les modules qui le
* déclarent.
*/
export {};
+119
View File
@@ -0,0 +1,119 @@
import { describe, expect, it } from 'vitest';
import {
RUP_COLUMNS,
missingMentions,
peopleWithGaps,
type RupPerson,
} from '@/domain/legal/rup';
/**
* Registre unique du personnel — mentions obligatoires.
*
* La sanction se compte **par salarié concerné**, pas par registre : c'est ce
* que le décompte doit refléter, et c'est ce qui décide de l'avertissement
* affiché avant l'édition.
*/
const complete: RupPerson = {
lastName: 'Ferrand',
firstName: 'Camille',
sex: 'F',
nationality: 'France',
birthDate: new Date('1990-04-12T00:00:00Z'),
jobTitle: 'Hôte de caisse',
qualification: 'Niveau 3',
contractLabel: 'CDI',
entryDate: new Date('2024-09-01T00:00:00Z'),
exitDate: null,
};
describe('registre unique du personnel', () => {
it('énumère les mentions fixées par le texte', () => {
expect(RUP_COLUMNS.map((column) => column.key)).toEqual([
'lastName',
'firstName',
'sex',
'nationality',
'birthDate',
'jobTitle',
'qualification',
'contractLabel',
'entryDate',
'exitDate',
]);
});
it('répartit la largeur sur la totalité de la page', () => {
const total = RUP_COLUMNS.reduce((sum, column) => sum + column.width, 0);
expect(total).toBeCloseTo(1, 5);
});
it('ne réclame rien quand toutes les mentions sont là', () => {
expect(missingMentions(complete)).toEqual([]);
});
it('tient une date de sortie absente pour une présence, pas pour un oubli', () => {
// Un salarié en poste n'a pas de date de sortie : la réclamer ferait
// apparaître tout l'effectif courant comme incomplet.
expect(missingMentions({ ...complete, exitDate: null })).toEqual([]);
});
it('nomme les mentions manquantes', () => {
expect(
missingMentions({ ...complete, nationality: null, qualification: '' }),
).toEqual(['Nationalité', 'Qualification']);
});
it('compte les personnes concernées, pas les champs vides', () => {
const bancal: RupPerson = {
...complete,
sex: null,
nationality: null,
birthDate: null,
};
// Trois manques sur une seule personne : une contravention, pas trois.
expect(peopleWithGaps([complete, bancal])).toBe(1);
});
});
describe('rendu du registre', () => {
it('produit un PDF paginé qui porte les manques en pied', async () => {
const { renderRegisterPdf } = await import('@/server/employees/rup-pdf');
// Assez de lignes pour dépasser une page : c'est la rupture que le rendu
// rate le plus volontiers.
const people: RupPerson[] = Array.from({ length: 60 }, (_, index) => ({
...complete,
lastName: `Nom manifestement trop long pour la colonne ${index}`,
sex: index % 2 === 0 ? null : 'F',
}));
const pdf = await renderRegisterPdf({
locationId: 'loc',
locationName: 'La Foir’Fouille',
siret: '12345678901234',
people,
incomplete: 30,
});
expect(Buffer.from(pdf).subarray(0, 5).toString()).toBe('%PDF-');
expect(pdf.length).toBeGreaterThan(1000);
});
it('édite un registre vide plutôt que de refuser', async () => {
const { renderRegisterPdf } = await import('@/server/employees/rup-pdf');
// Un établissement qui vient d'ouvrir n'a personne : le registre existe
// quand même, et son absence serait plus embarrassante que sa vacuité.
const pdf = await renderRegisterPdf({
locationId: 'loc',
locationName: 'Nouvel établissement',
siret: null,
people: [],
incomplete: 0,
});
expect(Buffer.from(pdf).subarray(0, 5).toString()).toBe('%PDF-');
});
});