Lot T4 : l'ADR 0014, le fichier de relecture textes-amorce.md, les contrôles e2e au document de T4

ADR 0014 « Les informations de fonctionnement ne se votent pas » : ce qui sort
(F1, F1.1, F1.2, F1.3, N1, N1.1, O1 et l'ordre de leurs sections), comment (rien
d'effacé, units.retired_at), la garde et ses refus, aucun vote futur (404 / 400),
ce que dira l'interface (U3), la file de publication, le retour arrière (prouvé
avec l'image de de1b7eb), les limites. textes-amorce.md : chaque texte avant /
après en fr, en, es, E12, ce qui sort, les choix à confirmer par Yvv.

e2e (aucun changement de web/src) : C3, C9 (27 articles, cinq sections, quatre
Propositions seed), C17 (33 modules à voter), C18 et C31 (le texte d'I1 de
T4), C25 (cinq lignes d'ordre, E12 dans l'ordre de « fondamental »), C35 (E12
entre E1 et E2 ; le glisser d'une place), C38 (4 Propositions seed).
CONTROLES.md suit ; CLAUDE.md : article_amend.rs, retire.rs, amorce.rs,
fixtures/seed-m8, le seed de 27 articles, l'invariant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 21:45:27 +01:00
co-authored by Claude Opus 5.5
parent 093d1ca471
commit 3996f6f99d
11 changed files with 512 additions and 53 deletions
+6 -6
View File
@@ -142,10 +142,10 @@ test('C17 — Décisions : une ligne par doc, les modules à voter, dont sans to
const rows = page.locator('ul.lines > li');
await expect(rows).toHaveCount(1);
await expect(rows.first()).toContainText("Acte d'engagement Certification");
// 33 articles, the title of the document and the order of 7 sections (ADR 0009; the
// section of a single article has no order to decide).
await expect(rows.first()).toContainText('41 modules à voter');
await expect(rows.first()).toContainText('dont 41 sans ton vote');
// 27 articles, the title of the document and the order of its 5 sections (ADR 0009; since
// lot T4, the rules of operation are out of the document, ADR 0014).
await expect(rows.first()).toContainText('33 modules à voter');
await expect(rows.first()).toContainText('dont 33 sans ton vote');
// A vote on one version of E3 (it has a variant): one module fewer without her vote. « Contre »
// counts as much, and leaves the previews of C9 unchanged (the server is shared). In Monnaie
// libre the vote goes through the Ğ1 chain: the ballot, then what her wallet (Charlie's
@@ -165,8 +165,8 @@ test('C17 — Décisions : une ligne par doc, les modules à voter, dont sans to
.poll(async () => (await (await page.request.get(`/api/ballots/${ballot.id}`)).json()).ballot.status, { timeout: 15_000 })
.toBe('confirmed');
await page.reload();
await expect(rows.first()).toContainText('41 modules à voter');
await expect(rows.first()).toContainText('dont 40 sans ton vote');
await expect(rows.first()).toContainText('33 modules à voter');
await expect(rows.first()).toContainText('dont 32 sans ton vote');
await rows.first().getByRole('link').click();
await expect(page).toHaveURL(path(DOC));
});
+2 -1
View File
@@ -119,7 +119,8 @@ export async function testDoc(page: Page): Promise<{
* force in the test group, as the vote does it: voices for until each enters, then those
* voices withdrawn. A version in force stays so (a permanent vote: only a rival displaces
* it), at zero votes, as the controls written before the seed proposals found it. Never in
* Monnaie libre, whose vote goes through the chain; nothing done twice.
* Monnaie libre, whose vote goes through the chain; nothing done twice. E12 « Responsabilité
* civique », the fourth (lot T4), stays a seed proposal: open, as C35 reads it.
*/
export async function seedsInForce(browser: Browser) {
const voices = [await person(browser, 'Amorce A'), await person(browser, 'Amorce B')];
+4 -3
View File
@@ -42,9 +42,10 @@ test('C25 — l’ordre des articles : une ligne en fin de section, réordonnée
await signIn(page, 'Chiara');
await page.goto(TEST_DOC);
await unfold(page);
// Each section ends with its order, in one line; a section of one article has nothing to decide.
// Each section ends with its order, in one line: five sections (formule, inertie and
// ordonnancement are out of the document, ADR 0014).
const modules = page.getByRole('group', { name: /^Ordre des articles · / });
await expect(modules).toHaveCount(7);
await expect(modules).toHaveCount(5);
await expect(page.getByRole('group', { name: 'Ordre des articles · Ordonnancement' })).toHaveCount(0);
const order = page.getByRole('group', { name: 'Ordre des articles · Conclusion' });
await expect(order).toContainText('Ordre de la section');
@@ -62,7 +63,7 @@ test('C25 — l’ordre des articles : une ligne en fin de section, réordonnée
const fondVote = page.getByRole('region', { name: 'Le vote de « Ordre des articles · Engagements fondamentaux »' });
// Its inertia, said once, in its own place (lot U2).
await expect(fondVote.getByRole('heading', { name: 'Inertie · 2 / 5 · basse' })).toBeVisible();
await expect(fondVote.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText(['Engagements fondamentaux', 'Unicité du compte', 'Réciprocité']);
await expect(fondVote.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText(['Engagements fondamentaux', 'Unicité du compte', 'Responsabilité civique', 'Réciprocité']);
await fond.getByRole('button', { name: 'Le vote' }).click();
// Another order: « Réordonner », the article, then its new place — in place, no panel.
+3 -3
View File
@@ -243,11 +243,11 @@ test('C31 — une coquille sur un texte en vigueur attend sa validation, puis s
const panel = vera.locator('aside.panel');
await panel.getByText('Corriger une coquille').click();
const body = panel.getByLabel('Texte', { exact: true });
await body.fill((await body.inputValue()).replace("C'est le seul", 'C’est le seul'));
await body.fill((await body.inputValue()).replace("C'est l'unique", 'C’est l’unique'));
await panel.getByRole('button', { name: 'Enregistrer la correction' }).click();
await expect(vera.getByText(/^Ta correction attend un administrateur, ce texte venant du seed/)).toBeVisible();
// In force, only a validated fix changes the text.
await expect(purpose).toContainText("C'est le seul et unique propos");
await expect(purpose).toContainText("C'est l'unique enjeu");
await expect(panel.locator('li.fix')).toContainText("en attente : s'applique d'elle-même le");
await ada.reload();
@@ -258,7 +258,7 @@ test('C31 — une coquille sur un texte en vigueur attend sa validation, puis s
await expect(ada.getByText('Correction appliquée.')).toBeVisible();
await vera.reload();
await expect(purpose).toContainText('C’est le seul et unique propos');
await expect(purpose).toContainText('C’est l’unique enjeu');
const doc = await testDoc(vera);
const unit = doc.units.find((u) => u.inForce?.title === "Raison d'être");
const after = await details(vera, unit?.inForce?.id ?? '');
+21 -17
View File
@@ -10,6 +10,8 @@ test.beforeAll(async ({ browser }) => seedsInForce(browser));
const EF = 'Engagements fondamentaux';
const E1 = 'Unicité du compte';
// Lot T4 (Yvv, 2026-11-03): between the two, a seed proposal, left open by `seedsInForce`.
const EC = 'Responsabilité civique';
const E2 = 'Réciprocité';
const ORDER = `Ordre des articles · ${EF}`;
const CSRF = { 'x-requested-with': 'libredecision' };
@@ -41,11 +43,11 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss
await expect(page.locator('aside.panel')).toHaveCount(0);
const list = page.locator('section.text').getByRole('list', { name: 'Nouvel ordre des articles' });
const names = list.locator('button.item .name');
await expect(names).toHaveText([EF, E1, E2]);
await expect(names).toHaveText([EF, E1, EC, E2]);
// The articles in force of the section are in the list now, not twice; the one only proposed says so.
await expect(page.locator('section.text article.unit', { hasText: E1 })).toHaveCount(0);
await expect(list.locator('li', { hasText: EF })).toContainText('proposé');
await expect(list.locator('.grip')).toHaveCount(3);
await expect(list.locator('.grip')).toHaveCount(4);
const submit = page.getByRole('button', { name: 'Proposer cet ordre' });
await expect(submit).toBeDisabled();
// Off, and it says why (UX-lecteur.md: « le bouton reste éteint et le dit » — lot U2).
@@ -54,43 +56,45 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss
await expect(submit).toHaveAccessibleDescription(why);
await shot(page, 'c35-2-poignees');
// By dragging its handle: Réciprocité onto the place of Unicité du compte.
await list.locator('li', { hasText: E2 }).locator('.grip').dragTo(list.locator('li', { hasText: E1 }));
await expect(names).toHaveText([EF, E2, E1]);
await expect(page.getByText(`« ${E2} » en place 2 sur 3.`)).toBeAttached();
// By dragging its handle: Réciprocité onto the place of the article above it, Responsabilité
// civique (lot T4; a drag over two rows is not announced, see the report of lot T4 for U3).
await list.locator('li', { hasText: E2 }).locator('.grip').dragTo(list.locator('li', { hasText: EC }));
await expect(names).toHaveText([EF, E1, E2, EC]);
await expect(page.getByText(`« ${E2} » en place 3 sur 4.`)).toBeAttached();
await expect(submit).toBeEnabled();
await expect(page.getByText(why)).toHaveCount(0);
// With the keyboard: the article keeps the focus, the arrows move it.
const items = list.locator('button.item');
await items.nth(1).focus();
await items.nth(2).focus();
await page.keyboard.press('ArrowUp');
await expect(names).toHaveText([E2, EF, E1]);
await expect(items.nth(0)).toBeFocused();
await expect(names).toHaveText([EF, E2, E1, EC]);
await expect(items.nth(1)).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(names).toHaveText([EF, E2, E1]);
await expect(names).toHaveText([EF, E1, E2, EC]);
// Without dragging: its arrows, or an article then its new place.
await list.getByRole('button', { name: `Descendre « ${E2} »` }).click();
await expect(names).toHaveText([EF, E1, E2]);
await expect(names).toHaveText([EF, E1, EC, E2]);
await expect(submit).toBeDisabled();
await expect(page.getByText(why)).toBeVisible();
await items.nth(2).click();
await expect(items.nth(2)).toHaveAttribute('aria-pressed', 'true');
await items.nth(3).click();
await expect(items.nth(3)).toHaveAttribute('aria-pressed', 'true');
await items.nth(1).click();
await expect(names).toHaveText([EF, E2, E1]);
await expect(names).toHaveText([EF, E2, E1, EC]);
// « Annuler » gives the section back as it read.
await page.getByRole('button', { name: 'Annuler' }).click();
await expect(list).toHaveCount(0);
await expect(page.locator('section.text article.unit', { hasText: E1 })).toHaveCount(1);
await line.getByRole('button', { name: 'Réordonner' }).click();
await list.getByRole('button', { name: `Monter « ${E2} »` }).click();
await expect(names).toHaveText([EF, E2, E1]);
await list.getByRole('button', { name: `Monter « ${E2} »` }).click();
await expect(names).toHaveText([EF, E2, E1, EC]);
// Then the vote: the order goes to the vote, and its vote unfolds at once, in the same place.
await submit.click();
await expect(page.getByText('Ordre proposé au vote.')).toBeVisible();
const fold = line.locator('section.fold');
await expect(fold).toHaveAttribute('aria-label', `Le vote de « ${ORDER} »`);
await expect(fold.getByRole('list', { name: 'Ordre proposé' }).locator('li')).toHaveText([EF, E2, E1]);
await expect(fold.getByRole('list', { name: 'Ordre proposé' }).locator('li')).toHaveText([EF, E2, E1, EC]);
await expect(line).toContainText('1 ordre proposé');
const pid = ((await fold.getAttribute('id')) ?? '').replace(/^vote-/, '');
await fold.getByRole('button', { name: 'Pour', exact: true }).click();
@@ -257,7 +261,7 @@ test('C38 — l’en-tête compte ce que montre la liste ; un changement d’ine
await page.goto(DOC);
await unfold(page);
const facts = page.locator('.facts');
await expect.soft(facts).toContainText('0 article en vigueur · 3 Propositions seed · ');
await expect.soft(facts).toContainText('0 article en vigueur · 4 Propositions seed · ');
await expect.soft(facts).toContainText(new RegExp(`· ${await listed()} propositions en cours`));
// A change of inertia on the order of a section and on the title (test group): each is found at
+13 -10
View File
@@ -37,7 +37,7 @@ 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 — au départ, aucun texte en vigueur : la raison d’être et les deux engagements, Propositions seed, ouvrent le texte', async ({ page }) => {
test('C3 — au départ, aucun texte en vigueur : la raison d’être et les trois engagements, Propositions seed, ouvrent le texte', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(DOC);
await expect(page.getByRole('heading', { level: 1 })).toHaveText("Acte d'engagement Certification");
@@ -45,20 +45,22 @@ test('C3 — au départ, aucun texte en vigueur : la raison d’être et les deu
// read whole at the head of the text (lot U), each marked: never the bare page of a text in force.
await expect(page.getByText("Aucun article n'est encore en vigueur. Commence par les Propositions seed : elles y entrent par le vote, comme toute proposition.")).toBeVisible();
const seeds = page.locator('section.text article.unit');
await expect(seeds.locator('h2')).toHaveText(["Raison d'être", 'Unicité du compte', 'Réciprocité']);
await expect(seeds.locator('.tag', { hasText: 'Proposition seed' })).toHaveCount(3);
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);
// Lot T4 (Yvv, 2026-11-03): « Responsabilité civique » between the two engagements.
await expect(seeds.locator('h2')).toHaveText(["Raison d'être", 'Unicité du compte', 'Responsabilité civique', 'Réciprocité']);
await expect(seeds.locator('.tag', { hasText: 'Proposition seed' })).toHaveCount(4);
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(4);
// At the low level they start at, how far each is from passing.
for (const seed of await seeds.all()) await expect(seed).toContainText('0 pour · 0 contre · 9 oui sur 12');
for (const seed of await seeds.all()) await expect(seed).toContainText('inertie 2 / 5 · basse');
// Below, the open proposals, folded by section; the seed proposals are not said twice.
await expect(page.getByRole('heading', { name: 'Propositions en cours' })).toBeVisible();
await expect(page.locator('ul.deep li.section')).toHaveCount(8);
// Five sections: formule, inertie and ordonnancement are out of the document (ADR 0014).
await expect(page.locator('ul.deep li.section')).toHaveCount(5);
await expect(page.locator('ul.deep button.row')).toHaveCount(0);
await expect(page.locator('ul.deep li.section', { hasText: 'Engagements techniques' })).toContainText('11 articles');
await unfold(page);
// One line per article: the 33 less the 3 at the head, E3 and E7 holding their two variants inside.
await expect(page.locator('ul.deep button.row')).toHaveCount(30);
// One line per article: the 27 less the 4 at the head, E3 and E7 holding their two variants inside.
await expect(page.locator('ul.deep button.row')).toHaveCount(23);
await expect(page.locator('ul.deep .tag', { hasText: 'Proposition seed' })).toHaveCount(0);
});
@@ -172,7 +174,7 @@ test('C9 — les aperçus : bien cotées, et tout', async ({ page }) => {
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 expect(page.locator('section.text article.unit')).toHaveCount(27);
// 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(0);
@@ -267,8 +269,9 @@ test('C18 — la Raison d’être ouvre le texte, en trois langues', async ({ pa
// 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");
// The text of Yvv of 2026-11-03 (lot T4).
await expect(paragraphs.nth(0).locator('em')).toContainText('Derrière une clé il y a un et un seul être humain vivant');
await expect(paragraphs.nth(1)).toContainText("C'est l'unique enjeu de la certification et de votre engagement");
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');