Files
decision/web/e2e/modules.spec.ts
T
YvvandClaude Opus 5.5 785580c78f v3 : modules d'ordre et de titre — l'interface, restes mineurs (L11, C25–C27)
Chaque section du lecteur finit par l'ordre de ses articles (inertie 2) : ses
articles numérotés, son vote déplié, « Proposer un autre ordre » — une liste où
l'on clique un article puis sa place, ses flèches, ou le clavier. Le titre du
document (inertie 4) se vote et se propose sous le titre, « traduction à
revoir » quand il a changé. Les deux sont dans la liste des propositions, le
panier et la chaîne Ğ1 ; un nouvel article se propose dans une section, à sa fin.
La liste du groupe compte les propositions ouvertes des modules.

Restes mineurs : la participation ramenée se dit en deux temps (la mesure, puis
ce que le groupe demande), les lignes « habituelle » des réglages resserrées, le
sélecteur d'inertie se parcourt aux flèches (le cran actuel atteignable, jamais
choisi), la marque « seed a minima » porte le vrai nombre de votes.

Contrôles : C25 (ordre, dont la chaîne et le téléphone), C26 (titre), C27
(participation ramenée) ; C3, C17 (41 modules), C20 mis à jour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:35:02 +01:00

226 lines
13 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 { devices, expect, test, type Page } from '@playwright/test';
import { ALICE, DOC, person, shot, signIn, TEST_DOC, wallet } from './helpers';
// Lot D — the modules (Yvv, 2026-10-30; ADR 0009): at the end of each section, the
// order of its articles (inertia 2), proposed with a list one clicks; at the head of
// the document, its title (inertia 4). Both voted like an article: unfolded, in the
// basket, on the Ğ1 chain in Monnaie libre (simulated here: e2e/chain-mock.mjs).
// The server is shared with the specs that follow: the test group keeps its articles
// and their votes; its conclusion is reordered and its title changes.
const K1 = 'Règles abrégées de la toile de confiance';
const K2 = 'Monnaie G1';
/** A vote unfolded in the list of proposals: « Pour », then the news. */
async function voteFor(page: Page, row: string) {
await page.locator('ul.deep button.row', { hasText: row }).click();
await page.locator('ul.deep section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
}
test('C25 — l’ordre des articles : en fin de section, proposé en cliquant, au clavier, voté, en vigueur', async ({ page, browser }) => {
await signIn(page, 'Chiara');
await page.goto(TEST_DOC);
// Each section ends with its order; a section of one article has nothing to decide.
const modules = page.locator('section.text [role="group"]');
await expect(modules).toHaveCount(7);
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.locator(':scope > ol li')).toHaveText([`1 ${K1}`, `2 ${K2}`]);
await expect(order).toContainText('2 articles · inertie 2 / 5 · basse');
await expect(order.getByText('seed a minima · 0 vote')).toBeVisible();
await order.scrollIntoViewIfNeeded();
await shot(page, 'c25-1-ordre-lecture');
// Its vote unfolds under it, like an article's; the mark tells the real number of votes.
const fond = page.getByRole('group', { name: 'Ordre des articles · Engagements fondamentaux' });
await fond.hover();
await fond.getByRole('button', { name: 'Le vote' }).click();
const fondVote = page.getByRole('region', { name: 'Le vote de « Ordre des articles · Engagements fondamentaux »' });
await expect(fondVote.getByText('inertie 2 / 5 · basse', { exact: true })).toBeVisible();
await expect(fondVote.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText(['Engagements fondamentaux', 'Unicité du compte', 'Réciprocité']);
await fondVote.getByRole('button', { name: 'Pour', exact: true }).click();
await expect(fond.getByText('seed a minima · 1 vote')).toBeVisible();
await fondVote.getByRole('button', { name: 'Retirer mon vote' }).click();
await expect(fond.getByText('seed a minima · 0 vote')).toBeVisible();
await fond.getByRole('button', { name: 'Le vote' }).click();
// Another order: the article, then its new place.
await order.hover();
await order.getByRole('button', { name: 'Proposer un autre ordre' }).click();
await expect(page).toHaveURL(/onglet=proposer/);
const panel = page.locator('aside.panel');
const list = panel.getByRole('list', { name: 'Nouvel ordre des articles' });
const items = list.locator('button.item');
const names = list.locator('button.item .name');
await expect(names).toHaveText([K1, K2]);
const submit = panel.getByRole('button', { name: 'Proposer cet ordre' });
await expect(submit).toBeDisabled();
await items.nth(1).click();
await expect(items.nth(1)).toHaveAttribute('aria-pressed', 'true');
await items.nth(0).click();
await expect(names).toHaveText([K2, K1]);
await expect(panel.getByText(`« ${K2} » en place 1 sur 2.`)).toBeAttached();
await expect(submit).toBeEnabled();
// With the keyboard: the moved article keeps the focus, the arrows move it.
await expect(items.nth(0)).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(names).toHaveText([K1, K2]);
await expect(submit).toBeDisabled();
await page.keyboard.press('ArrowUp');
await expect(names).toHaveText([K2, K1]);
// Or its arrows.
await panel.getByRole('button', { name: `Descendre « ${K2} »` }).click();
await panel.getByRole('button', { name: `Monter « ${K2} »` }).click();
await expect(names).toHaveText([K2, K1]);
await shot(page, 'c25-2-ordre-propose');
await submit.click();
await expect(page.getByText('Ordre proposé au vote.')).toBeVisible();
// Its vote unfolds in the list of proposals, the list it proposes in sight.
const fold = page.locator('ul.deep section.fold');
await expect(fold).toHaveAttribute('aria-label', 'Le vote de « Ordre des articles · Conclusion »');
await expect(fold.locator('ol.order li')).toHaveText([K2, K1]);
await expect(page.locator('ul.deep li.section', { hasText: 'Conclusion' })).toBeVisible();
await expect(page.locator('ul.deep button.row', { hasText: 'Ordre des articles · Conclusion' })).toContainText(`${K2} · ${K1}`);
await fold.getByRole('button', { name: 'Pour', exact: true }).click();
await expect(page.getByText('Vote enregistré.')).toBeVisible();
await shot(page, 'c25-3-ordre-vote');
// In the basket too: at the end of its section.
await page.getByRole('button', { name: /^Voter en lot/ }).click();
const basket = panel.getByRole('region', { name: 'Conclusion' });
await expect(basket.locator('li.slot').last()).toContainText('Ordre des articles · Conclusion');
await panel.getByRole('button', { name: 'Fermer le panneau' }).click();
// A second voice brings it into force: the section reads in its new order.
const yvv = await person(browser, 'Yvv');
await yvv.goto(TEST_DOC);
await voteFor(yvv, 'Ordre des articles · Conclusion');
await expect(yvv.getByText('Le nouvel ordre de « Conclusion » est entré en vigueur.')).toBeVisible();
await page.reload();
await expect(order.locator(':scope > ol li')).toHaveText([`1 ${K2}`, `2 ${K1}`]);
// In force now: its vote stays unfolded, under the module, the list whole.
await expect(order.locator('section.fold')).toContainText('2 pour · 0 contre');
await order.scrollIntoViewIfNeeded();
await shot(page, 'c25-3b-ordre-en-vigueur');
await expect(order).toContainText(/adopté le/);
const rows = await page.locator('ul.deep button.row .title').allTextContents();
expect(rows.indexOf(K2)).toBeLessThan(rows.indexOf(K1));
});
test('C26 — le titre du document : proposé et voté en tête, ses traductions à revoir', async ({ page, browser }) => {
await signIn(page, 'Chiara');
await page.goto(TEST_DOC);
const head = page.locator('header.head');
const bar = head.locator('.title-unit');
await expect(bar).toContainText('Titre du document · inertie 4 / 5 · haute · seed a minima · 0 vote');
await head.hover();
await bar.getByRole('button', { name: 'Proposer un autre titre' }).click();
await expect(page).toHaveURL(/onglet=proposer/);
const panel = page.locator('aside.panel');
const input = panel.getByRole('textbox', { name: 'Nouveau titre' });
await expect(input).toHaveValue("Acte d'engagement Certification");
const submit = panel.getByRole('button', { name: 'Proposer un autre titre' });
await expect(submit).toBeDisabled();
await expect(panel.getByText("Le titre du document se vote à l'inertie 4 / 5 · haute.", { exact: false })).toBeVisible();
const next = "Acte d'engagement de la certification";
await input.fill(next);
await shot(page, 'c26-1-titre-propose');
await submit.click();
await expect(page.getByText('Titre proposé au vote.')).toBeVisible();
const fold = page.locator('ul.deep section.fold');
await expect(fold).toHaveAttribute('aria-label', `Le vote de « ${next} »`);
await expect(page.locator('ul.deep button.row', { hasText: next })).toContainText('Titre du document');
await fold.getByRole('button', { name: 'Pour', exact: true }).click();
await expect(page.getByText('Vote enregistré.')).toBeVisible();
const yvv = await person(browser, 'Yvv');
await yvv.goto(TEST_DOC);
await voteFor(yvv, next);
await expect(yvv.getByText(`Le titre « ${next} » est entré en vigueur.`)).toBeVisible();
// The document changes title; its translations stay editorial, to review.
await page.reload();
await expect(page.getByRole('heading', { level: 1 })).toHaveText(next);
await expect(bar).toContainText(/adopté le/);
await expect(bar.locator('.tag')).toHaveCount(0);
// In force now: its vote stays unfolded, under the title.
const vote = bar.locator('section.fold');
await expect(bar.getByRole('button', { name: 'Le vote' })).toHaveAttribute('aria-expanded', 'true');
await expect(vote.getByText('inertie 4 / 5 · haute', { exact: true })).toBeVisible();
await expect(vote.getByText('2 pour · 0 contre')).toBeVisible();
await shot(page, 'c26-2-titre-vote');
await bar.getByRole('button', { name: 'Le vote' }).click();
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(bar.locator('.tag')).toHaveText('translation needs review');
await shot(page, 'c26-3-titre-traduction-a-revoir');
});
test('C27 — la participation ramenée dans ses bornes : la mesure, puis ce que le groupe demande', async ({ page }) => {
await signIn(page, 'Chiara');
// A group of four whose median is 2,5 voters, brought back to half of it by the scale.
await page.route(/\/api\/docs\/[^/?]+$/, async (route) => {
const res = await route.fetch();
const body = await res.json();
body.doc.electorate = 4;
body.doc.participation = { ...body.doc.participation, source: 'median', median: 2.5, sample: 12, bounded: true, voters: 2, ratio: 0.5, bounds: [0.05, 0.5] };
await route.fulfill({ response: res, json: body });
});
await page.goto(TEST_DOC);
await page.getByRole('button', { name: 'Réglages de la formule' }).click();
const panel = page.locator('aside.panel');
// The measure as it is: 2,5 of 4 is 62,5 %, never 3 of 4 rounded first.
await expect(panel.getByText(/^3 votants sur 4 \(62,5\s%\) : la médiane des 12 propositions votées depuis un an\.$/)).toBeVisible();
// Then what the group asks, the group as subject.
await expect(panel.getByText(/^Ramenée entre 5\s% et 50\s% de l'électorat : le groupe demande ses voix pour comme si 2 personnes votaient \(50\s%\)\.$/)).toBeVisible();
await expect(panel.getByText("Part exigée : les « pour » demandés au-delà de C, à 2 votants comme d'habitude.")).toBeVisible();
await shot(page, 'c27-participation-bornee');
});
test('C25 — sur la chaîne Ğ1 et sur un téléphone : l’ordre se vote dans le panier, la liste tient', async ({ browser }) => {
// The chain is waited for: the server reads the indexer every 10 seconds.
test.setTimeout(90_000);
const ctx = await browser.newContext({ ...devices['Pixel 7'], locale: 'fr-FR' });
const page = await ctx.newPage();
await signIn(page, 'Chiara');
await wallet(page, [ALICE]);
await page.goto(DOC);
// The basket of Monnaie libre: the order of the fundamental commitments, alone, in one transaction.
await page.getByRole('button', { name: /^Voter en lot/ }).click();
const panel = page.locator('aside.panel');
await panel.getByRole('group', { name: 'Ton choix pour « Ordre des articles · Engagements fondamentaux »' }).getByRole('button', { name: 'Pour' }).click();
await panel.getByRole('button', { name: 'Signer et envoyer (1)' }).click();
const dialog = page.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' });
await expect(dialog.getByText('Ton vote compte : il est inscrit sur la chaîne Ğ1.')).toBeVisible({ timeout: 20_000 });
await dialog.getByRole('button', { name: 'Fermer' }).click();
await panel.getByRole('button', { name: 'Fermer le panneau' }).click();
const fond = page.getByRole('group', { name: 'Ordre des articles · Engagements fondamentaux' });
await expect(fond.getByText('seed a minima · 1 vote')).toBeVisible();
await fond.getByRole('button', { name: 'Le vote' }).click();
await expect(page.getByRole('region', { name: 'Le vote de « Ordre des articles · Engagements fondamentaux »' }).locator('.voted')).toHaveText(
'Ton vote : pour · inscrit sur la chaîne Ğ1',
);
await fond.scrollIntoViewIfNeeded();
await shot(page, 'c25-4-ordre-mobile');
// The title on a phone: its gestures in sight, without a pointer to hover with.
await page.goto(TEST_DOC);
await expect(page.locator('.title-unit .links')).toHaveCSS('opacity', '1');
await shot(page, 'c26-4-titre-mobile');
// The list one clicks, on a phone: nothing overflows, the buttons are big enough to touch.
const tech = page.getByRole('group', { name: 'Ordre des articles · Engagements techniques' });
await tech.getByRole('button', { name: 'Proposer un autre ordre' }).click();
const items = panel.getByRole('list', { name: 'Nouvel ordre des articles' }).locator('button.item');
await expect(items).toHaveCount(11);
await items.nth(3).click();
await items.nth(0).click();
await expect(panel.getByRole('button', { name: 'Proposer cet ordre' })).toBeEnabled();
const holds = await panel.locator('.body').evaluate((el) => el.scrollWidth <= el.clientWidth + 1);
expect(holds).toBe(true);
const box = await items.nth(0).boundingBox();
expect(box?.height ?? 0).toBeGreaterThanOrEqual(44);
await shot(page, 'c25-5-ordre-propose-mobile');
await page.getByRole('heading', { level: 1 }).scrollIntoViewIfNeeded();
await ctx.close();
});