Files
decision/web/e2e/reader.spec.ts
T
YvvandClaude Opus 5.5 94907afb20 v3 : points de la relecture indépendante — une personne une voix, ordre de déploiement, durcissements
- Dans Monnaie libre (électorat = toile de confiance), seuls les membres de la
  toile votent (trustin_access::votes_in) ; une identité d'invitation y lit,
  propose et signale. L'interface le dit à la place des boutons (fr/en/es).
- Un lien d'invitation ne vise jamais une identité Ğ1 ; le lien est réservé
  avant de créer l'identité (deux acceptations simultanées : une seule passe).
- Un nombre n'est jamais une coquille (« 50 » → « 5,0 », « 1 5 » → « 15 »).
- Redirection après connexion : caractères de contrôle refusés.
- En-têtes : X-Frame-Options DENY, frame-ancestors 'none', nosniff, Referrer-Policy.
- Déploiement : le produit passe en premier ; la maquette attend son succès.
  Les conteneurs, même arrêtés, qui revendiquent encore la racine sortent de
  « restart: always » (ils reviendraient au redémarrage de Docker).
- Tests : 57 Rust (dont 14 scénarios d'API), e2e 12 / 12 avec C11.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 04:23:51 +02:00

158 lines
8.7 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 : l’intro, les quatre sections, 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.getByText('en fonction du contexte, processus et modalités, boîte à outils')).toBeVisible();
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.disabled')).toHaveCount(3);
await expect(page.getByRole('link', { name: 'Se connecter avec sa clé Ğ1' })).toBeVisible();
});
test('C2 — les groupes de la même clé ; pas l’Atelier du canal', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto('/groupes');
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);
});
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(/\/groupes$/);
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();
});