From 6fb82382c0a8b2a1b642e9e608aad83b4c5d71ba Mon Sep 17 00:00:00 2001 From: Yvv Date: Tue, 3 Nov 2026 08:19:15 +0100 Subject: [PATCH] =?UTF-8?q?Lot=20U=20:=20=C2=AB=20Autres=20versions=20?= =?UTF-8?q?=C2=BB=20repli=C3=A9es=20dans=20le=20vote=20;=20contr=C3=B4les?= =?UTF-8?q?=20corrig=C3=A9s=20apr=C3=A8s=20le=20premier=20passage=20(?= =?UTF-8?q?=C3=A9tat=20partag=C3=A9,=20nom=20du=20titre=20en=20anglais,=20?= =?UTF-8?q?attente=20du=20chargement)=20;=20le=20panier=20dit=20=C2=AB=20s?= =?UTF-8?q?imul=C3=A9=20=C2=BB=20;=20CLAUDE.md=20(composants,=20panneau,?= =?UTF-8?q?=2038=20contr=C3=B4les)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 10 +++--- web/e2e/bulletins.spec.ts | 1 + web/e2e/mobile.spec.ts | 3 +- web/e2e/modules.spec.ts | 9 ++++-- web/e2e/ordre-quorum.spec.ts | 3 +- web/e2e/reader.spec.ts | 5 +-- web/e2e/vote.spec.ts | 8 +++-- web/src/lib/components/BallotPanel.svelte | 2 +- web/src/lib/components/VoteFold.svelte | 37 ++++++++++++++++++++--- web/src/lib/i18n/en.ts | 1 + web/src/lib/i18n/es.ts | 1 + web/src/lib/i18n/fr.ts | 1 + 12 files changed, 61 insertions(+), 20 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 388636c..771e658 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,7 +80,8 @@ web/src/ lib/i18n/ # fr.ts SOURCE (types Key) · en.ts · es.ts — parité testée lib/components/ # Sidebar, DocPanel, VoteFold (le vote déplié sous l'article), BallotPanel (panier), # ChainSign (signature, Ğecko), VersionDetails, ProposeView, UnitBlock, DeepList, - # OrderModule / OrderEditor (l'ordre d'une section, la liste où l'on clique), + # OrderLine / Reorder (l'ordre d'une section : une ligne en fin de section, les poignées + # sur ses articles, sur place), QuorumSetting (le quorum de l'auteur), Crans (l'inertie dessinée), # TitleBar / TitlePropose (le titre du document), Seal… lib/wallet/ # injected.ts (portefeuille injectedWeb3, polkadot-api chargé à la signature) · gecko.ts lib/basket.ts # PUR : sections, tout pour / tout contre, versions en concurrence, versions retenues, modules @@ -88,11 +89,12 @@ web/src/ lib/order.ts # PUR : placer, monter / descendre, ce qui a bougé routes/ # / (accueil + choix du groupe) · connexion · invitation · equipe · g/[group] (piliers, en cours) · # g/[group]/docs · g/[group]/docs/[doc] (lecteur, - # vote déplié ?vote=, panneau ?onglet=panier|propositions|proposer|details|reglages) + # vote déplié ?vote=, panneau sans onglets ?onglet=panier|proposer|details|reglages ; + # l'ancien onglet=propositions déplie le vote ; note : docs/chantiers/acte-certification/UX-lecteur.md) # · g/[group]/decisions · g/[group]/mandats · # g/[group]/protocoles (à venir) · acces (trustinWallet) · messages (la boîte) ; # /groupes, /decisions… redirigent -web/e2e/ # Playwright C1–C34 + M1 ; serve.sh = serveur jetable /tmp/ld-e2e sur :8092 et +web/e2e/ # Playwright C1–C37 + M1 ; serve.sh = serveur jetable /tmp/ld-e2e sur :8092 et # chain-mock.mjs (nœud et indexeur Ğ1 simulés) ; faux portefeuille dans helpers.ts docker/ # app.Dockerfile · app-compose.yml (sonic) .woodpecker/ # app.yml · maquette.yml @@ -114,7 +116,7 @@ maquette/ # v2 intacte (frontend Nuxt, backend FastAPI, docker, docs-v ```bash export PATH=$HOME/.cargo/bin:$PATH cargo fmt --all --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace -cd web && npm run check && npm test && npm run build && npm run e2e # e2e : 35 contrôles (36 tests, C25 en deux) +cd web && npm run check && npm test && npm run build && npm run e2e # e2e : 38 contrôles (40 tests, C25 et C35 en deux) LD_DEV_LOGIN=1 cargo run -p ld-server # :8002, sert web/build cd web && npm run dev # :3002 à chaud diff --git a/web/e2e/bulletins.spec.ts b/web/e2e/bulletins.spec.ts index bd5affa..a9660bd 100644 --- a/web/e2e/bulletins.spec.ts +++ b/web/e2e/bulletins.spec.ts @@ -14,6 +14,7 @@ const mine = async (page: Page): Promise => (await (await page.request. /** Unfolds the vote of an open proposal, from « Propositions en cours » (its sections folded: lot U). */ async function unfold(page: Page, title: string) { + await page.locator('ul.deep li.section').first().waitFor(); const all = page.getByRole('button', { name: 'Tout déplier' }); if (await all.isVisible()) await all.click(); // By its title only: the excerpt of another proposal may name it. diff --git a/web/e2e/mobile.spec.ts b/web/e2e/mobile.spec.ts index bb3177b..cb7e0de 100644 --- a/web/e2e/mobile.spec.ts +++ b/web/e2e/mobile.spec.ts @@ -87,7 +87,8 @@ test('M1 — mobile : logo dans la barre, l’accueil sous la voûte, menu en ti expect((await toggle.boundingBox())?.height ?? 0).toBeGreaterThanOrEqual(44); await toggle.click(); const fold = page.locator('section.text section.fold'); - await expect(fold.getByText('9 oui sur 12 votants pour passer', { exact: true })).toBeVisible(); + // Level 2, the quorum 12: « sur 12 votants » before any vote; the desktop controls may have voted. + await expect(fold.locator('.need')).toHaveText(/^9 oui (sur 12 votants pour passer|pour passer · le quorum la bloque)$/); // The settings of the formula read on a phone: the table of the five levels holds in the sheet. await fold.getByRole('button', { name: 'Réglages de la formule' }).click(); const panel = page.locator('aside.panel'); diff --git a/web/e2e/modules.spec.ts b/web/e2e/modules.spec.ts index 2d346f5..4c10d14 100644 --- a/web/e2e/modules.spec.ts +++ b/web/e2e/modules.spec.ts @@ -100,8 +100,9 @@ test('C25 — l’ordre des articles : une ligne en fin de section, réordonnée await expect(fold).toHaveAttribute('aria-label', 'Le vote de « Ordre des articles · Conclusion »'); 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(page.locator('ul.deep button.row .title', { hasText: /^Ordre des articles · / })).toHaveCount(0); + // The order in force is read from it, among its other versions (folded until asked). + await fold.getByRole('button', { name: 'Autres versions (1)' }).click(); 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(); @@ -132,7 +133,9 @@ 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.getByRole('group', { name: 'Titre du document' }); + // A module, named for what it is (in French here; the language changes below). + await expect(head.getByRole('group', { name: 'Titre du document' })).toBeVisible(); + const bar = head.locator('.title-unit'); await expect(bar).toContainText('Titre du document · seed a minima · 0 vote'); await expect(bar).toContainText('inertie 4 / 5 · haute'); await head.hover(); diff --git a/web/e2e/ordre-quorum.spec.ts b/web/e2e/ordre-quorum.spec.ts index f220928..f30c60b 100644 --- a/web/e2e/ordre-quorum.spec.ts +++ b/web/e2e/ordre-quorum.spec.ts @@ -32,7 +32,7 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss await expect(line.getByText(E1)).toHaveCount(0); // Nowhere a block that repeats the articles of a section, and no order among the proposals. await expect(page.locator('section.text').getByText('Ordre des articles ·', { exact: false })).toHaveCount(0); - await expect(page.locator('ul.deep').getByText('Ordre des articles', { exact: false })).toHaveCount(0); + await expect(page.locator('ul.deep').getByText(/^Ordre des articles · /)).toHaveCount(0); await line.scrollIntoViewIfNeeded(); await shot(page, 'c35-1-ligne-ordre'); @@ -99,6 +99,7 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss } expect(await moduleInForce(page, pid)).toBe(true); await page.goto(TEST_DOC); + await expect(page.locator('section.text article.unit', { hasText: E2 })).toBeVisible(); const titles = await page.locator('section.text article.unit h2').allTextContents(); expect(titles.indexOf(E2)).toBeGreaterThan(-1); expect(titles.indexOf(E2)).toBeLessThan(titles.indexOf(E1)); diff --git a/web/e2e/reader.spec.ts b/web/e2e/reader.spec.ts index 3c0c1d7..0c10a7a 100644 --- a/web/e2e/reader.spec.ts +++ b/web/e2e/reader.spec.ts @@ -295,8 +295,9 @@ test('C19 — un article, une ligne : ses versions se déplient', async ({ page // A version unfolds its vote below it, as any proposal. await versions.nth(1).click(); await expect(versions.nth(1)).toHaveAttribute('aria-expanded', 'true'); - // Level 3, the quorum 50 (ADR 0013): 39 « pour » at no vote, the voters missing counted as « non ». - await expect(page.locator('section.fold').getByText('39 oui sur 50 votants pour passer', { exact: true })).toBeVisible(); + // Level 3, the quorum 50 (ADR 0013): 39 « oui » pass it, the voters missing counted as « non » + // (« sur 50 votants » before any vote; a « contre » of another control may be there already). + await expect(page.locator('section.fold .need')).toHaveText(/^39 oui (sur 50 votants pour passer|pour passer · le quorum la bloque)$/); await expect(page).toHaveURL(/vote=/); await e3.click(); await expect(e3).toHaveAttribute('aria-expanded', 'false'); diff --git a/web/e2e/vote.spec.ts b/web/e2e/vote.spec.ts index bd7f9dd..2126685 100644 --- a/web/e2e/vote.spec.ts +++ b/web/e2e/vote.spec.ts @@ -20,7 +20,8 @@ test('C21 — le vote déplié sous l’article : ton vote, les totaux, le seuil await expect(toggle).toHaveAttribute('aria-expanded', 'true'); await expect(toggle).toHaveAttribute('aria-controls', (await fold.getAttribute('id')) ?? ''); // How far the threshold is: the water, the totals, the votes asked and those missing. - await expect(fold.getByRole('img', { name: /Niveau : \d+ % du seuil/ })).toBeVisible(); + // (the first: its other versions, inside, have theirs) + await expect(fold.getByRole('img', { name: /Niveau : \d+ % du seuil/ }).first()).toBeVisible(); await expect(fold.getByText(/^\d+ oui sur \d+ votants? pour passer$/)).toBeVisible(); await expect(fold.getByText(/^Il manque \d+ voix pour\.$/)).toBeVisible(); await expect(fold.getByText("Tu n'as pas voté sur cette version.")).toBeVisible(); @@ -196,8 +197,9 @@ test('C24 — sans compte de vote, le vote par la chaîne est fermé et le dit ; const fold = page.locator('section.text section.fold'); await expect(fold.getByText("Le vote par la chaîne Ğ1 n'est pas encore ouvert : le compte qui reçoit les votes reste à créer.", { exact: false })).toBeVisible(); await expect(fold.getByRole('button', { name: 'Pour', exact: true })).toHaveCount(0); - // The totals and how far it is stay in sight: Raison d'être, level 2, the quorum 12 (ADR 0013). - await expect(fold.getByText('9 oui sur 12 votants pour passer', { exact: true })).toBeVisible(); + // The totals and how far it is stay in sight: Raison d'être, level 2, the quorum 12 (ADR 0013) — + // « sur 12 votants » before any vote; C23 may have counted a voice already. + await expect(fold.locator('.need')).toHaveText(/^9 oui (sur 12 votants pour passer|pour passer · le quorum la bloque)$/); await fold.getByText('Ton vote ne se montre à personne', { exact: false }).scrollIntoViewIfNeeded(); await shot(page, '6-vote-ferme'); // The basket, opened by its address, says the vote is closed — never « Tes votes comptent dès l'envoi ». diff --git a/web/src/lib/components/BallotPanel.svelte b/web/src/lib/components/BallotPanel.svelte index 092c8c2..04e9af5 100644 --- a/web/src/lib/components/BallotPanel.svelte +++ b/web/src/lib/components/BallotPanel.svelte @@ -205,7 +205,7 @@ {#if open}

