Créer un salarié demandait cinq champs. Le contrat, la déclaration et le registre en réclament une vingtaine, et rien ne permettait de les saisir : le sexe, le nom de naissance, le pays et le département de naissance, la situation de famille, les personnes à charge, le téléphone fixe, le complément d'adresse et l'heure d'embauche n'existaient pas en base. La migration les ajoute, toutes facultatives : un dossier incomplet doit pouvoir exister — c'est au registre de signaler ce qui lui manque, pas à la base de refuser l'embauche. Seul l'envoi des plannings par SMS fait exception : c'est un consentement, donc faux par défaut, la charge de la preuve pesant sur l'employeur. Le pays et le département de naissance sont des colonnes à part et non une commune saisie librement : la déclaration sociale les demande séparément, et les rétro-extraire échouerait au premier « Bar-le-Duc (Meuse) ». Le formulaire devient un panneau latéral : une trentaine de champs posés au centre masquaient l'annuaire, et on embauche en regardant qui est déjà là. Le matricule y est proposé à la suite du dernier, attribué dans la transaction pour que deux embauches simultanées ne tombent pas sur le même rang. Le nom de naissance vaut le nom de famille quand il n'en diffère pas, plutôt que d'imposer une ressaisie à l'immense majorité des dossiers. Le responsable hiérarchique et la politique RTT se posent enfin à l'embauche : les deux modèles existaient sans qu'aucun écran ne les alimente. Le registre unique du personnel gagne sa colonne « sexe » et se tient désormais au nom de naissance — le nom d'usage peut changer sans que la personne change. Migration écrite et validée contre le schéma, non appliquée : aucune base n'était ouverte ici. À passer avec `pnpm db:migrate`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
5.7 KiB
TypeScript
137 lines
5.7 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test';
|
||
|
||
/**
|
||
* Fiche salarié — onglets et saisie du dossier.
|
||
*
|
||
* Le dossier personnel est la première chose qu'on remplit après une embauche,
|
||
* et la dernière qu'on relit avant une déclaration. Ce parcours éprouve donc ce
|
||
* que voit un gestionnaire : les onglets tiennent leur route, et ce qui est
|
||
* saisi est encore là au rechargement.
|
||
*/
|
||
|
||
async function createEmployee(page: Page) {
|
||
const suffix = `${Date.now()}-fiche`;
|
||
const lastName = `Fiche${suffix}`;
|
||
|
||
await page.goto('/equipe');
|
||
// Le formulaire d’embauche n’est plus posé au bas de la liste : il s’ouvre
|
||
// en modale, à la demande.
|
||
await page.getByRole('button', { name: 'Ajouter un collaborateur' }).click();
|
||
const form = page.locator('form').filter({ hasText: 'Informations salarié' });
|
||
await form.getByLabel('Prénom').fill('Awa');
|
||
await form.getByLabel('Nom de famille').fill(lastName);
|
||
await form.getByRole('button', { name: 'Saisir un matricule' }).click();
|
||
await form.getByLabel('Matricule').fill(`FIC${suffix}`);
|
||
// Le formulaire propose d’ouvrir un contrat d’emblée : ce parcours n’en veut
|
||
// pas, et un salarié sans contrat doit rester créable.
|
||
await form.getByLabel('Ouvrir un contrat maintenant').uncheck();
|
||
await form.getByRole('button', { name: 'Enregistrer' }).click();
|
||
await expect(page.getByText('Salarié ajouté.')).toBeVisible();
|
||
|
||
await page.getByRole('link', { name: new RegExp(lastName) }).click();
|
||
await expect(
|
||
page.getByRole('heading', { name: new RegExp(lastName) }),
|
||
).toBeVisible();
|
||
|
||
return { lastName, url: page.url() };
|
||
}
|
||
|
||
test('les onglets de la fiche sont des routes', async ({ page }) => {
|
||
const employee = await createEmployee(page);
|
||
|
||
// Le bandeau ne bouge pas d'un onglet à l'autre : c'est ce qui dit de qui on
|
||
// parle pendant qu'on navigue.
|
||
for (const [label, segment] of [
|
||
['Contrats', '/contrats'],
|
||
['Planification et accès', '/planification'],
|
||
['Congés et Absences', '/absences'],
|
||
['Documents', '/documents'],
|
||
] as const) {
|
||
await page.getByRole('link', { name: label, exact: true }).click();
|
||
await expect(page).toHaveURL(new RegExp(`${segment}$`));
|
||
await expect(
|
||
page.getByRole('heading', { name: new RegExp(employee.lastName) }),
|
||
).toBeVisible();
|
||
}
|
||
|
||
// Rouvrir l'onglet directement par son adresse doit donner le même écran :
|
||
// une fiche s'envoie par lien.
|
||
await page.goto(`${employee.url}/contrats`);
|
||
await expect(page.getByText('Tous les contrats et avenants')).toBeVisible();
|
||
});
|
||
|
||
test('le dossier personnel se saisit et se conserve', async ({ page }) => {
|
||
const employee = await createEmployee(page);
|
||
|
||
await expect(page.getByText('Non renseigné').first()).toBeVisible();
|
||
await page
|
||
.getByRole('button', { name: 'Modifier les informations personnelles' })
|
||
.click();
|
||
|
||
await page.getByLabel('Date de naissance').fill('1988-03-12');
|
||
await page.getByLabel('Lieu de naissance').fill('Nancy');
|
||
await page.getByLabel('Nationalité').fill('France');
|
||
await page.getByLabel('Téléphone mobile').fill('+33 6 12 34 56 78');
|
||
await page.getByLabel('Ville').fill('Frouard');
|
||
await page.getByRole('button', { name: 'Enregistrer les modifications' }).click();
|
||
|
||
await expect(page.getByText('Nancy')).toBeVisible();
|
||
|
||
// Le rechargement est la seule preuve qui vaille : un écran qui affiche ce
|
||
// qu'on vient de taper ne dit rien de ce qui a été écrit.
|
||
await page.goto(employee.url);
|
||
await expect(page.getByText('Nancy')).toBeVisible();
|
||
await expect(page.getByText('Frouard')).toBeVisible();
|
||
});
|
||
|
||
test('une date de naissance à venir est refusée', async ({ page }) => {
|
||
await createEmployee(page);
|
||
|
||
await page
|
||
.getByRole('button', { name: 'Modifier les informations personnelles' })
|
||
.click();
|
||
await page.getByLabel('Date de naissance').fill('2999-01-01');
|
||
await page.getByRole('button', { name: 'Enregistrer les modifications' }).click();
|
||
|
||
await expect(page.getByText('Date de naissance invalide.')).toBeVisible();
|
||
});
|
||
|
||
test('une embauche pose le contrat en même temps que le dossier', async ({
|
||
page,
|
||
}) => {
|
||
const suffix = `${Date.now()}-embauche`;
|
||
const lastName = `Embauche${suffix}`;
|
||
|
||
await page.goto('/equipe');
|
||
// Le formulaire d’embauche n’est plus posé au bas de la liste : il s’ouvre
|
||
// en modale, à la demande.
|
||
await page.getByRole('button', { name: 'Ajouter un collaborateur' }).click();
|
||
const form = page.locator('form').filter({ hasText: 'Informations salarié' });
|
||
await form.getByLabel('Prénom').fill('Sofia');
|
||
await form.getByLabel('Nom de famille').fill(lastName);
|
||
await form.getByRole('button', { name: 'Saisir un matricule' }).click();
|
||
await form.getByLabel('Matricule').fill(`EMB${suffix}`);
|
||
|
||
// Le contrat est proposé coché : un dossier créé sans lui n'apparaît sur
|
||
// aucune grille et ne se déclare pas.
|
||
await expect(
|
||
form.getByLabel('Ouvrir un contrat maintenant'),
|
||
).toBeChecked();
|
||
await form.getByLabel('Début du contrat').fill('2026-01-05');
|
||
await form.getByRole('button', { name: 'Enregistrer' }).click();
|
||
await expect(page.getByText('Salarié ajouté.')).toBeVisible();
|
||
|
||
// Le rattachement paraît dans l'annuaire : un dossier sans contrat y porte
|
||
// « Sans contrat », celui-ci porte son établissement.
|
||
await expect(
|
||
page.getByRole('row', { name: new RegExp(lastName) }),
|
||
).not.toContainText('Sans contrat');
|
||
|
||
await page.getByRole('link', { name: new RegExp(lastName) }).click();
|
||
// Le bandeau porte le contrat : c'est ce qui distingue un dossier embauché
|
||
// d'un dossier ouvert.
|
||
await expect(page.getByText('Type de contrat')).toBeVisible();
|
||
await page.getByRole('link', { name: 'Contrats', exact: true }).click();
|
||
await expect(page.getByText('35 heures hebdomadaires')).toBeVisible();
|
||
});
|