forked from yvv/decision
Lot U2 : les contrôles d'abord, vus rouges sur l'interface d'avant — cibles de 44 px sur téléphone et les trois lectures entières (M2), « Proposer cet ordre » dit pourquoi il est éteint (C35), une information à un seul endroit dans le vote déplié (onceonly, C19, C36), l'en-tête compte ce que montre la liste et un changement d'inertie d'un module se trouve à sa ligne (C38), settings.reading et l'anglais (lexicon)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+66
-1
@@ -1,5 +1,5 @@
|
||||
import { expect, test, type Locator } from '@playwright/test';
|
||||
import { DOC, expectWelcome, shot, signIn } from './helpers';
|
||||
import { DOC, expectWelcome, propose, seedsInForce, shot, signIn, TEST_DOC, testDoc } from './helpers';
|
||||
|
||||
/** WCAG contrast ratio of two CSS colours `rgb(r, g, b)`. */
|
||||
function contrast(a: string, b: string): number {
|
||||
@@ -112,3 +112,68 @@ test('M1 — mobile : logo dans la barre, l’accueil sous la voûte, menu en ti
|
||||
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();
|
||||
});
|
||||
|
||||
/** The height of a target, in CSS pixels. */
|
||||
async function height(target: Locator): Promise<number> {
|
||||
return (await target.boundingBox())?.height ?? 0;
|
||||
}
|
||||
|
||||
/** Every gesture in sight under `root` — buttons, links, summaries — under 44 px high. */
|
||||
async function small(root: Locator): Promise<string[]> {
|
||||
return root.evaluate((el) =>
|
||||
[...el.querySelectorAll('button, a[href], summary, [role="radio"]')]
|
||||
.filter((b) => b.checkVisibility({ visibilityProperty: true }) && b.getClientRects().length > 0)
|
||||
.map((b) => ({ what: (b.textContent || b.getAttribute('aria-label') || '').replace(/\s+/g, ' ').trim(), h: b.getBoundingClientRect().height }))
|
||||
.filter((x) => x.h < 44)
|
||||
.map((x) => `${x.what} (${Math.round(x.h)} px)`),
|
||||
);
|
||||
}
|
||||
|
||||
test('M2 — téléphone : les gestes du lecteur font au moins 44 px, les trois lectures se lisent en entier', async ({ page, browser }) => {
|
||||
await seedsInForce(browser);
|
||||
await signIn(page, 'Chiara');
|
||||
// In the test group: a rival of the Raison d'être (in force), so that its vote names its
|
||||
// « Autres versions », and an article of hers, whose quorum she sets before any vote.
|
||||
const doc = await testDoc(page);
|
||||
const i1 = doc.units.find((u) => u.code === 'I1');
|
||||
const r = await page.request.post(`/api/docs/${doc.id}/proposals`, {
|
||||
data: { kind: 'text', unitId: i1?.id, title: "Raison d'être", body: 'Une clé, une personne vivante : la certification ne garantit rien d’autre.', submit: true },
|
||||
headers: { 'x-requested-with': 'libredecision' },
|
||||
});
|
||||
expect(r.ok()).toBe(true);
|
||||
const mine = await propose(page, 'Cibles au doigt', 'Je propose cet article pour régler son quorum au doigt.');
|
||||
await page.goto(TEST_DOC);
|
||||
|
||||
// The three readings, each whole on the screen: « Si tout passe » is never cut (lot U2).
|
||||
const views = page.getByRole('radiogroup', { name: 'Lire' });
|
||||
const frame = await views.boundingBox();
|
||||
expect.soft(await views.evaluate((el) => el.scrollWidth <= el.clientWidth + 1)).toBe(true);
|
||||
for (const reading of await views.getByRole('radio').all()) {
|
||||
const b = await reading.boundingBox();
|
||||
expect.soft(b && frame && b.x >= frame.x - 1 && b.x + b.width <= frame.x + frame.width + 1, await reading.textContent()).toBe(true);
|
||||
}
|
||||
await expect.soft(views.getByRole('radio', { name: 'Si tout passe' })).toBeInViewport({ ratio: 1 });
|
||||
await shot(page, 'm2-1-lectures');
|
||||
|
||||
// « Tout déplier », then the vote of the Raison d'être: every gesture a finger's target.
|
||||
expect.soft(await height(page.getByRole('button', { name: 'Tout déplier' }))).toBeGreaterThanOrEqual(44);
|
||||
const first = page.locator('section.text article.unit').first();
|
||||
await first.getByRole('button', { name: 'Le vote' }).click();
|
||||
const fold = first.locator('section.fold');
|
||||
const others = fold.getByRole('button', { name: 'Autres versions (1)' });
|
||||
expect.soft(await height(others)).toBeGreaterThanOrEqual(44);
|
||||
await others.click();
|
||||
await fold.getByText('La formule du seuil').click();
|
||||
expect.soft(await small(fold)).toEqual([]);
|
||||
await shot(page, 'm2-2-vote-deplie');
|
||||
|
||||
// Her own article: « Fixer ce quorum » and the rest of its vote.
|
||||
await page.goto(`${TEST_DOC}?vote=${mine}`);
|
||||
const hers = page.getByRole('region', { name: 'Le vote de « Cibles au doigt »' });
|
||||
const set = hers.getByRole('button', { name: 'Fixer ce quorum' });
|
||||
expect.soft(await height(set)).toBeGreaterThanOrEqual(44);
|
||||
await hers.getByText("Proposer un changement d'inertie").click();
|
||||
expect.soft(await small(hers)).toEqual([]);
|
||||
await set.scrollIntoViewIfNeeded();
|
||||
await shot(page, 'm2-3-quorum');
|
||||
});
|
||||
|
||||
@@ -48,6 +48,10 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss
|
||||
await expect(list.locator('.grip')).toHaveCount(3);
|
||||
const submit = page.getByRole('button', { name: 'Proposer cet ordre' });
|
||||
await expect(submit).toBeDisabled();
|
||||
// Off, and it says why (UX-lecteur.md: « le bouton reste éteint et le dit » — lot U2).
|
||||
const why = "C'est l'ordre en vigueur : déplace au moins un article.";
|
||||
await expect(page.getByText(why)).toBeVisible();
|
||||
await expect(submit).toHaveAccessibleDescription(why);
|
||||
await shot(page, 'c35-2-poignees');
|
||||
|
||||
// By dragging its handle: Réciprocité onto the place of Unicité du compte.
|
||||
@@ -55,6 +59,7 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss
|
||||
await expect(names).toHaveText([EF, E2, E1]);
|
||||
await expect(page.getByText(`« ${E2} » en place 2 sur 3.`)).toBeAttached();
|
||||
await expect(submit).toBeEnabled();
|
||||
await expect(page.getByText(why)).toHaveCount(0);
|
||||
// With the keyboard: the article keeps the focus, the arrows move it.
|
||||
const items = list.locator('button.item');
|
||||
await items.nth(1).focus();
|
||||
@@ -67,6 +72,7 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss
|
||||
await list.getByRole('button', { name: `Descendre « ${E2} »` }).click();
|
||||
await expect(names).toHaveText([EF, E1, E2]);
|
||||
await expect(submit).toBeDisabled();
|
||||
await expect(page.getByText(why)).toBeVisible();
|
||||
await items.nth(2).click();
|
||||
await expect(items.nth(2)).toHaveAttribute('aria-pressed', 'true');
|
||||
await items.nth(1).click();
|
||||
@@ -145,6 +151,9 @@ test('C36 — combien de oui pour passer, combien de non pour bloquer : sous l
|
||||
await expect(need).toBeVisible();
|
||||
// The quorum in a tooltip, never as a title.
|
||||
await expect(need).toHaveAttribute('title', 'Quorum : 12 votants. En dessous, les votants qui manquent comptent comme « non ».');
|
||||
// Said once: the line of the seed proposal, right above its vote, does not say it again (lot U2).
|
||||
await expect(i1.locator('footer')).not.toContainText(/oui (sur|pour)/);
|
||||
await expect(i1.locator('footer')).not.toContainText(/\d+ pour · \d+ contre/);
|
||||
// In the list, short: at level 3, quorum 50.
|
||||
await unfold(page);
|
||||
await expect(deepRow(page, 'Vérification personnelle de la clé')).toContainText('39 oui sur 50');
|
||||
@@ -237,3 +246,48 @@ test('C37 — le quorum d’un article : son auteur le fixe avant le premier vot
|
||||
await expect(fold.getByRole('region', { name: /^Quorum/ })).toHaveCount(0);
|
||||
await yvv.context().close();
|
||||
});
|
||||
|
||||
test('C38 — l’en-tête compte ce que montre la liste ; un changement d’inertie d’un ordre ou du titre se trouve à sa ligne', async ({ page }) => {
|
||||
await signIn(page, 'Chiara');
|
||||
/** The versions « Propositions en cours » shows: one per line, or those inside a line of several. */
|
||||
const listed = () =>
|
||||
page.locator('ul.deep li.article').evaluateAll((lis) => lis.reduce((n, li) => n + Math.max(1, li.querySelectorAll('ul.versions > li').length), 0));
|
||||
// Monnaie libre, nothing in force: the seed proposals at the head, said apart; the count of the
|
||||
// proposals in progress is what the list below shows (lot U2: the orders are not counted there).
|
||||
await page.goto(DOC);
|
||||
await unfold(page);
|
||||
const facts = page.locator('.facts');
|
||||
await expect.soft(facts).toContainText('0 article en vigueur · 3 Propositions seed · ');
|
||||
await expect.soft(facts).toContainText(new RegExp(`· ${await listed()} propositions en cours`));
|
||||
|
||||
// A change of inertia on the order of a section and on the title (test group): each is found at
|
||||
// the line of its module, where its vote unfolds — never a line of « Propositions en cours ».
|
||||
const g = await (await page.request.get('/api/groups/essai')).json();
|
||||
const d = await (await page.request.get(`/api/docs/${g.docs[0].id}`)).json();
|
||||
const modules: { id: string; inertia: number }[] = [d.modules.orders.find((o: { section: string }) => o.section === 'conclusion'), d.modules.title];
|
||||
const made: string[] = [];
|
||||
for (const unit of modules) {
|
||||
const r = await page.request.post(`/api/units/${unit.id}/inertia`, { data: { target: unit.inertia === 5 ? 4 : unit.inertia + 1 }, headers: CSRF });
|
||||
expect(r.ok()).toBe(true);
|
||||
made.push((await r.json()).id);
|
||||
}
|
||||
await page.goto(TEST_DOC);
|
||||
const title = page.getByRole('group', { name: 'Titre du document' });
|
||||
const titleChange = title.getByRole('button', { name: "1 changement d'inertie proposé" });
|
||||
await expect(titleChange).toBeVisible();
|
||||
await titleChange.click();
|
||||
await expect(title.getByRole('region', { name: /^Le vote de « inertie vers \d \/ 5 · / })).toBeVisible();
|
||||
await shot(page, 'c38-1-titre-inertie');
|
||||
await unfold(page);
|
||||
await expect.soft(facts).toContainText(new RegExp(`· ${await listed()} propositions en cours`));
|
||||
await expect(page.locator('ul.deep').getByText(/inertie vers/)).toHaveCount(0);
|
||||
const order = page.getByRole('group', { name: 'Ordre des articles · Conclusion' });
|
||||
const orderChange = order.getByRole('button', { name: "1 changement d'inertie proposé" });
|
||||
await expect(orderChange).toBeVisible();
|
||||
await orderChange.click();
|
||||
await expect(order.getByRole('region', { name: /^Le vote de « inertie vers \d \/ 5 · / })).toBeVisible();
|
||||
await order.scrollIntoViewIfNeeded();
|
||||
await shot(page, 'c38-2-ordre-inertie');
|
||||
// Withdrawn: the group is left as found.
|
||||
for (const id of made) expect((await page.request.post(`/api/proposals/${id}/withdraw`, { headers: CSRF })).ok()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -298,6 +298,8 @@ test('C19 — un article, une ligne : ses versions se déplient', async ({ page
|
||||
// Level 3, the quorum 50 (ADR 0013): 39 « oui » pass it, the voters missing counted as « non »
|
||||
// (« sur 50 votants » before any vote; a « contre » of another control may be there already).
|
||||
await expect(page.locator('section.fold .need')).toHaveText(/^39 oui (sur 50 votants pour passer|pour passer · le quorum la bloque)$/);
|
||||
// Its other version is right there in the list: the vote does not list it again (lot U2).
|
||||
await expect(page.locator('section.fold').getByRole('button', { name: /^Autres versions/ })).toHaveCount(0);
|
||||
await expect(page).toHaveURL(/vote=/);
|
||||
await e3.click();
|
||||
await expect(e3).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
Reference in New Issue
Block a user