CI / verify (pull_request) Canceled after 0s
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>
168 lines
7.0 KiB
TypeScript
168 lines
7.0 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('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();
|
||
});
|