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:
+85
-9
@@ -1,7 +1,31 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { DOC, signIn } from './helpers';
|
||||
import { expect, test, type Locator } from '@playwright/test';
|
||||
import { DOC, shot, signIn } from './helpers';
|
||||
|
||||
test('M1 — mobile : logo dans la barre, menu en tiroir, actions visibles, panneau en feuille, réglages lisibles', async ({ page }) => {
|
||||
/** 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, 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');
|
||||
@@ -13,15 +37,67 @@ test('M1 — mobile : logo dans la barre, menu en tiroir, actions visibles, pann
|
||||
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');
|
||||
await expect(drawer.getByRole('radiogroup', { name: 'Ambiance' }).getByRole('radio')).toHaveCount(4);
|
||||
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.
|
||||
const first = page.locator('section.text article.unit').first();
|
||||
await first.getByRole('button', { name: 'Le vote' }).click();
|
||||
await expect(page.locator('aside.panel').getByText('Seuil : 50 voix pour')).toBeVisible();
|
||||
const fold = first.locator('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 page.locator('aside.panel').getByRole('button', { name: 'Réglages de la formule' }).click();
|
||||
await expect(page.locator('aside.panel').getByRole('tab', { name: 'Réglages' })).toBeInViewport();
|
||||
await expect(page.locator('aside.panel table tbody tr')).toHaveCount(5);
|
||||
const fits = await page.locator('aside.panel .scroll').evaluate((el) => el.scrollWidth <= el.clientWidth + 1);
|
||||
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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user