Files
decision/web/e2e/reader.spec.ts
T
YvvandClaude Opus 5.5 8727315612
ci/woodpecker/push/app Pipeline was successful
ci/woodpecker/push/maquette Pipeline was successful
Fusion du lot A : accueil sur le choix du groupe, logo Clé de voûte, quatre ambiances, menu, À venir, Décisions
Contrôles du lot R renumérotés C18 (Raison d'être) et C19 (versions d'un article).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:24:36 +01:00

245 lines
14 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 } from '@playwright/test';
import { DOC, person, signIn, TEST_DOC } from './helpers';
test('C1 — l’accueil sans session : le titre, le leitmotiv en trois lignes, la connexion, le menu complet', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Rendre possible la décision collective');
await expect(page.locator('.leitmotiv li')).toHaveText(['contexte et cadrage', 'processus et modalités', 'boîte à outils']);
const nav = page.getByRole('navigation', { name: 'Boîte à outils' });
for (const item of ['Décisions', 'Docs de référence', 'Mandats', 'Protocoles']) {
await expect(nav.locator('.tool', { hasText: item })).toBeVisible();
}
await expect(nav.locator('.tool.off')).toHaveCount(3);
await expect(page.getByRole('link', { name: 'Se connecter avec sa clé Ğ1' })).toBeVisible();
// The four pillars live on the page of a group, no longer here.
await expect(page.locator('.pillars')).toHaveCount(0);
await expect(page.getByText('Monnaie libre', { exact: true })).toHaveCount(0);
});
test('C2 — l’accueil avec session : le choix du groupe, ceux de la même clé ; pas l’Atelier du canal', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto('/');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Rendre possible la décision collective');
await expect(page.locator('a.root')).toContainText('Monnaie libre');
for (const g of ['Monnaie libre', 'Axiom', 'Librodrome', 'Groupe local Vallée Drôme Crestois']) {
await expect(page.getByText(g, { exact: true })).toBeVisible();
}
await expect(page.getByText('Atelier du canal')).toHaveCount(0);
await expect(page.getByRole('link', { name: 'Se connecter avec sa clé Ğ1' })).toHaveCount(0);
// The old address and « Mes groupes » lead here.
await page.goto('/groupes');
await expect(page).toHaveURL((u) => u.pathname === '/');
await page.goto('/g/monnaie-libre');
await page.getByRole('navigation', { name: 'Boîte à outils' }).getByRole('link', { name: 'Mes groupes' }).click();
await expect(page).toHaveURL((u) => u.pathname === '/');
});
test('C3 — le texte en vigueur : raison d’être et deux engagements, seed a minima', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(DOC);
await expect(page.getByRole('heading', { level: 1 })).toHaveText("Acte d'engagement Certification");
const units = page.locator('section.text article.unit');
await expect(units).toHaveCount(3);
await expect(units.nth(0).locator('h2')).toHaveText("Raison d'être");
await expect(units.nth(1).locator('h2')).toHaveText('Unicité du compte');
await expect(units.nth(2).locator('h2')).toHaveText('Réciprocité');
await expect(page.getByText('seed a minima · 0 vote')).toHaveCount(3);
await expect(page.getByRole('heading', { name: 'Propositions en cours' })).toBeVisible();
// One line per article: E3 and E7 hold their two variants inside.
await expect(page.locator('ul.deep button.row')).toHaveCount(30);
});
test('C4 — les actions sont contextuelles, le vote s’ouvre en panneau', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(DOC);
const first = page.locator('section.text article.unit').first();
await first.hover();
await first.getByRole('button', { name: 'Le vote' }).click();
const panel = page.locator('aside.panel');
await expect(panel.getByText('Seuil : 50 voix pour')).toBeVisible();
await expect(panel.getByText('Il manque 50 voix pour.')).toBeVisible();
await panel.getByText('La formule du seuil').click();
await expect(panel.getByText(/W — électorat/)).toBeVisible();
// The panel lives in the URL.
await expect(page).toHaveURL(/onglet=vote/);
await page.keyboard.press('Escape');
await expect(panel).toHaveCount(0);
});
test('C5 — une proposition remonte en vigueur quand elle atteint son seuil (groupe d’essai)', async ({ browser }) => {
const yvv = await person(browser, 'Yvv');
const chiara = await person(browser, 'Chiara');
for (const p of [yvv, chiara]) {
await p.goto(TEST_DOC);
await p.locator('ul.deep button.row', { hasText: 'Les deux garanties réciproques' }).click();
await p.locator('aside.panel').getByRole('button', { name: 'Pour', exact: true }).click();
await expect(p.getByText('Vote enregistré.')).toBeVisible();
}
await expect(chiara.getByText('« Les deux garanties réciproques » est entré en vigueur.')).toBeVisible();
await chiara.keyboard.press('Escape');
await expect(chiara.locator('section.text article.unit')).toHaveCount(4);
});
test('C6 — une coquille se corrige sans vote', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(TEST_DOC);
const unicity = page.locator('section.text article.unit', { hasText: 'Unicité du compte' });
await unicity.hover();
await unicity.getByRole('button', { name: 'Le vote' }).click();
const panel = page.locator('aside.panel');
await panel.getByText('Corriger une coquille').click();
const body = panel.getByLabel('Texte', { exact: true });
// Typographic apostrophes: the letters and the signs stay, only their style changes.
await body.fill((await body.inputValue()).replaceAll("'", '’'));
await panel.getByRole('button', { name: 'Enregistrer la correction' }).click();
await expect(page.getByText('Coquille corrigée, sans toucher aux votes.')).toBeVisible();
await expect(unicity).toContainText('Je m’engage sur l’honneur');
// A change of meaning is turned away.
await panel.getByText('Corriger une coquille').click();
await body.fill('Tout autre chose, qui change le sens de cet engagement.');
await panel.getByRole('button', { name: 'Enregistrer la correction' }).click();
await expect(panel.getByText('Ce changement touche au sens : propose-le comme modification.')).toBeVisible();
});
test('C7 — l’assistant propose de découper deux décisions', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(TEST_DOC);
await page.getByRole('button', { name: 'Proposer un nouvel article' }).click();
const panel = page.locator('aside.panel');
await panel.getByLabel('Titre').fill('Joignabilité et vérification');
await panel.getByLabel('Texte').fill(
"Je m'engage à rester joignable par mes certifieurs.\n\nJe m'engage à vérifier la clé publique avec la personne.",
);
await expect(panel.getByText('Ce texte semble porter 2 décisions.', { exact: false })).toBeVisible();
await panel.getByRole('button', { name: 'Découper en 2 brouillons' }).click();
await expect(page.getByText('Découpé en 2 brouillons.')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Tes brouillons' })).toBeVisible();
});
test('C8 — multilingue : le texte et l’interface en anglais et en espagnol', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(DOC);
await page.getByRole('radiogroup', { name: 'Langue du texte' }).getByRole('radio', { name: 'EN' }).click();
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Certification Commitment Act');
await expect(page.getByText('Open proposals', { exact: false }).first()).toBeVisible();
await page.getByRole('radiogroup', { name: 'Text language' }).getByRole('radio', { name: 'ES' }).click();
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Acta de compromiso de Certificación');
});
test('C9 — les aperçus : bien cotées, et tout', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(DOC);
await page.getByRole('radio', { name: 'Si tout passe' }).click();
await expect(page.getByText("Aperçu : le texte tel qu'il serait si toutes les propositions", { exact: false })).toBeVisible();
await expect(page.locator('section.text article.unit')).toHaveCount(33);
await page.getByRole('radio', { name: 'Si les bien cotées passent' }).click();
await expect(page.locator('section.text article.unit')).toHaveCount(3);
});
test('C10 — trustinWallet : la gestion des accès est réservée', async ({ browser }) => {
const chiara = await person(browser, 'Chiara');
await chiara.goto('/acces');
await expect(chiara.getByText('La gestion des accès est réservée aux administrateurs de trustinWallet.')).toBeVisible();
const yvv = await person(browser, 'Yvv', true);
await yvv.goto('/acces');
await expect(yvv.getByRole('heading', { name: 'trustinWallet — accès' })).toBeVisible();
await yvv.getByLabel('Pour qui').fill('Invitée');
await yvv.getByRole('button', { name: "Créer le lien d'invitation" }).click();
await expect(yvv.locator('.link code')).toContainText('/invitation#');
});
test('C11 — une personne, une voix : dans Monnaie libre, seuls les membres de la toile votent', async ({ browser }) => {
const yvv = await person(browser, 'Yvv', true);
const r = await yvv.request.post('/api/access/invitations', {
data: { label: 'Invitée', groups: ['monnaie-libre', 'essai'] },
headers: { 'x-requested-with': 'libredecision' },
});
const { link } = (await r.json()) as { link: string };
const ctx = await browser.newContext({ locale: 'fr-FR' });
const invited = await ctx.newPage();
await invited.goto(`/invitation#${link.split('#')[1]}`);
await expect(invited).toHaveURL((u) => u.pathname === '/');
await invited.goto(DOC);
await invited.locator('ul.deep button.row', { hasText: 'Vérification personnelle de la clé' }).click();
const panel = invited.locator('aside.panel');
await expect(panel.getByText('Ici, seuls les membres de la toile de confiance Ğ1 votent', { exact: false })).toBeVisible();
await expect(panel.getByRole('button', { name: 'Pour', exact: true })).toHaveCount(0);
// In the test group, the same person votes.
await invited.goto(TEST_DOC);
await invited.locator('ul.deep button.row', { hasText: 'Vérification personnelle de la clé' }).click();
await expect(invited.locator('aside.panel').getByRole('button', { name: 'Pour', exact: true })).toBeVisible();
});
test('C12 — lien d’équipe : chaque personne qui l’ouvre a sa propre identité', async ({ browser }) => {
const yvv = await person(browser, 'Yvv', true);
await yvv.goto('/acces');
const team = yvv.locator('section', { has: yvv.getByRole('heading', { name: "Liens d'équipe" }) });
await team.getByLabel('Pour quelle équipe').fill('Équipe de test');
await team.getByLabel("Groupe d'essai").check();
await team.getByRole('button', { name: "Créer le lien d'équipe" }).click();
const link = (await team.locator('.link code').textContent()) ?? '';
expect(link).toContain('/equipe#');
for (const name of ['Luna', 'Paul']) {
const ctx = await browser.newContext({ locale: 'fr-FR' });
const page = await ctx.newPage();
await page.goto(`/equipe#${link.split('#')[1]}`);
await expect(page.getByText('« Équipe de test » t’ouvre', { exact: false })).toBeVisible();
await page.getByLabel('Ton nom').fill(name);
await page.getByRole('button', { name: 'Rejoindre' }).click();
await expect(page).toHaveURL((u) => u.pathname === '/');
await ctx.close();
}
// A shared browser: the person signed in steps aside, the next one gives a name.
const shared = await person(browser, 'Zoé');
await shared.goto(`/equipe#${link.split('#')[1]}`);
await expect(shared.getByText('Ta session est ouverte sous le nom Zoé', { exact: false })).toBeVisible();
await shared.getByRole('button', { name: "Ce n'est pas moi : me déconnecter" }).click();
await shared.getByLabel('Ton nom').fill('luna');
await shared.getByRole('button', { name: 'Rejoindre' }).click();
await expect(shared.getByRole('alert')).toContainText('Ce nom est déjà pris');
await yvv.reload();
await expect(team.getByText('2 / 30 personnes', { exact: false })).toBeVisible();
await expect(yvv.getByText('venue par « Équipe de test »')).toHaveCount(2);
});
test('C18 — la Raison d’être ouvre le texte en vigueur, en trois langues', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(DOC);
const first = page.locator('section.text article.unit').first();
await expect(first.locator('h2')).toHaveText("Raison d'être");
// The first sentence in italics, then two paragraphs.
const paragraphs = first.locator('.prose p');
await expect(paragraphs).toHaveCount(3);
await expect(paragraphs.nth(0).locator('em')).toContainText("Derrière une clé il n'y a qu'un seul être humain vivant");
await expect(paragraphs.nth(1)).toContainText("C'est le seul et unique propos de la certification");
await expect(paragraphs.nth(2)).toContainText('la première monnaie libre');
await page.getByRole('radiogroup', { name: 'Langue du texte' }).getByRole('radio', { name: 'EN' }).click();
await expect(first.locator('h2')).toHaveText('Purpose');
await page.getByRole('radiogroup', { name: 'Text language' }).getByRole('radio', { name: 'ES' }).click();
await expect(first.locator('h2')).toHaveText('Razón de ser');
});
test('C19 — un article, une ligne : ses versions se déplient', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(DOC);
const e3 = page.locator('ul.deep button.row', { hasText: 'Connaissance suffisante' });
await expect(e3).toHaveCount(1);
await expect(e3).toContainText('2 versions');
await expect(e3).toHaveAttribute('aria-expanded', 'false');
// Unfolded with the keyboard: its two versions, each with its label.
await e3.focus();
await page.keyboard.press('Enter');
await expect(e3).toHaveAttribute('aria-expanded', 'true');
const versions = page.locator(`#${await e3.getAttribute('aria-controls')}`).getByRole('button');
await expect(versions).toHaveCount(2);
await expect(versions.nth(0)).toContainText('variante 1.0.0');
await expect(versions.nth(1)).toContainText('variante 0.4.0-draft');
// A version opens its vote, as any proposal.
await versions.nth(1).click();
await expect(page.locator('aside.panel').getByText('Seuil : 50 voix pour')).toBeVisible();
await expect(page).toHaveURL(/version=/);
await e3.click();
await expect(e3).toHaveAttribute('aria-expanded', 'false');
await expect(versions).toHaveCount(0);
});