- {#if access?.way === 'chain' && access.chain}{t('basket.note.chain', { amount: g1Amount(access.chain.amount) })}{:else}{t('basket.note.server')}{/if} + {#if access?.way === 'chain' && access.chain}{t('basket.note.chain', { amount: g1Amount(access.chain.amount) })}{:else if access?.simulated}{t('vote.simulated.note')}{:else}{t('basket.note.server')}{/if}

{/if} {#if error}{/if} diff --git a/web/src/lib/components/VoteFold.svelte b/web/src/lib/components/VoteFold.svelte index 92c852d..201424f 100644 --- a/web/src/lib/components/VoteFold.svelte +++ b/web/src/lib/components/VoteFold.svelte @@ -58,6 +58,8 @@ } let detail = $state(null); + /** « Autres versions », folded until asked: the vote stays short. */ + let othersOpen = $state(false); let busy = $state(false); let error = $state(''); let root: HTMLElement | undefined = $state(); @@ -249,8 +251,12 @@
{#if others.length} -
-

{t('vote.others')}

+ + {/if} + {#if others.length && othersOpen} +
    {#each others as v (v.id)}
  • @@ -331,10 +337,28 @@ gap: 0.35rem; min-width: 0; } - .others h4 { - margin: 0; + .disclose { + justify-self: start; + display: inline-flex; + align-items: center; + gap: 0.45rem; + min-height: 2.25rem; + padding: 0; + border: 0; + background: none; + color: var(--water); + font-weight: 750; font-size: 0.92rem; - font-weight: 800; + } + .tri { + width: 0; + height: 0; + border-block: 0.3rem solid transparent; + border-left: 0.45rem solid currentColor; + transition: transform 0.15s var(--ease); + } + .disclose[aria-expanded='true'] .tri { + transform: rotate(90deg); } .others ul { list-style: none; @@ -496,5 +520,8 @@ .dot { animation: none; } + .tri { + transition: none; + } } diff --git a/web/src/lib/i18n/en.ts b/web/src/lib/i18n/en.ts index 3773e1a..be653d4 100644 --- a/web/src/lib/i18n/en.ts +++ b/web/src/lib/i18n/en.ts @@ -241,6 +241,7 @@ export const en: Messages = { 'vote.saved.simulated': 'Vote simulated.', 'vote.simulated.mark': 'simulated vote', 'vote.others': 'Other versions', + 'vote.others.count': 'Other versions ({n})', 'vote.need': '{yes} yes to pass · {no} no to block', 'vote.need.quorum': '{yes} yes to pass · the quorum blocks it', 'vote.need.first.one': '{yes} yes out of {quorum} voter to pass', diff --git a/web/src/lib/i18n/es.ts b/web/src/lib/i18n/es.ts index 09d5f99..b4e148c 100644 --- a/web/src/lib/i18n/es.ts +++ b/web/src/lib/i18n/es.ts @@ -241,6 +241,7 @@ export const es: Messages = { 'vote.saved.simulated': 'Voto simulado.', 'vote.simulated.mark': 'voto simulado', 'vote.others': 'Otras versiones', + 'vote.others.count': 'Otras versiones ({n})', 'vote.need': '{yes} sí para aprobar · {no} no para bloquear', 'vote.need.quorum': '{yes} sí para aprobar · el quórum la bloquea', 'vote.need.first.one': '{yes} sí de {quorum} votante para aprobar', diff --git a/web/src/lib/i18n/fr.ts b/web/src/lib/i18n/fr.ts index 26b8148..c77ee41 100644 --- a/web/src/lib/i18n/fr.ts +++ b/web/src/lib/i18n/fr.ts @@ -245,6 +245,7 @@ export const fr = { 'vote.saved.simulated': 'Vote simulé.', 'vote.simulated.mark': 'vote simulé', 'vote.others': 'Autres versions', + 'vote.others.count': 'Autres versions ({n})', 'vote.need': '{yes} oui pour passer · {no} non pour bloquer', 'vote.need.quorum': '{yes} oui pour passer · le quorum la bloque', 'vote.need.first.one': '{yes} oui sur {quorum} votant pour passer',