Files
decision/web/e2e/reader.spec.ts
T
YvvandClaude Opus 5.5 81b604e5f2 v3 : l'entrée — accueil avec le choix du groupe, logo « Clé de voûte », quatre ambiances, menu et pages d'outil par groupe
- Accueil : le titre, le leitmotiv en trois lignes (contexte et cadrage ·
  processus et modalités · boîte à outils), puis directement le choix du
  groupe ; sans session, la connexion à sa place. /groupes redirige vers /,
  « Mes groupes » et le retour après connexion mènent à /.
- Logo « Clé de voûte » du DA (signe en currentColor, clé en --cle, libre
  Literata italique, Decision romain), droit, dans le menu et la barre
  mobile ; Stamp retiré, le sceau 井 ne bouge pas. Favicon, icônes 16/32/48,
  apple-touch, 192/512, manifeste.
- Quatre ambiances : pierre et calcaire (claires), nuit et ardoise
  (sombres), --cle par ambiance ; quatre pastilles à la place du soleil /
  lune ; première visite selon l'appareil (pierre / nuit). Calcaire et
  ardoise : textes, accent, mousse et avertissement à 4,5:1 au moins.
- Page d'un groupe : les quatre piliers, puis « En cours ».
- Menu : nombre de docs en cours à côté de « Docs de référence », surligné
  seulement dedans, seul Docs réagit au survol ; les trois autres éteints
  mais ouvrent leur page ; hors d'un groupe, le dernier groupe ouvert.
- /g/[group]/docs « En cours » + « À venir » ; /mandats et /protocoles du
  groupe : « À venir » éteints ; /g/[group]/decisions : une ligne par doc,
  « N modules à voter », « dont N sans ton vote ». Anciennes adresses
  /decisions, /mandats, /protocoles : vers le groupe courant, sinon /.
- e2e : C1, C2, C11, C12, M1 mis à jour ; C13–C17 pour L1–L6 ; CONTROLES.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:21:57 +01:00

202 lines
12 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 : préambule 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('Préambule');
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();
await expect(page.locator('ul.deep button.row')).toHaveCount(32);
});
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 preamble = page.locator('section.text article.unit').first();
await preamble.hover();
await preamble.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 });
await body.fill((await body.inputValue()).replace('latransmission', 'la transmission'));
await panel.getByRole('button', { name: 'Enregistrer la correction' }).click();
await expect(page.getByText('Coquille corrigée, sans toucher aux votes.')).toBeVisible();
await expect(preamble).toContainText('la transmission de ce document');
// A change of meaning is turned away.
await panel.getByText('Corriger une coquille').click();
await body.fill('Tout autre chose, qui change le sens du préambule.');
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: 'Connaissance suffisante' }).first().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: 'Connaissance suffisante' }).first().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);
});