forked from yvv/decision
v3 : le vote déplié sous l'article, le panier, la signature par le portefeuille Ğ1
- Le vote se déplie sous l'article, façon accordéon, l'URL pour état (?vote=) :
niveau d'eau, totaux, seuil et voix manquantes, ton vote et où il en est
(enregistré, inscrit sur la chaîne, en attente, à reconfirmer, non compté),
inertie et formule. Le reste d'une version (coquilles, traductions,
signalements, historique) passe dans l'onglet « Détails » du panneau.
- Le panier : article par article, « tout pour / tout contre » par section ou
pour tout le document, jamais pour un article à plusieurs versions ; un seul
geste (une écriture, ou une transaction).
- La signature : le portefeuille de la page (injectedWeb3), polkadot-api chargé
au moment de signer ; sans portefeuille, Ğecko par lien et QR ; la page
attend la confirmation. Un bulletin que le portefeuille n'a pas signé est
lâché aussitôt (DELETE /api/ballots/{id}).
- Pastilles d'ambiance : flèches dans le groupe, anneau de focus pointillé à
la couleur d'accent de la page (3:1 au moins), distinct du choix.
- e2e : chaîne simulée (indexeur et nœud), faux portefeuille ; C21–C24, et
C4, C5, C6, C11, C17, C19, C20, M1 mis à jour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+56
-33
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { DOC, person, signIn, TEST_DOC } from './helpers';
|
||||
import { BOB, DOC, person, signIn, TEST_DOC, wallet } from './helpers';
|
||||
|
||||
test('C1 — l’accueil sans session : le titre, le leitmotiv en trois lignes, la connexion, le menu complet', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -49,24 +49,29 @@ test('C3 — le texte en vigueur : raison d’être et deux engagements, seed a
|
||||
await expect(page.locator('ul.deep button.row')).toHaveCount(30);
|
||||
});
|
||||
|
||||
test('C4 — les actions sont contextuelles, le vote s’ouvre en panneau', async ({ page }) => {
|
||||
test('C4 — les actions sont contextuelles, le vote se déplie sous l’article', async ({ page }) => {
|
||||
await signIn(page, 'Chiara');
|
||||
await page.goto(DOC);
|
||||
const first = page.locator('section.text article.unit').first();
|
||||
await first.hover();
|
||||
await first.getByRole('button', { name: 'Le vote' }).click();
|
||||
const panel = page.locator('aside.panel');
|
||||
await expect(panel.getByText('Seuil : 50 voix pour')).toBeVisible();
|
||||
await expect(panel.getByText('Il manque 50 voix pour.')).toBeVisible();
|
||||
await panel.getByText('La formule du seuil').click();
|
||||
await expect(panel.getByText(/W — électorat/)).toBeVisible();
|
||||
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 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(panel.getByText('M — majorité à pleine participation : 55 % — inertie 3 / 5 · moyenne')).toBeVisible();
|
||||
await expect(panel.getByRole('button', { name: 'Réglages de la formule' })).toBeVisible();
|
||||
// The panel lives in the URL.
|
||||
await expect(page).toHaveURL(/onglet=vote/);
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(panel).toHaveCount(0);
|
||||
await expect(fold.getByText('M — majorité à pleine participation : 55 % — inertie 3 / 5 · moyenne')).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);
|
||||
await expect(page).toHaveURL(/vote=/);
|
||||
await toggle.click();
|
||||
await expect(fold).toHaveCount(0);
|
||||
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
||||
await expect(page).not.toHaveURL(/vote=/);
|
||||
});
|
||||
|
||||
test('C5 — une proposition remonte en vigueur quand elle atteint son seuil (groupe d’essai)', async ({ browser }) => {
|
||||
@@ -75,12 +80,13 @@ test('C5 — une proposition remonte en vigueur quand elle atteint son seuil (gr
|
||||
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('aside.panel').getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
await p.locator('section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
await expect(p.getByText('Vote enregistré.')).toBeVisible();
|
||||
}
|
||||
await expect(chiara.getByText('« Les deux garanties réciproques » est entré en vigueur.')).toBeVisible();
|
||||
await chiara.keyboard.press('Escape');
|
||||
await expect(chiara.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');
|
||||
});
|
||||
|
||||
test('C6 — une coquille se corrige sans vote', async ({ page }) => {
|
||||
@@ -89,7 +95,10 @@ test('C6 — une coquille se corrige sans vote', async ({ page }) => {
|
||||
const unicity = page.locator('section.text article.unit', { hasText: 'Unicité du compte' });
|
||||
await unicity.hover();
|
||||
await unicity.getByRole('button', { name: 'Le vote' }).click();
|
||||
// What is not the vote lives in the panel, tab « Détails ».
|
||||
await unicity.getByRole('button', { name: 'Coquilles, traductions, signalements, historique' }).click();
|
||||
const panel = page.locator('aside.panel');
|
||||
await expect(panel.getByRole('tab', { name: 'Détails' })).toHaveAttribute('aria-selected', 'true');
|
||||
await panel.getByText('Corriger une coquille').click();
|
||||
const body = panel.getByLabel('Texte', { exact: true });
|
||||
// Typographic apostrophes: the letters and the signs stay, only their style changes.
|
||||
@@ -151,7 +160,7 @@ test('C10 — trustinWallet : la gestion des accès est réservée', async ({ br
|
||||
await expect(yvv.locator('.link code')).toContainText('/invitation#');
|
||||
});
|
||||
|
||||
test('C11 — une personne, une voix : dans Monnaie libre, seuls les membres de la toile votent', async ({ browser }) => {
|
||||
test('C11 — une personne, une voix : dans Monnaie libre, seuls les membres de la toile votent, par la chaîne', async ({ browser }) => {
|
||||
const yvv = await person(browser, 'Yvv', true);
|
||||
const r = await yvv.request.post('/api/access/invitations', {
|
||||
data: { label: 'Invitée', groups: ['monnaie-libre', 'essai'] },
|
||||
@@ -160,17 +169,28 @@ test('C11 — une personne, une voix : dans Monnaie libre, seuls les membres de
|
||||
const { link } = (await r.json()) as { link: string };
|
||||
const ctx = await browser.newContext({ locale: 'fr-FR' });
|
||||
const invited = await ctx.newPage();
|
||||
// Her wallet holds Bob's account: an identity, not a member.
|
||||
await wallet(invited, [BOB]);
|
||||
await invited.goto(`/invitation#${link.split('#')[1]}`);
|
||||
await expect(invited).toHaveURL((u) => u.pathname === '/');
|
||||
await invited.goto(DOC);
|
||||
await invited.locator('ul.deep button.row', { hasText: 'Vérification personnelle de la clé' }).click();
|
||||
const panel = invited.locator('aside.panel');
|
||||
await expect(panel.getByText('Ici, seuls les membres de la toile de confiance Ğ1 votent', { exact: false })).toBeVisible();
|
||||
await expect(panel.getByRole('button', { name: 'Pour', exact: true })).toHaveCount(0);
|
||||
// In the test group, the same person votes.
|
||||
const fold = invited.locator('section.fold');
|
||||
// An invitation gives no voice: the voice is that of the Ğ1 identity of the wallet that signs.
|
||||
await expect(fold.getByText("Ta voix compte pour l'identité Ğ1 du portefeuille qui signe", { exact: false })).toBeVisible();
|
||||
await fold.getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
const dialog = invited.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' });
|
||||
await expect(
|
||||
dialog.getByText("Ton vote n'a pas été compté : le portefeuille qui a signé n'est pas celui d'un membre de la toile de confiance Ğ1."),
|
||||
).toBeVisible({ timeout: 20_000 });
|
||||
await dialog.getByRole('button', { name: 'Fermer' }).click();
|
||||
await expect(fold.getByText("Ton dernier vote n'a pas été compté", { exact: false })).toBeVisible();
|
||||
await expect(fold.getByText('0 pour · 0 contre')).toBeVisible();
|
||||
// In the test group, the same person votes, directly.
|
||||
await invited.goto(TEST_DOC);
|
||||
await invited.locator('ul.deep button.row', { hasText: 'Vérification personnelle de la clé' }).click();
|
||||
await expect(invited.locator('aside.panel').getByRole('button', { name: 'Pour', exact: true })).toBeVisible();
|
||||
await expect(invited.locator('section.fold').getByRole('button', { name: 'Pour', exact: true })).toBeVisible();
|
||||
await expect(invited.locator('section.fold').getByText('Ta voix compte', { exact: false })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('C12 — lien d’équipe : chaque personne qui l’ouvre a sa propre identité', async ({ browser }) => {
|
||||
@@ -237,10 +257,11 @@ test('C19 — un article, une ligne : ses versions se déplient', async ({ page
|
||||
await expect(versions).toHaveCount(2);
|
||||
await expect(versions.nth(0)).toContainText('variante 1.0.0');
|
||||
await expect(versions.nth(1)).toContainText('variante 0.4.0-draft');
|
||||
// A version opens its vote, as any proposal.
|
||||
// A version unfolds its vote below it, as any proposal.
|
||||
await versions.nth(1).click();
|
||||
await expect(page.locator('aside.panel').getByText('Seuil : 50 voix pour')).toBeVisible();
|
||||
await expect(page).toHaveURL(/version=/);
|
||||
await expect(versions.nth(1)).toHaveAttribute('aria-expanded', 'true');
|
||||
await expect(page.locator('section.fold').getByText('Seuil : 50 voix pour')).toBeVisible();
|
||||
await expect(page).toHaveURL(/vote=/);
|
||||
await e3.click();
|
||||
await expect(e3).toHaveAttribute('aria-expanded', 'false');
|
||||
await expect(versions).toHaveCount(0);
|
||||
@@ -273,8 +294,8 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => {
|
||||
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(panel.getByRole('heading', { name: 'Inertie · 4 / 5 · haute' })).toBeVisible();
|
||||
await panel.getByRole('button', { name: 'Réglages de la formule' }).click();
|
||||
await expect(e1.getByRole('heading', { name: 'Inertie · 4 / 5 · haute' })).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('cet article');
|
||||
@@ -287,15 +308,17 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => {
|
||||
const e2 = page.locator('section.text article.unit', { hasText: 'Réciprocité' });
|
||||
await e2.hover();
|
||||
await e2.getByRole('button', { name: 'Le vote' }).click();
|
||||
await panel.getByText("Proposer un changement d'inertie").click();
|
||||
const levels = panel.getByRole('radiogroup', { name: 'Nouvelle inertie' }).getByRole('radio');
|
||||
await e2.getByText("Proposer un changement d'inertie").click();
|
||||
const levels = e2.getByRole('radiogroup', { name: 'Nouvelle inertie' }).getByRole('radio');
|
||||
await expect(levels).toHaveCount(5);
|
||||
await expect(levels.nth(3)).toBeDisabled();
|
||||
await expect(levels.nth(3)).toContainText('actuelle');
|
||||
await expect(panel.getByText("Un changement d'inertie se vote lui-même à l'inertie de l'article : 4 / 5 · haute.")).toBeVisible();
|
||||
await expect(e2.getByText("Un changement d'inertie se vote lui-même à l'inertie de l'article : 4 / 5 · haute.")).toBeVisible();
|
||||
await levels.nth(4).click();
|
||||
await expect(levels.nth(4)).toHaveAttribute('aria-checked', 'true');
|
||||
await panel.getByRole('button', { name: 'Soumettre au vote' }).click();
|
||||
await expect(panel.getByRole('heading', { name: 'inertie vers 5 / 5 · très haute' })).toBeVisible();
|
||||
await expect(panel.getByText('inertie 4 / 5 · haute', { exact: true })).toBeVisible();
|
||||
await e2.getByRole('button', { name: 'Soumettre au vote' }).click();
|
||||
// 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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user