Lot U : contrôles repris sans les affaiblir (C3, C4, C6, C19 à C21, C23 à C26, C34, M1) ; CONTROLES.md (C35 à C37) ; retouches vues aux captures (ligne d'ordre discrète, en-tête plus court sur téléphone, liste à poignées)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 08:08:28 +01:00
co-authored by Claude Opus 5.5
parent 4cf4e47ee9
commit 5bbcfcdfd5
15 changed files with 208 additions and 151 deletions
+54 -62
View File
@@ -1,22 +1,18 @@
import { devices, expect, test, type Browser, type Page } from '@playwright/test';
import { ALICE, DOC, person, shot, signIn, TEST_DOC, wallet } from './helpers';
import { ALICE, DOC, person, shot, signIn, TEST_DOC, unfold, 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.
// order of its articles (inertia 2); 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). Lot U (Yvv, 2026-11-02): the order is one line at
// the end of its section, never the list of its articles again; « Réordonner » puts handles
// on them in place (C35); every version of a module is voted at its place, never among the
// proposals. 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();
}
/** Whether a version of a module of the test group is in force now. */
async function inForce(page: Page, proposalId: string): Promise<boolean> {
const g = await (await page.request.get('/api/groups/essai')).json();
@@ -27,37 +23,39 @@ async function inForce(page: Page, proposalId: string): Promise<boolean> {
/**
* The voices that bring a module version of the test group into force: hers, then a
* second one if hers was not enough. Every dev sign-in joins the test group: from 16
* members B^W vanishes and, C being 1, one voice may suffice (ADR 0007). Returns the
* number of voices for it.
* members B^W vanishes and, the quorum being 1, one voice may suffice (ADR 0013). Returns
* the number of voices for it. The second one votes at the module, by its address.
*/
async function secondVoice(page: Page, browser: Browser, proposalId: string, row: string, news: string): Promise<number> {
async function secondVoice(page: Page, browser: Browser, proposalId: string, fold: string, news: string): Promise<number> {
if (await inForce(page, proposalId)) {
await expect(page.getByText(news)).toBeVisible();
return 1;
}
const yvv = await person(browser, 'Yvv');
await yvv.goto(TEST_DOC);
await voteFor(yvv, row);
await yvv.goto(`${TEST_DOC}?vote=${proposalId}`);
await yvv.getByRole('region', { name: fold }).getByRole('button', { name: 'Pour', exact: true }).click();
await expect(yvv.getByText(news)).toBeVisible();
return 2;
}
test('C25 — l’ordre des articles : en fin de section, proposé en cliquant, au clavier, voté, en vigueur', async ({ page, browser }) => {
test('C25 — l’ordre des articles : une ligne en fin de section, réordonnée sur place, votée là, 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 unfold(page);
// Each section ends with its order, in one line; a section of one article has nothing to decide.
const modules = page.getByRole('group', { name: /^Ordre des articles · / });
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).toContainText('Ordre de la section');
await expect(order).toContainText('inertie 2 / 5 · basse');
await expect(order.getByText('seed a minima · 0 vote')).toBeVisible();
// Never the list of its articles again: it is read in its vote, or while reordering.
await expect(order.getByText(K1)).toHaveCount(0);
await order.scrollIntoViewIfNeeded();
await shot(page, 'c25-1-ordre-lecture');
// Its vote unfolds under it, like an article's, the list whole (the mark's real number of
// votes is seen in Monnaie libre, below: here one voice may confirm a seed).
// Its vote unfolds under its line, like an article's, the list whole.
const fond = page.getByRole('group', { name: 'Ordre des articles · Engagements fondamentaux' });
await fond.hover();
await fond.getByRole('button', { name: 'Le vote' }).click();
@@ -66,22 +64,21 @@ test('C25 — l’ordre des articles : en fin de section, proposé en cliquant,
await expect(fondVote.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText(['Engagements fondamentaux', 'Unicité du compte', 'Réciprocité']);
await fond.getByRole('button', { name: 'Le vote' }).click();
// Another order: the article, then its new place.
// Another order: « Réordonner », the article, then its new place — in place, no panel.
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' });
await order.getByRole('button', { name: 'Réordonner' }).click();
await expect(page.locator('aside.panel')).toHaveCount(0);
const list = order.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' });
const submit = order.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(order.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();
@@ -91,38 +88,42 @@ test('C25 — l’ordre des articles : en fin de section, proposé en cliquant,
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 order.getByRole('button', { name: `Descendre « ${K2} »` }).click();
await order.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');
// Its vote unfolds at the line, the list it proposes in sight; never a line among the proposals.
const fold = order.locator('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 expect(fold.getByRole('list', { name: 'Ordre proposé' }).locator('li')).toHaveText([K2, K1]);
await expect(order).toContainText('1 ordre proposé');
await expect(page.locator('ul.deep button.row', { hasText: 'Ordre des articles' })).toHaveCount(0);
// The order in force is read from it, among its other versions.
await expect(fold.getByRole('region', { name: 'Autres versions' })).toContainText('Ordre en vigueur');
const pid = ((await fold.getAttribute('id')) ?? '').replace(/^vote-/, '');
await fold.getByRole('button', { name: 'Pour', exact: true }).click();
await expect(page.getByText('Vote enregistré.')).toBeVisible();
await shot(page, 'c25-3-ordre-vote');
// Into force (her voice, or a second one): the section reads in its new order.
const voices = await secondVoice(page, browser, pid, 'Ordre des articles · Conclusion', 'Le nouvel ordre de « Conclusion » est entré en vigueur.');
const voices = await secondVoice(page, browser, pid, 'Le vote de « Ordre des articles · Conclusion »', 'Le nouvel ordre de « Conclusion » est entré en vigueur.');
// In the basket too: at the end of its section.
await page.getByRole('button', { name: /^Voter en lot/ }).click();
const panel = page.locator('aside.panel');
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();
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.
// In force now: its vote stays unfolded under its line, the list whole, in the new order.
await expect(order.locator('section.fold')).toContainText(`${voices} pour · 0 contre`);
await expect(order.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText([K2, K1]);
await order.scrollIntoViewIfNeeded();
await shot(page, 'c25-3b-ordre-en-vigueur');
await expect(order).toContainText(/adopté le/);
await unfold(page);
const rows = await page.locator('ul.deep button.row .title').allTextContents();
expect(rows.indexOf(K2)).toBeLessThan(rows.indexOf(K1));
});
@@ -131,12 +132,14 @@ test('C26 — le titre du document : proposé et voté en tête, ses traductions
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');
const bar = head.getByRole('group', { name: 'Titre du document' });
await expect(bar).toContainText('Titre du document · seed a minima · 0 vote');
await expect(bar).toContainText('inertie 4 / 5 · haute');
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');
await expect(panel.getByRole('heading', { name: 'Proposer un autre titre' })).toBeVisible();
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' });
@@ -147,13 +150,15 @@ test('C26 — le titre du document : proposé et voté en tête, ses traductions
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');
// Its vote unfolds under the title, where it is read; never a line among the proposals.
const fold = bar.locator('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 expect(bar).toContainText('1 titre proposé');
await expect(page.locator('ul.deep').getByText(next)).toHaveCount(0);
const pid = ((await fold.getAttribute('id')) ?? '').replace(/^vote-/, '');
await fold.getByRole('button', { name: 'Pour', exact: true }).click();
await expect(page.getByText('Vote enregistré.')).toBeVisible();
const voices = await secondVoice(page, browser, pid, next, `Le titre « ${next} » est entré en vigueur.`);
const voices = await secondVoice(page, browser, pid, `Le vote de « ${next} »`, `Le titre « ${next} » est entré en vigueur.`);
// The document changes title; its translations stay editorial, to review.
await page.reload();
@@ -194,7 +199,7 @@ test('C27 — la participation ramenée dans ses bornes : la mesure, puis ce que
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 }) => {
test('C25 — sur la chaîne Ğ1 et sur un téléphone : l’ordre se vote dans le panier, puis se lit à sa ligne', 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' });
@@ -219,23 +224,10 @@ test('C25 — sur la chaîne Ğ1 et sur un téléphone : l’ordre se vote dans
);
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.
// The title on a phone: its gestures in sight, without a pointer to hover with. Reordering on a
// phone: C35.
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();
});