- 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>
158 lines
8.7 KiB
TypeScript
158 lines
8.7 KiB
TypeScript
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();
|
||
});
|