import { expect, test, type Page } from '@playwright/test'; import { 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(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"); await expect(rows.first()).toContainText('33 modules à voter'); await expect(rows.first()).toContainText('dont 33 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). const group = await (await page.request.get('/api/groups/monnaie-libre')).json(); const doc = await (await page.request.get(`/api/docs/${group.docs[0].id}`)).json(); const e3 = doc.units.find((u: { code: string }) => u.code === 'E3'); const r = await page.request.post(`/api/proposals/${e3.proposals[1].id}/vote`, { data: { choice: 'against' }, headers: { 'x-requested-with': 'libredecision' }, }); expect(r.ok()).toBe(true); await page.reload(); await expect(rows.first()).toContainText('33 modules à voter'); await expect(rows.first()).toContainText('dont 32 sans ton vote'); await rows.first().getByRole('link').click(); await expect(page).toHaveURL(path(DOC)); });