Files
decision/web/e2e/entree.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

173 lines
9.8 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 { expect, test, type Page } from '@playwright/test';
import { chain, CHARLIE, DOC, person, signIn } from './helpers';
const ML = '/g/monnaie-libre';
const path = (p: string) => (u: URL) => u.pathname === p;
async function background(page: Page, selector: string) {
return page.locator(selector).evaluate((el) => getComputedStyle(el).backgroundColor);
}
test('C13 — logo « Clé de voûte » sans tampon, quatre ambiances, icônes', async ({ browser }) => {
// First visit: the device's light or dark setting picks pierre or nuit.
for (const [scheme, expected] of [['light', 'pierre'], ['dark', 'nuit']] as const) {
const ctx = await browser.newContext({ locale: 'fr-FR', colorScheme: scheme });
const page = await ctx.newPage();
await page.goto('/');
await expect(page.locator('html')).toHaveAttribute('data-mood', expected);
await ctx.close();
}
const ctx = await browser.newContext({ locale: 'fr-FR', colorScheme: 'light', viewport: { width: 1360, height: 900 } });
const page = await ctx.newPage();
await page.goto('/');
const nav = page.getByRole('navigation', { name: 'Boîte à outils' });
const brand = nav.locator('a.brand');
await expect(brand).toHaveAccessibleName('libreDecision');
await expect(brand.locator('svg')).toBeVisible();
await expect(page.locator('.stamp')).toHaveCount(0);
expect(await brand.locator('svg').evaluate((el) => getComputedStyle(el).transform)).toBe('none');
// The seal stays where it is.
await expect(page.locator('svg.seal')).toBeVisible();
const moods = nav.getByRole('radiogroup', { name: 'Ambiance' });
for (const [name, mood, cle] of [
['Calcaire', 'calcaire', '#a8661e'],
['Ardoise', 'ardoise', '#e0a65c'],
['Nuit', 'nuit', '#6ec1c6'],
['Pierre', 'pierre', '#16808c'],
]) {
await moods.getByRole('radio', { name }).click();
await expect(page.locator('html')).toHaveAttribute('data-mood', mood);
await expect(moods.getByRole('radio', { name })).toHaveAttribute('aria-checked', 'true');
expect(await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--cle').trim())).toBe(cle);
}
await moods.getByRole('radio', { name: 'Ardoise' }).click();
await page.reload();
await expect(page.locator('html')).toHaveAttribute('data-mood', 'ardoise');
// Favicon and icons.
for (const href of ['/favicon.svg', '/apple-touch-icon.png', '/icon-192.png', '/icon-512.png', '/manifest.webmanifest']) {
expect((await page.request.get(href)).status(), href).toBe(200);
}
await expect(page.locator('link[rel="icon"][type="image/svg+xml"]')).toHaveAttribute('href', /favicon\.svg$/);
await expect(page.locator('link[rel="apple-touch-icon"]')).toHaveAttribute('href', /apple-touch-icon\.png$/);
await ctx.close();
});
test('C14 — la page d’un groupe : les quatre piliers, puis « En cours »', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto(ML);
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Monnaie libre');
const pillars = page.locator('.pillars .pillar');
await expect(pillars).toHaveCount(4);
for (const name of ['Décisions', 'Docs de référence', 'Mandats', 'Protocoles']) {
await expect(pillars.filter({ hasText: name })).toHaveCount(1);
}
await expect(pillars.filter({ hasText: 'Docs de référence' })).toContainText('ouvert');
await expect(pillars.filter({ hasText: 'à venir' })).toHaveCount(3);
const current = page.locator('section', { has: page.getByRole('heading', { name: 'En cours' }) });
await current.getByRole('link', { name: /Acte d'engagement Certification/ }).click();
await expect(page).toHaveURL(path(DOC));
await expect(page.getByRole('heading', { level: 1 })).toHaveText("Acte d'engagement Certification");
});
test('C15 — le menu : le nombre de docs, surligné seulement dedans, seul Docs réagit au survol', async ({ browser }) => {
const page = await person(browser, 'Lina');
// Nothing remembered yet: the tools lead home.
await page.goto('/');
const nav = page.getByRole('navigation', { name: 'Boîte à outils' });
const tool = (name: string) => nav.locator('.tool', { hasText: name });
await expect(tool('Docs de référence')).toHaveAttribute('href', '/');
await page.goto(ML);
await expect(tool('Docs de référence').locator('.count')).toHaveText('1');
await expect(tool('Docs de référence')).toHaveAccessibleName('Docs de référence, 1 doc en cours');
await expect(nav.locator('.tool[aria-current="page"]')).toHaveCount(0);
await expect(nav.locator('.tool.off')).toHaveCount(3);
// Hover: Docs reacts, a tool to come does not.
const docsBefore = await background(page, 'nav .tool.docs');
await tool('Docs de référence').hover();
await expect.poll(() => background(page, 'nav .tool.docs')).not.toBe(docsBefore);
const mandatesBefore = await background(page, 'nav .tool.mandats');
await tool('Mandats').hover();
await page.waitForTimeout(400);
expect(await background(page, 'nav .tool.mandats')).toBe(mandatesBefore);
// Inside a tool, it alone is highlighted.
await tool('Docs de référence').click();
await expect(page).toHaveURL(path(`${ML}/docs`));
await expect(nav.locator('.tool[aria-current="page"]')).toHaveText(/Docs de référence/);
await page.goto(DOC);
await expect(nav.locator('.tool[aria-current="page"]')).toHaveText(/Docs de référence/);
// A tool to come opens its page, which shows what is coming, dimmed.
await tool('Mandats').click();
await expect(page).toHaveURL(path(`${ML}/mandats`));
await expect(nav.locator('.tool[aria-current="page"]')).toHaveText(/Mandats/);
// Outside a group, the tools aim at the last group opened.
await page.goto('/');
await expect(tool('Docs de référence')).toHaveAttribute('href', `${ML}/docs`);
await expect(tool('Protocoles')).toHaveAttribute('href', `${ML}/protocoles`);
await expect(nav.locator('.tool[aria-current="page"]')).toHaveCount(0);
});
test('C16 — Docs, Mandats, Protocoles : « En cours » et « À venir », éteints', async ({ browser }) => {
const page = await person(browser, 'Noé');
// Old addresses, before any group: home.
await page.goto('/mandats');
await expect(page).toHaveURL(path('/'));
await page.goto(`${ML}/docs`);
const section = (name: string) => page.locator('section', { has: page.getByRole('heading', { name, exact: true }) });
await expect(section('En cours').getByRole('link', { name: /Acte d'engagement Certification/ })).toBeVisible();
const soon = section('À venir');
await expect(soon.locator('li')).toHaveText(["Acte d'engagement forgerons"]);
await expect(soon.getByRole('link')).toHaveCount(0);
await page.goto(`${ML}/mandats`);
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Mandats');
await expect(section('À venir').locator('li')).toHaveText(['Administration forum Duniter', 'Administration forum Monnaie libre']);
await expect(section('À venir').getByRole('link')).toHaveCount(0);
await page.goto(`${ML}/protocoles`);
await expect(section('À venir').locator('li')).toHaveText(['Runtime upgrade — décision on-chain']);
// Old addresses lead to the tools of the current group.
for (const tool of ['decisions', 'mandats', 'protocoles']) {
await page.goto(`/${tool}`);
await expect(page).toHaveURL(path(`${ML}/${tool}`));
}
// In English and Spanish, what is coming follows.
await page.getByRole('navigation', { name: 'Boîte à outils' }).getByRole('button', { name: 'ES' }).click();
await page.goto(`${ML}/protocoles`);
await expect(page.locator('section.soon li')).toHaveText(['Runtime upgrade — decisión on-chain']);
await page.getByRole('navigation', { name: 'Caja de herramientas' }).getByRole('button', { name: 'FR' }).click();
});
test('C17 — Décisions : une ligne par doc, les modules à voter, dont sans ton vote', async ({ browser }) => {
const page = await person(browser, 'Maëlle');
await page.goto(`${ML}/decisions`);
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Décisions');
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');
// 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
// account) would send, on the simulated chain.
const { doc, node } = await chain(page);
const e3 = doc.units.find((u) => u.code === 'E3');
const r = await page.request.post(`/api/docs/${doc.doc.id}/ballots`, {
data: { lines: [{ proposalId: e3?.proposals[1]?.id, choice: 'against' }], author: CHARLIE.address },
headers: { 'x-requested-with': 'libredecision' },
});
expect(r.ok()).toBe(true);
const { ballot } = await r.json();
const fields = { address: CHARLIE.address, dest: ballot.account, amount: String(ballot.amount), remark: ballot.comment, signature: '0x01' };
const extrinsic = `0x${Buffer.from(JSON.stringify(fields)).toString('hex')}`;
await page.request.post(node, { data: { jsonrpc: '2.0', id: 1, method: 'author_submitExtrinsic', params: [extrinsic] } });
await expect
.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 rows.first().getByRole('link').click();
await expect(page).toHaveURL(path(DOC));
});