Lot S : « Proposition seed » dans l'interface (fr, en, es), le texte sans article en vigueur le dit ; e2e repris

Interface au strict nécessaire (le lot U refera la présentation) : la marque
« Proposition seed » là où une version est montrée (liste des propositions,
Détails, panneau Propositions), seedProposal?: boolean dans types.ts ; au-dessus
des ordres, « Aucun article n'est en vigueur » quand c'est le cas.

e2e : C3 (aucun texte en vigueur, 33 lignes, trois Propositions seed au cran 2),
C9 et C18 (« si tout passe »), C20, C23, C24 et M1 par la liste des propositions
dans Monnaie libre ; dans le groupe d'essai, I1, E1 et E2 entrent d'abord par le
vote (seedsInForce : deux voix puis leur retrait, le texte reste en vigueur) pour
les contrôles qui lisent un texte en vigueur (C4 à C6, C20, C21, C24, C28 à C31).
Boucle e2e : 35 / 35.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 04:27:41 +01:00
co-authored by Claude Opus 5.5
parent aaa653e1a7
commit 9a9c99b93b
13 changed files with 147 additions and 77 deletions
+69 -49
View File
@@ -1,5 +1,9 @@
import { expect, test } from '@playwright/test';
import { BOB, DOC, expectWelcome, person, signIn, TEST_DOC, wallet } from './helpers';
import { expect, test, type Page } from '@playwright/test';
import { BOB, deepRow, DOC, expectWelcome, person, seedsInForce, signIn, TEST_DOC, testDoc, wallet } from './helpers';
// The seed proposals (ADR 0011) are not in force at the start: the controls that read a
// text in force find I1, E1 and E2 brought in by the vote in the test group.
test.beforeAll(async ({ browser }) => seedsInForce(browser));
test('C1 — l’accueil sans session : la voûte abrite les trois points, le titre centré en deux lignes, la connexion, le menu complet', async ({ page }) => {
await page.goto('/');
@@ -33,38 +37,41 @@ test('C2 — l’accueil avec session : le choix du groupe, ceux de la même cl
await expect(page).toHaveURL((u) => u.pathname === '/');
});
test('C3 — le texte en vigueur : raison d’être et deux engagements, seed a minima', async ({ page }) => {
test('C3 — au départ, aucun texte en vigueur : la raison d’être et les deux engagements sont des Propositions seed', 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é');
// The seed mark, with the real number of votes beside it (none yet on these three).
await expect(units.getByText('seed a minima · 0 vote')).toHaveCount(3);
await expect(units.getByTitle("Entré en vigueur sans vote, pour ne pas partir d'une page blanche. Il reste sous vote, comme tout texte.")).toHaveCount(3);
// Nothing in force (Yvv, 2026-11-02): said once, above the orders of the sections.
await expect(page.locator('section.text article.unit')).toHaveCount(0);
await expect(page.getByText("Aucun article n'est en vigueur. Les propositions ci-dessous remontent dès qu'elles atteignent leur seuil.")).toBeVisible();
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);
// One line per article: the 33, E3 and E7 holding their two variants inside.
await expect(page.locator('ul.deep button.row')).toHaveCount(33);
// The three seed proposals, marked, at the low level they start at.
const seeds = page.locator('ul.deep button.row', { has: page.locator('.tag', { hasText: 'Proposition seed' }) });
await expect(seeds.locator('.title')).toHaveText(["Raison d'être", 'Unicité du compte', 'Réciprocité']);
await expect(seeds.getByTitle("Texte de départ mis en avant pour lancer le vote. Il n'est pas en vigueur : il y entre par le vote, comme toute proposition.")).toHaveCount(3);
for (const row of await seeds.all()) await expect(row).toContainText('0 pour · 0 contre');
for (const row of await seeds.all()) await expect(row).toContainText('inertie 2 / 5 · basse');
});
test('C4 — les actions sont contextuelles, le vote se déplie sous l’article', async ({ page }) => {
// In the test group, where the Raison d'être was voted into force (beforeAll).
await signIn(page, 'Chiara');
await page.goto(DOC);
await page.goto(TEST_DOC);
const first = page.locator('section.text article.unit').first();
await expect(first.locator('h2')).toHaveText("Raison d'être");
await first.hover();
const toggle = first.getByRole('button', { name: 'Le vote' });
await toggle.click();
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
const fold = first.locator('section.fold');
await expect(fold.getByText('Seuil : 50 voix pour')).toBeVisible();
await expect(fold.getByText('Il manque 50 voix pour.')).toBeVisible();
await expect(fold.getByText(/^Seuil : \d+ voix pour$/)).toBeVisible();
await expect(fold.getByText(/^Il manque \d+ voix pour\.$/)).toBeVisible();
await fold.getByText('La formule du seuil').click();
await expect(fold.getByText(/W — électorat/)).toBeVisible();
// The level of the article sets M and G: the Raison d'être is at 3 / 5.
await expect(fold.getByText('M — majorité à pleine participation : 55 % — inertie 3 / 5 · moyenne')).toBeVisible();
// The level of the article sets M and G: the Raison d'être, a seed proposal, starts at 2 / 5.
await expect(fold.getByText('M — majorité à pleine participation : 50 % — inertie 2 / 5 · basse')).toBeVisible();
await expect(fold.getByRole('button', { name: 'Réglages de la formule' })).toBeVisible();
// No window opens: the vote unfolds in place, lives in the URL, and folds back.
await expect(page.locator('aside.panel')).toHaveCount(0);
@@ -76,18 +83,28 @@ test('C4 — les actions sont contextuelles, le vote se déplie sous l’article
});
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('section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
await expect(p.getByText('Vote enregistré.')).toBeVisible();
// Every dev sign-in joins the test group: from 16 members B^W vanishes and, C being 1, one
// voice may suffice (ADR 0007); else the second one brings it in.
const title = 'Les deux garanties réciproques';
const news = `« ${title} » est entré en vigueur.`;
const inForce = async (page: Page) => (await testDoc(page)).units.some((u) => u.inForce?.title === title);
let voices = 0;
let last: Page | undefined;
for (const name of ['Yvv', 'Chiara']) {
last = await person(browser, name);
await last.goto(TEST_DOC);
await last.locator('ul.deep button.row', { hasText: title }).click();
await last.locator('section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
await expect(last.getByText('Vote enregistré.')).toBeVisible();
voices += 1;
if (await inForce(last)) break;
}
await expect(chiara.getByText('« Les deux garanties réciproques » est entré en vigueur.')).toBeVisible();
await expect(chiara.locator('section.text article.unit')).toHaveCount(4);
if (!last) throw new Error('nobody voted');
await expect(last.getByText(news)).toBeVisible();
// With the three seed proposals voted in (beforeAll).
await expect(last.locator('section.text article.unit')).toHaveCount(4);
// In force now: its vote stays unfolded, under the text.
await expect(chiara.locator('section.text section.fold')).toContainText('2 pour · 0 contre');
await expect(last.locator('section.text section.fold')).toContainText(`${voices} pour · 0 contre`);
});
test('C6 — une coquille se corrige sans vote', async ({ page }) => {
@@ -147,8 +164,9 @@ test('C9 — les aperçus : bien cotées, et tout', async ({ page }) => {
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);
// Nothing in force in Monnaie libre, and nothing well rated yet (no « pour » there so far).
await page.getByRole('radio', { name: 'Si les bien cotées passent' }).click();
await expect(page.locator('section.text article.unit')).toHaveCount(3);
await expect(page.locator('section.text article.unit')).toHaveCount(0);
});
test('C10 — trustinWallet : la gestion des accès est réservée', async ({ browser }) => {
@@ -228,9 +246,11 @@ test('C12 — lien d’équipe : chaque personne qui l’ouvre a sa propre ident
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 }) => {
test('C18 — la Raison d’être ouvre le texte, en trois langues', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(DOC);
// A seed proposal, not in force: it opens the text « if everything passes ».
await page.getByRole('radio', { name: 'Si tout passe' }).click();
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.
@@ -294,20 +314,20 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => {
await expect(panel.getByText("Part exigée : les « pour » demandés au-delà de C, à 100 votants comme d'habitude.")).toBeVisible();
await page.keyboard.press('Escape');
// From the vote of an article: Unicité du compte is at 4 / 5, its row is marked.
const e1 = page.locator('section.text article.unit', { hasText: 'Unicité du compte' });
await e1.hover();
await e1.getByRole('button', { name: 'Le vote' }).click();
await expect(e1.getByRole('heading', { name: 'Inertie · 4 / 5 · haute' })).toBeVisible();
// From the vote of an article: Unicité du compte, a seed proposal, is at 2 / 5, its row is marked.
await deepRow(page, 'Unicité du compte').click();
const e1 = page.getByRole('region', { name: 'Le vote de « Unicité du compte »' });
await expect(e1.getByRole('heading', { name: 'Inertie · 2 / 5 · basse' })).toBeVisible();
await e1.getByRole('button', { name: 'Réglages de la formule' }).click();
await expect(panel.locator('tr.current')).toHaveCount(1);
await expect(panel.locator('tr.current th')).toContainText('4');
await expect(panel.locator('tr.current th')).toContainText('2');
await expect(panel.locator('tr.current th')).toContainText('cet article');
await expect(required).toHaveText(['50', '50', '50', '50', '50']);
await expect(panel.getByText('Voix pour : à 0 votant, la participation de cette version.')).toBeVisible();
await page.keyboard.press('Escape');
// The five levels to choose from, in the test group; the change is voted at the article's level.
// The five levels to choose from, in the test group (Réciprocité voted into force there,
// beforeAll); the change is voted at the article's level.
await page.goto(TEST_DOC);
const e2 = page.locator('section.text article.unit', { hasText: 'Réciprocité' });
await e2.hover();
@@ -316,25 +336,25 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => {
const levels = e2.getByRole('radiogroup', { name: 'Nouvelle inertie' }).getByRole('radio');
await expect(levels).toHaveCount(5);
// The current level is reachable — one stop in the tab order — and never chosen.
await expect(levels.nth(3)).toHaveAttribute('aria-disabled', 'true');
await expect(levels.nth(3)).toContainText('actuelle');
await expect(levels.nth(1)).toHaveAttribute('aria-disabled', 'true');
await expect(levels.nth(1)).toContainText('actuelle');
await expect(e2.locator('[role="radio"][tabindex="0"]')).toHaveCount(1);
await expect(levels.nth(3)).toHaveAttribute('tabindex', '0');
await expect(e2.getByText("Un changement d'inertie se vote lui-même à l'inertie de l'article : 4 / 5 · haute.")).toBeVisible();
await expect(levels.nth(1)).toHaveAttribute('tabindex', '0');
await expect(e2.getByText("Un changement d'inertie se vote lui-même à l'inertie de l'article : 2 / 5 · basse.")).toBeVisible();
// A click on it (Playwright refuses an aria-disabled target: the event is sent as is) chooses nothing.
await levels.nth(3).dispatchEvent('click');
await levels.nth(1).dispatchEvent('click');
await expect(e2.locator('[role="radio"][aria-checked="true"]')).toHaveCount(0);
// With the keyboard: the arrows go from level to level and choose it, wrapping round.
await levels.nth(3).focus();
await page.keyboard.press('ArrowRight');
await expect(levels.nth(4)).toBeFocused();
await expect(levels.nth(4)).toHaveAttribute('aria-checked', 'true');
await page.keyboard.press('ArrowRight');
await levels.nth(1).focus();
await page.keyboard.press('ArrowLeft');
await expect(levels.nth(0)).toBeFocused();
await expect(levels.nth(0)).toHaveAttribute('aria-checked', 'true');
await expect(e2.locator('[role="radio"][tabindex="0"]')).toHaveCount(1);
await page.keyboard.press('ArrowLeft');
await expect(levels.nth(4)).toBeFocused();
await expect(levels.nth(4)).toHaveAttribute('aria-checked', 'true');
await expect(e2.locator('[role="radio"][tabindex="0"]')).toHaveCount(1);
await page.keyboard.press('ArrowRight');
await expect(levels.nth(0)).toHaveAttribute('aria-checked', 'true');
// Or with a click.
await levels.nth(4).click();
await expect(levels.nth(4)).toHaveAttribute('aria-checked', 'true');
@@ -342,5 +362,5 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => {
// Its own vote unfolds, in the list of proposals.
const change = page.getByRole('region', { name: 'Le vote de « inertie vers 5 / 5 · très haute »' });
await expect(change).toBeVisible();
await expect(change.getByText('inertie 4 / 5 · haute', { exact: true })).toBeVisible();
await expect(change.getByText('inertie 2 / 5 · basse', { exact: true })).toBeVisible();
});