Files
planflow/tests/e2e/fiche.spec.ts
T
MichaelandClaude Opus 5 481af181fa
CI / verify (pull_request) Canceled after 0s
Fermer les listes du dossier, et déclarer les titres de séjour
Quatre champs se saisissaient en texte libre là où la valeur appartient à
une liste : nationalité, pays de naissance, pays de l'adresse, département.
« Frnace » y entrait aussi bien que « France », et le registre imprimait la
faute.

Les pays sont stockés par leur code ISO 3166-1 et nommés à l'affichage par
`Intl.DisplayNames` : un nom dépend de la langue et des versions d'ICU, un
code ne bouge pas, et deux cent cinquante traductions n'ont pas à être
entretenues à la main. Les valeurs héritées — « France » en toutes lettres
— sont reconnues à la lecture puis normalisées au premier enregistrement,
plutôt que blanchies par un menu qui ne les retrouve pas.

Les départements portent la Corse et l'outre-mer : « 2A » n'est pas un
nombre, et le département de naissance sert à la déclaration sociale, qui
attend le code.

Les téléphones gagnent leur indicatif, choisi à part et recollé en un seul
champ. L'enregistrer séparément obligerait chaque lecture — annuaire,
export, SMS — à le recomposer, et un numéro à moitié lu ne joint personne.
La découpe prend l'indicatif le plus long : « +352 » commence par « +3 ».

Le titre de séjour se déclare enfin. `WorkPermit` existait depuis le début
sans qu'aucun écran ne l'alimente. L'échéance est exigée dès qu'un titre est
posé — employer quelqu'un dont l'autorisation a expiré est un délit, et sans
date rien ne peut le signaler à temps. Décocher retire le titre plutôt que
de le laisser alerter pour quelqu'un qui n'en relève plus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:48:51 +02:00

168 lines
7.0 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, 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('Commune de naissance').fill('Nancy');
// Pays et département viennent de listes fermées : la saisie libre ne peut
// plus y introduire « Frnace ».
await page.getByLabel('Nationalité').selectOption('FR');
await page.getByLabel('Département de naissance').selectOption('54');
// L'indicatif est un contrôle à part ; le champ ne porte que le reste.
await page.getByLabel('Téléphone mobile', { exact: true }).fill('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();
await expect(page.getByText('54 - Meurthe-et-Moselle')).toBeVisible();
await expect(page.getByText('+33 6 12 34 56 78')).toBeVisible();
});
test('un titre de séjour expiré est signalé sur la fiche', async ({ page }) => {
await createEmployee(page);
await page
.locator('form')
.filter({ hasText: 'Travailleur étranger' })
.or(page.getByText('Autorisation de travail'))
.first()
.scrollIntoViewIfNeeded();
const card = page
.locator('section')
.filter({ hasText: 'Autorisation de travail' });
await card.getByRole('button', { name: 'Modifier' }).click();
await card.getByLabel(/Travailleur étranger/).check();
// Sans échéance, rien ne peut alerter : le refus est le comportement voulu.
await card.getByLabel('Nature du titre').fill('Carte de séjour');
await card.getByLabel('Référence').fill('AGDREF-123456');
await card.getByLabel('Expire le').fill('2020-01-01');
await card.getByRole('button', { name: 'Enregistrer' }).click();
await expect(page.getByText(/Ce titre a expiré/)).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();
});