Files
decision/web/e2e/mobile.spec.ts
T
YvvandClaude Opus 5.5 9a9c99b93b Lot S : « Proposition seed » dans l'interface (fr, en, es), le texte sans article en vigueur le dit ; e2e repris
Interface au strict nécessaire (le lot U refera la présentation) : la marque
« Proposition seed » là où une version est montrée (liste des propositions,
Détails, panneau Propositions), seedProposal?: boolean dans types.ts ; au-dessus
des ordres, « Aucun article n'est en vigueur » quand c'est le cas.

e2e : C3 (aucun texte en vigueur, 33 lignes, trois Propositions seed au cran 2),
C9 et C18 (« si tout passe »), C20, C23, C24 et M1 par la liste des propositions
dans Monnaie libre ; dans le groupe d'essai, I1, E1 et E2 entrent d'abord par le
vote (seedsInForce : deux voix puis leur retrait, le texte reste en vigueur) pour
les contrôles qui lisent un texte en vigueur (C4 à C6, C20, C21, C24, C28 à C31).
Boucle e2e : 35 / 35.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 04:27:41 +01:00

110 lines
6.2 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 Locator } from '@playwright/test';
import { DOC, expectWelcome, shot, signIn } from './helpers';
/** WCAG contrast ratio of two CSS colours `rgb(r, g, b)`. */
function contrast(a: string, b: string): number {
const lum = (c: string) => {
const [r, g, bl] = (c.match(/[\d.]+/g) ?? []).slice(0, 3).map((x) => {
const v = Number(x) / 255;
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * (r ?? 0) + 0.7152 * (g ?? 0) + 0.0722 * (bl ?? 0);
};
const [hi, lo] = [lum(a), lum(b)].sort((x, y) => y - x);
return ((hi ?? 0) + 0.05) / ((lo ?? 0) + 0.05);
}
/** The colour the ring of a swatch is drawn on: the first opaque background around it. */
async function backdrop(el: Locator): Promise<string> {
return el.evaluate((node) => {
for (let n: Element | null = node.parentElement; n; n = n.parentElement) {
const bg = getComputedStyle(n).backgroundColor;
if (bg && bg !== 'transparent' && !/rgba\(.*,\s*0\)$/.test(bg)) return bg;
}
return getComputedStyle(document.body).backgroundColor;
});
}
test('M1 — mobile : logo dans la barre, l’accueil sous la voûte, menu en tiroir, vote déplié, panier, réglages lisibles, pastilles au clavier', async ({ page }) => {
await signIn(page, 'Chiara');
await page.goto('/');
const brand = page.locator('header.topbar a.brand');
await expect(brand).toHaveAccessibleName('libreDecision');
await expect(brand.locator('svg')).toBeVisible();
await expect(page.locator('a.root')).toContainText('Monnaie libre');
// The welcome on this phone, then on a narrow one (360): the arch still shelters the three points.
await expectWelcome(page);
const size = page.viewportSize();
await page.setViewportSize({ width: 360, height: 780 });
await expectWelcome(page);
if (size) await page.setViewportSize(size);
await page.goto(DOC);
await page.getByRole('button', { name: 'Menu' }).click();
const drawer = page.getByRole('dialog', { name: 'Boîte à outils' });
await expect(drawer.locator('.tool', { hasText: 'Protocoles' })).toBeVisible();
await expect(drawer.locator('.tool', { hasText: 'Docs de référence' }).locator('.count')).toHaveText('1');
const moods = drawer.getByRole('radiogroup', { name: 'Ambiance' });
await expect(moods.getByRole('radio')).toHaveCount(4);
// The swatches, by keyboard: one stop for Tab, the arrows move and choose; the focus ring
// reads at 3:1 or more on the page whatever the swatch, and is not the mark of the chosen one.
await expect(moods.locator('[tabindex="0"]')).toHaveCount(1);
const start = (await page.locator('html').getAttribute('data-mood')) ?? '';
await moods.getByRole('radio', { checked: true }).focus();
for (let i = 0; i < 4; i++) {
await page.keyboard.press('ArrowRight');
const now = moods.getByRole('radio', { checked: true });
await expect(now).toBeFocused();
await expect(moods.locator('[tabindex="0"]')).toHaveCount(1);
const ring = await now.evaluate((el) => {
const cs = getComputedStyle(el);
return { style: cs.outlineStyle, color: cs.outlineColor, width: parseFloat(cs.outlineWidth) };
});
expect(ring.style).toBe('dashed');
expect(ring.width).toBeGreaterThanOrEqual(2);
const mood = await page.locator('html').getAttribute('data-mood');
expect(contrast(ring.color, await backdrop(moods)), `${mood}: ${ring.color}`).toBeGreaterThanOrEqual(3);
if (i === 0) await shot(page, '7-pastilles-focus-mobile');
}
await expect(page.locator('html')).toHaveAttribute('data-mood', start);
await page.keyboard.press('ArrowLeft');
await expect(page.locator('html')).not.toHaveAttribute('data-mood', start);
await page.keyboard.press('ArrowRight');
await expect(page.locator('html')).toHaveAttribute('data-mood', start);
// The chosen one, without the focus: its own mark, a ring of ink, never the dashed one.
const chosen = moods.getByRole('radio', { checked: true });
await chosen.evaluate((el) => (el as HTMLElement).blur());
const mark = await chosen.evaluate((el) => ({ outline: getComputedStyle(el).outlineStyle, shadow: getComputedStyle(el).boxShadow }));
expect(mark.outline).toBe('none');
expect(mark.shadow).toContain('0px 0px 0px 2px');
await drawer.getByRole('button', { name: 'Fermer' }).first().click();
// The vote unfolds under the article, on a phone too (a seed proposal, ADR 0011: nothing is
// in force at the start, its vote unfolds in the list of proposals).
await page.locator('ul.deep button.row').first().click();
const fold = page.locator('ul.deep section.fold');
await expect(fold.getByText('Seuil : 50 voix pour')).toBeVisible();
// 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');
await expect(panel.getByRole('tab', { name: 'Réglages' })).toBeInViewport();
await expect(panel.locator('table tbody tr')).toHaveCount(5);
const fits = await panel.locator('.scroll').evaluate((el) => el.scrollWidth <= el.clientWidth + 1);
expect(fits).toBe(true);
await panel.getByRole('button', { name: 'Fermer le panneau' }).click();
// The basket holds on a phone: its send button stays in sight, nothing overflows.
await page.getByRole('button', { name: /^Voter en lot/ }).click();
await expect(panel.getByRole('heading', { name: 'Panier de vote' })).toBeVisible();
await panel.getByRole('group', { name: 'Ton choix pour « Unicité du compte »' }).getByRole('button', { name: 'Pour' }).click();
await panel.getByRole('group', { name: 'Ton choix pour « Réciprocité »' }).getByRole('button', { name: 'Contre' }).click();
const send = panel.getByRole('button', { name: 'Signer et envoyer (2)' });
await expect(send).toBeInViewport();
const holds = await panel.locator('.body').evaluate((el) => el.scrollWidth <= el.clientWidth + 1);
expect(holds).toBe(true);
await shot(page, '3-panier-mobile');
// Taken back out of the basket: nothing was sent.
await panel.getByRole('group', { name: 'Tout le document' }).getByRole('button', { name: 'Vider' }).click();
await expect(panel.getByText("Rien dans le panier pour l'instant.")).toBeVisible();
});