forked from yvv/decision
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>
This commit is contained in:
+41
-1
@@ -75,6 +75,11 @@ export async function signed(page: Page): Promise<{ address: string; dest: strin
|
||||
);
|
||||
}
|
||||
|
||||
/** The line of an article in « Propositions en cours », by its title alone (an excerpt may name it). */
|
||||
export function deepRow(page: Page, title: string) {
|
||||
return page.locator('ul.deep button.row').filter({ has: page.locator('span.title').getByText(title, { exact: true }) });
|
||||
}
|
||||
|
||||
/** A screenshot for the visual check, when `LD_SHOTS` names a directory (never in the loop). */
|
||||
export async function shot(page: Page, name: string) {
|
||||
const dir = process.env.LD_SHOTS;
|
||||
@@ -86,13 +91,48 @@ const CSRF = { 'x-requested-with': 'libredecision' };
|
||||
/** The document of the test group, as the server gives it. */
|
||||
export async function testDoc(page: Page): Promise<{
|
||||
id: string;
|
||||
units: { id: string; code: string; inForce: { id: string; title: string } | null; proposals: { id: string; title: string }[] }[];
|
||||
units: {
|
||||
id: string;
|
||||
code: string;
|
||||
inForce: { id: string; title: string } | null;
|
||||
proposals: { id: string; title: string; seedProposal?: boolean }[];
|
||||
}[];
|
||||
}> {
|
||||
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();
|
||||
return { id: d.doc.id, units: d.units };
|
||||
}
|
||||
|
||||
/**
|
||||
* The three seed proposals (ADR 0011: I1, E1 and E2, not in force at the start) brought into
|
||||
* force in the test group, as the vote does it: voices for until each enters, then those
|
||||
* voices withdrawn. A version in force stays so (a permanent vote: only a rival displaces
|
||||
* it), at zero votes, as the controls written before the seed proposals found it. Never in
|
||||
* Monnaie libre, whose vote goes through the chain; nothing done twice.
|
||||
*/
|
||||
export async function seedsInForce(browser: Browser) {
|
||||
const voices = [await person(browser, 'Amorce A'), await person(browser, 'Amorce B')];
|
||||
for (const code of ['I1', 'E1', 'E2']) {
|
||||
const unit = (await testDoc(voices[0]!)).units.find((u) => u.code === code);
|
||||
const seed = unit?.proposals.find((p) => p.seedProposal);
|
||||
if (!unit || unit.inForce || !seed) continue;
|
||||
const cast: Page[] = [];
|
||||
for (const page of voices) {
|
||||
const r = await page.request.post(`/api/proposals/${seed.id}/vote`, { data: { choice: 'for' }, headers: CSRF });
|
||||
if (!r.ok()) throw new Error(`vote ${r.status()} ${await r.text()}`);
|
||||
cast.push(page);
|
||||
if ((await r.json()).entered?.length) break;
|
||||
}
|
||||
for (const page of cast) {
|
||||
const r = await page.request.post(`/api/proposals/${seed.id}/vote`, { data: { choice: null }, headers: CSRF });
|
||||
if (!r.ok()) throw new Error(`withdraw ${r.status()} ${await r.text()}`);
|
||||
}
|
||||
const now = (await testDoc(voices[0]!)).units.find((u) => u.code === code);
|
||||
if (now?.inForce?.id !== seed.id) throw new Error(`${code} did not enter into force`);
|
||||
}
|
||||
for (const page of voices) await page.context().close();
|
||||
}
|
||||
|
||||
/** A new article proposed by the person of `page`, submitted to the vote; returns its id. */
|
||||
export async function propose(page: Page, title: string, body: string): Promise<string> {
|
||||
const doc = await testDoc(page);
|
||||
|
||||
@@ -79,10 +79,10 @@ test('M1 — mobile : logo dans la barre, l’accueil sous la voûte, menu en ti
|
||||
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();
|
||||
const fold = first.locator('section.fold');
|
||||
// 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();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { advanceClock, person, propose, shots, TEST_DOC, testDoc } from './helpers';
|
||||
import { advanceClock, person, propose, seedsInForce, shots, TEST_DOC, testDoc } from './helpers';
|
||||
|
||||
// Lot F (Yvv, 2026-10-30, ADR 0010): an objection « redit » or « contredit »
|
||||
// holds back the later of two versions until it is lifted; the author answers,
|
||||
@@ -10,6 +10,10 @@ import { advanceClock, person, propose, shots, TEST_DOC, testDoc } from './helpe
|
||||
|
||||
const DAY = 86_400;
|
||||
|
||||
// An objection names an article in force, a typo of C31 a text in force: the seed proposals
|
||||
// I1, E1 and E2 (ADR 0011) are voted into force in the test group first.
|
||||
test.beforeAll(async ({ browser }) => seedsInForce(browser));
|
||||
|
||||
/** The menu's « Messages », with its number of unread messages. */
|
||||
async function openBox(page: Page, unread: number) {
|
||||
const nav = page.getByRole('navigation', { name: 'Boîte à outils' });
|
||||
|
||||
+69
-49
@@ -1,5 +1,9 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { BOB, DOC, expectWelcome, person, signIn, TEST_DOC, wallet } from './helpers';
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { BOB, deepRow, DOC, expectWelcome, person, seedsInForce, signIn, TEST_DOC, testDoc, wallet } from './helpers';
|
||||
|
||||
// The seed proposals (ADR 0011) are not in force at the start: the controls that read a
|
||||
// text in force find I1, E1 and E2 brought in by the vote in the test group.
|
||||
test.beforeAll(async ({ browser }) => seedsInForce(browser));
|
||||
|
||||
test('C1 — l’accueil sans session : la voûte abrite les trois points, le titre centré en deux lignes, la connexion, le menu complet', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -33,38 +37,41 @@ test('C2 — l’accueil avec session : le choix du groupe, ceux de la même cl
|
||||
await expect(page).toHaveURL((u) => u.pathname === '/');
|
||||
});
|
||||
|
||||
test('C3 — le texte en vigueur : raison d’être et deux engagements, seed a minima', async ({ page }) => {
|
||||
test('C3 — au départ, aucun texte en vigueur : la raison d’être et les deux engagements sont des Propositions seed', async ({ page }) => {
|
||||
await signIn(page, 'Chiara');
|
||||
await page.goto(DOC);
|
||||
await expect(page.getByRole('heading', { level: 1 })).toHaveText("Acte d'engagement Certification");
|
||||
const units = page.locator('section.text article.unit');
|
||||
await expect(units).toHaveCount(3);
|
||||
await expect(units.nth(0).locator('h2')).toHaveText("Raison d'être");
|
||||
await expect(units.nth(1).locator('h2')).toHaveText('Unicité du compte');
|
||||
await expect(units.nth(2).locator('h2')).toHaveText('Réciprocité');
|
||||
// The seed mark, with the real number of votes beside it (none yet on these three).
|
||||
await expect(units.getByText('seed a minima · 0 vote')).toHaveCount(3);
|
||||
await expect(units.getByTitle("Entré en vigueur sans vote, pour ne pas partir d'une page blanche. Il reste sous vote, comme tout texte.")).toHaveCount(3);
|
||||
// Nothing in force (Yvv, 2026-11-02): said once, above the orders of the sections.
|
||||
await expect(page.locator('section.text article.unit')).toHaveCount(0);
|
||||
await expect(page.getByText("Aucun article n'est en vigueur. Les propositions ci-dessous remontent dès qu'elles atteignent leur seuil.")).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Propositions en cours' })).toBeVisible();
|
||||
// One line per article: E3 and E7 hold their two variants inside.
|
||||
await expect(page.locator('ul.deep button.row')).toHaveCount(30);
|
||||
// One line per article: the 33, E3 and E7 holding their two variants inside.
|
||||
await expect(page.locator('ul.deep button.row')).toHaveCount(33);
|
||||
// The three seed proposals, marked, at the low level they start at.
|
||||
const seeds = page.locator('ul.deep button.row', { has: page.locator('.tag', { hasText: 'Proposition seed' }) });
|
||||
await expect(seeds.locator('.title')).toHaveText(["Raison d'être", 'Unicité du compte', 'Réciprocité']);
|
||||
await expect(seeds.getByTitle("Texte de départ mis en avant pour lancer le vote. Il n'est pas en vigueur : il y entre par le vote, comme toute proposition.")).toHaveCount(3);
|
||||
for (const row of await seeds.all()) await expect(row).toContainText('0 pour · 0 contre');
|
||||
for (const row of await seeds.all()) await expect(row).toContainText('inertie 2 / 5 · basse');
|
||||
});
|
||||
|
||||
test('C4 — les actions sont contextuelles, le vote se déplie sous l’article', async ({ page }) => {
|
||||
// In the test group, where the Raison d'être was voted into force (beforeAll).
|
||||
await signIn(page, 'Chiara');
|
||||
await page.goto(DOC);
|
||||
await page.goto(TEST_DOC);
|
||||
const first = page.locator('section.text article.unit').first();
|
||||
await expect(first.locator('h2')).toHaveText("Raison d'être");
|
||||
await first.hover();
|
||||
const toggle = first.getByRole('button', { name: 'Le vote' });
|
||||
await toggle.click();
|
||||
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
||||
const fold = first.locator('section.fold');
|
||||
await expect(fold.getByText('Seuil : 50 voix pour')).toBeVisible();
|
||||
await expect(fold.getByText('Il manque 50 voix pour.')).toBeVisible();
|
||||
await expect(fold.getByText(/^Seuil : \d+ voix pour$/)).toBeVisible();
|
||||
await expect(fold.getByText(/^Il manque \d+ voix pour\.$/)).toBeVisible();
|
||||
await fold.getByText('La formule du seuil').click();
|
||||
await expect(fold.getByText(/W — électorat/)).toBeVisible();
|
||||
// The level of the article sets M and G: the Raison d'être is at 3 / 5.
|
||||
await expect(fold.getByText('M — majorité à pleine participation : 55 % — inertie 3 / 5 · moyenne')).toBeVisible();
|
||||
// The level of the article sets M and G: the Raison d'être, a seed proposal, starts at 2 / 5.
|
||||
await expect(fold.getByText('M — majorité à pleine participation : 50 % — inertie 2 / 5 · basse')).toBeVisible();
|
||||
await expect(fold.getByRole('button', { name: 'Réglages de la formule' })).toBeVisible();
|
||||
// No window opens: the vote unfolds in place, lives in the URL, and folds back.
|
||||
await expect(page.locator('aside.panel')).toHaveCount(0);
|
||||
@@ -76,18 +83,28 @@ test('C4 — les actions sont contextuelles, le vote se déplie sous l’article
|
||||
});
|
||||
|
||||
test('C5 — une proposition remonte en vigueur quand elle atteint son seuil (groupe d’essai)', async ({ browser }) => {
|
||||
const yvv = await person(browser, 'Yvv');
|
||||
const chiara = await person(browser, 'Chiara');
|
||||
for (const p of [yvv, chiara]) {
|
||||
await p.goto(TEST_DOC);
|
||||
await p.locator('ul.deep button.row', { hasText: 'Les deux garanties réciproques' }).click();
|
||||
await p.locator('section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
await expect(p.getByText('Vote enregistré.')).toBeVisible();
|
||||
// Every dev sign-in joins the test group: from 16 members B^W vanishes and, C being 1, one
|
||||
// voice may suffice (ADR 0007); else the second one brings it in.
|
||||
const title = 'Les deux garanties réciproques';
|
||||
const news = `« ${title} » est entré en vigueur.`;
|
||||
const inForce = async (page: Page) => (await testDoc(page)).units.some((u) => u.inForce?.title === title);
|
||||
let voices = 0;
|
||||
let last: Page | undefined;
|
||||
for (const name of ['Yvv', 'Chiara']) {
|
||||
last = await person(browser, name);
|
||||
await last.goto(TEST_DOC);
|
||||
await last.locator('ul.deep button.row', { hasText: title }).click();
|
||||
await last.locator('section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
await expect(last.getByText('Vote enregistré.')).toBeVisible();
|
||||
voices += 1;
|
||||
if (await inForce(last)) break;
|
||||
}
|
||||
await expect(chiara.getByText('« Les deux garanties réciproques » est entré en vigueur.')).toBeVisible();
|
||||
await expect(chiara.locator('section.text article.unit')).toHaveCount(4);
|
||||
if (!last) throw new Error('nobody voted');
|
||||
await expect(last.getByText(news)).toBeVisible();
|
||||
// With the three seed proposals voted in (beforeAll).
|
||||
await expect(last.locator('section.text article.unit')).toHaveCount(4);
|
||||
// In force now: its vote stays unfolded, under the text.
|
||||
await expect(chiara.locator('section.text section.fold')).toContainText('2 pour · 0 contre');
|
||||
await expect(last.locator('section.text section.fold')).toContainText(`${voices} pour · 0 contre`);
|
||||
});
|
||||
|
||||
test('C6 — une coquille se corrige sans vote', async ({ page }) => {
|
||||
@@ -147,8 +164,9 @@ test('C9 — les aperçus : bien cotées, et tout', async ({ page }) => {
|
||||
await page.getByRole('radio', { name: 'Si tout passe' }).click();
|
||||
await expect(page.getByText("Aperçu : le texte tel qu'il serait si toutes les propositions", { exact: false })).toBeVisible();
|
||||
await expect(page.locator('section.text article.unit')).toHaveCount(33);
|
||||
// Nothing in force in Monnaie libre, and nothing well rated yet (no « pour » there so far).
|
||||
await page.getByRole('radio', { name: 'Si les bien cotées passent' }).click();
|
||||
await expect(page.locator('section.text article.unit')).toHaveCount(3);
|
||||
await expect(page.locator('section.text article.unit')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('C10 — trustinWallet : la gestion des accès est réservée', async ({ browser }) => {
|
||||
@@ -228,9 +246,11 @@ test('C12 — lien d’équipe : chaque personne qui l’ouvre a sa propre ident
|
||||
await expect(yvv.getByText('venue par « Équipe de test »')).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('C18 — la Raison d’être ouvre le texte en vigueur, en trois langues', async ({ page }) => {
|
||||
test('C18 — la Raison d’être ouvre le texte, en trois langues', async ({ page }) => {
|
||||
await signIn(page, 'Chiara');
|
||||
await page.goto(DOC);
|
||||
// A seed proposal, not in force: it opens the text « if everything passes ».
|
||||
await page.getByRole('radio', { name: 'Si tout passe' }).click();
|
||||
const first = page.locator('section.text article.unit').first();
|
||||
await expect(first.locator('h2')).toHaveText("Raison d'être");
|
||||
// The first sentence in italics, then two paragraphs.
|
||||
@@ -294,20 +314,20 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => {
|
||||
await expect(panel.getByText("Part exigée : les « pour » demandés au-delà de C, à 100 votants comme d'habitude.")).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// From the vote of an article: Unicité du compte is at 4 / 5, its row is marked.
|
||||
const e1 = page.locator('section.text article.unit', { hasText: 'Unicité du compte' });
|
||||
await e1.hover();
|
||||
await e1.getByRole('button', { name: 'Le vote' }).click();
|
||||
await expect(e1.getByRole('heading', { name: 'Inertie · 4 / 5 · haute' })).toBeVisible();
|
||||
// From the vote of an article: Unicité du compte, a seed proposal, is at 2 / 5, its row is marked.
|
||||
await deepRow(page, 'Unicité du compte').click();
|
||||
const e1 = page.getByRole('region', { name: 'Le vote de « Unicité du compte »' });
|
||||
await expect(e1.getByRole('heading', { name: 'Inertie · 2 / 5 · basse' })).toBeVisible();
|
||||
await e1.getByRole('button', { name: 'Réglages de la formule' }).click();
|
||||
await expect(panel.locator('tr.current')).toHaveCount(1);
|
||||
await expect(panel.locator('tr.current th')).toContainText('4');
|
||||
await expect(panel.locator('tr.current th')).toContainText('2');
|
||||
await expect(panel.locator('tr.current th')).toContainText('cet article');
|
||||
await expect(required).toHaveText(['50', '50', '50', '50', '50']);
|
||||
await expect(panel.getByText('Voix pour : à 0 votant, la participation de cette version.')).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// The five levels to choose from, in the test group; the change is voted at the article's level.
|
||||
// The five levels to choose from, in the test group (Réciprocité voted into force there,
|
||||
// beforeAll); the change is voted at the article's level.
|
||||
await page.goto(TEST_DOC);
|
||||
const e2 = page.locator('section.text article.unit', { hasText: 'Réciprocité' });
|
||||
await e2.hover();
|
||||
@@ -316,25 +336,25 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => {
|
||||
const levels = e2.getByRole('radiogroup', { name: 'Nouvelle inertie' }).getByRole('radio');
|
||||
await expect(levels).toHaveCount(5);
|
||||
// The current level is reachable — one stop in the tab order — and never chosen.
|
||||
await expect(levels.nth(3)).toHaveAttribute('aria-disabled', 'true');
|
||||
await expect(levels.nth(3)).toContainText('actuelle');
|
||||
await expect(levels.nth(1)).toHaveAttribute('aria-disabled', 'true');
|
||||
await expect(levels.nth(1)).toContainText('actuelle');
|
||||
await expect(e2.locator('[role="radio"][tabindex="0"]')).toHaveCount(1);
|
||||
await expect(levels.nth(3)).toHaveAttribute('tabindex', '0');
|
||||
await expect(e2.getByText("Un changement d'inertie se vote lui-même à l'inertie de l'article : 4 / 5 · haute.")).toBeVisible();
|
||||
await expect(levels.nth(1)).toHaveAttribute('tabindex', '0');
|
||||
await expect(e2.getByText("Un changement d'inertie se vote lui-même à l'inertie de l'article : 2 / 5 · basse.")).toBeVisible();
|
||||
// A click on it (Playwright refuses an aria-disabled target: the event is sent as is) chooses nothing.
|
||||
await levels.nth(3).dispatchEvent('click');
|
||||
await levels.nth(1).dispatchEvent('click');
|
||||
await expect(e2.locator('[role="radio"][aria-checked="true"]')).toHaveCount(0);
|
||||
// With the keyboard: the arrows go from level to level and choose it, wrapping round.
|
||||
await levels.nth(3).focus();
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect(levels.nth(4)).toBeFocused();
|
||||
await expect(levels.nth(4)).toHaveAttribute('aria-checked', 'true');
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await levels.nth(1).focus();
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await expect(levels.nth(0)).toBeFocused();
|
||||
await expect(levels.nth(0)).toHaveAttribute('aria-checked', 'true');
|
||||
await expect(e2.locator('[role="radio"][tabindex="0"]')).toHaveCount(1);
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await expect(levels.nth(4)).toBeFocused();
|
||||
await expect(levels.nth(4)).toHaveAttribute('aria-checked', 'true');
|
||||
await expect(e2.locator('[role="radio"][tabindex="0"]')).toHaveCount(1);
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect(levels.nth(0)).toHaveAttribute('aria-checked', 'true');
|
||||
// Or with a click.
|
||||
await levels.nth(4).click();
|
||||
await expect(levels.nth(4)).toHaveAttribute('aria-checked', 'true');
|
||||
@@ -342,5 +362,5 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => {
|
||||
// Its own vote unfolds, in the list of proposals.
|
||||
const change = page.getByRole('region', { name: 'Le vote de « inertie vers 5 / 5 · très haute »' });
|
||||
await expect(change).toBeVisible();
|
||||
await expect(change.getByText('inertie 4 / 5 · haute', { exact: true })).toBeVisible();
|
||||
await expect(change.getByText('inertie 2 / 5 · basse', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
+15
-19
@@ -1,9 +1,12 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { ALICE, chain, CHARLIE, DOC, person, shot, signed, signIn, TEST_DOC, VOTE_ACCOUNT, wallet } from './helpers';
|
||||
import { ALICE, chain, CHARLIE, deepRow, DOC, person, seedsInForce, shot, signed, signIn, TEST_DOC, VOTE_ACCOUNT, wallet } from './helpers';
|
||||
|
||||
// Lot B — the vote (Yvv, 2026-10-30): unfolded under the article, grouped in a
|
||||
// basket, and on the Ğ1 chain where the electorate is the web of trust. The chain
|
||||
// here is simulated (e2e/chain-mock.mjs): never a real transaction.
|
||||
// here is simulated (e2e/chain-mock.mjs): never a real transaction. In Monnaie libre
|
||||
// nothing is in force at the start (the seed proposals, ADR 0011): their vote unfolds in
|
||||
// the list of proposals; in the test group they were voted into force.
|
||||
test.beforeAll(async ({ browser }) => seedsInForce(browser));
|
||||
|
||||
test('C21 — le vote déplié sous l’article : ton vote, les totaux, le seuil', async ({ page, browser }) => {
|
||||
await signIn(page, 'Chiara');
|
||||
@@ -116,19 +119,17 @@ test('C23 — le vote par la chaîne Ğ1 : un geste, confirmé par l’indexeur
|
||||
await expect(panel.getByText("Rien dans le panier pour l'instant.")).toBeVisible();
|
||||
await panel.getByRole('button', { name: 'Fermer le panneau' }).click();
|
||||
|
||||
// Under the article: her vote, from the chain, and the totals.
|
||||
const e1 = page.locator('section.text article.unit', { hasText: 'Unicité du compte' });
|
||||
await e1.hover();
|
||||
await e1.getByRole('button', { name: 'Le vote' }).click();
|
||||
// Under the seed proposal: her vote, from the chain, and the totals.
|
||||
const e1 = deepRow(page, 'Unicité du compte');
|
||||
await e1.click();
|
||||
const fold1 = page.getByRole('region', { name: 'Le vote de « Unicité du compte »' });
|
||||
await expect(fold1.locator('.voted')).toHaveText('Ton vote : pour · inscrit sur la chaîne Ğ1');
|
||||
await expect(fold1.getByText('1 pour · 0 contre')).toBeVisible();
|
||||
|
||||
// The last one stands, in the order of the chain: a « contre » included first, but seen late
|
||||
// by the indexer, never overrides the withdrawal included after it.
|
||||
const e2 = page.locator('section.text article.unit', { hasText: 'Réciprocité' });
|
||||
await e2.hover();
|
||||
await e2.getByRole('button', { name: 'Le vote' }).click();
|
||||
const e2 = deepRow(page, 'Réciprocité');
|
||||
await e2.click();
|
||||
const fold2 = page.getByRole('region', { name: 'Le vote de « Réciprocité »' });
|
||||
await control('hide');
|
||||
await fold2.getByRole('button', { name: 'Contre', exact: true }).click();
|
||||
@@ -147,18 +148,15 @@ test('C23 — le vote par la chaîne Ğ1 : un geste, confirmé par l’indexeur
|
||||
await expect(fold2.getByText('0 pour · 0 contre')).toBeVisible();
|
||||
// One vote unfolded at a time (an accordion): back to the first, untouched.
|
||||
await expect(fold1).toHaveCount(0);
|
||||
await e1.hover();
|
||||
await e1.getByRole('button', { name: 'Le vote' }).click();
|
||||
await e1.click();
|
||||
await expect(fold1.getByText('1 pour · 0 contre')).toBeVisible();
|
||||
await expect(fold2).toHaveCount(0);
|
||||
|
||||
// Without a wallet in the browser: Ğecko, a link and a QR code; the page waits for the chain.
|
||||
const other = await person(browser, 'Maëlle');
|
||||
await other.goto(DOC);
|
||||
const raison = other.locator('section.text article.unit').first();
|
||||
await raison.hover();
|
||||
await raison.getByRole('button', { name: 'Le vote' }).click();
|
||||
await raison.locator('section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
await deepRow(other, "Raison d'être").click();
|
||||
await other.locator('ul.deep section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
const sheet = other.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' });
|
||||
await expect(sheet.getByText("Aucun portefeuille n'est branché à ce navigateur", { exact: false })).toBeVisible();
|
||||
await sheet.getByRole('button', { name: 'Passer par Ğecko' }).click();
|
||||
@@ -192,10 +190,8 @@ test('C24 — sans compte de vote, le vote par la chaîne est fermé et le dit ;
|
||||
await page.goto(DOC);
|
||||
await expect(page.getByRole('button', { name: /^Voter en lot/ })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: 'Voter cette section' })).toHaveCount(0);
|
||||
const first = page.locator('section.text article.unit').first();
|
||||
await first.hover();
|
||||
await first.getByRole('button', { name: 'Le vote' }).click();
|
||||
const fold = first.locator('section.fold');
|
||||
await page.locator('ul.deep button.row').first().click();
|
||||
const fold = page.locator('ul.deep section.fold');
|
||||
await expect(fold.getByText("Le vote par la chaîne Ğ1 n'est pas encore ouvert : le compte qui reçoit les votes reste à créer.", { exact: false })).toBeVisible();
|
||||
await expect(fold.getByRole('button', { name: 'Pour', exact: true })).toHaveCount(0);
|
||||
// The totals and the threshold stay in sight.
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
{/snippet}
|
||||
|
||||
{#snippet marks(p: Proposal)}
|
||||
{#if p.seedProposal}<span class="tag water" title={t('doc.seedProposal.long')}>{t('doc.seedProposal')}</span>{/if}
|
||||
{#if p.evaluation?.wellRated}<span class="tag water">{t('deep.wellrated')}</span>{/if}
|
||||
{#if p.blocked}<span class="tag ochre">{t('deep.held')}</span>{/if}
|
||||
{#if p.myVoteStale}<span class="tag ochre">{t('deep.stale')}</span>{/if}
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
<span class="text">
|
||||
<span class="eyebrow">{v.status === 'in_force' ? t('panel.inforce') : t('panel.proposal')}</span>
|
||||
<span class="title">{nameOfVersion(v)}</span>
|
||||
<span class="faint small">{v.status === 'in_force' ? (v.seedMinimal ? t('doc.seed') : '') : relationOf(v, unit)} · {t('tally.short', { for: v.tally.for, against: v.tally.against })}{#if v.myVote && !v.myVoteStale} · {t('basket.mine', { choice: t(`vote.choice.${v.myVote}`) })}{/if}{#if v.blocked} · <b class="held">{t('deep.held')}</b>{/if}</span>
|
||||
<span class="faint small">{v.status === 'in_force' ? (v.seedMinimal ? t('doc.seed') : '') : v.seedProposal ? t('doc.seedProposal') : relationOf(v, unit)} · {t('tally.short', { for: v.tally.for, against: v.tally.against })}{#if v.myVote && !v.myVoteStale} · {t('basket.mine', { choice: t(`vote.choice.${v.myVote}`) })}{/if}{#if v.blocked} · <b class="held">{t('deep.held')}</b>{/if}</span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -238,6 +238,7 @@
|
||||
<h3>{p.kind === 'text' || p.kind === 'title' ? text.title : titleOf(p, unit)}</h3>
|
||||
<p class="meta faint">
|
||||
{#if p.seedMinimal}<span class="tag water" title={t('doc.seed.long')}>{t('doc.seed')}</span>
|
||||
{:else if p.seedProposal}<span class="tag water" title={t('doc.seedProposal.long')}>{t('doc.seedProposal')}</span>
|
||||
{:else if p.confirmedAt}<span class="tag moss">{t('doc.confirmed')}</span>
|
||||
{:else if p.enteredAt && p.status === 'in_force'}<span class="tag moss">{t('doc.adopted', { date: i18n.date(p.enteredAt) })}</span>{/if}
|
||||
{#if p.blocked}<span class="tag ochre">{t('deep.held')}</span>{/if}
|
||||
|
||||
@@ -123,6 +123,8 @@ export const en: Messages = {
|
||||
'doc.seed.long': 'Came into force without a vote, so as not to start from a blank page. It remains open for voting, like any text.',
|
||||
'doc.seed.votes.one': 'seed a minima · {n} vote',
|
||||
'doc.seed.votes.other': 'seed a minima · {n} votes',
|
||||
'doc.seedProposal': 'Seed proposal',
|
||||
'doc.seedProposal.long': 'A starting text put forward to start the vote. It is not in force: it comes into force through the vote, like any proposal.',
|
||||
'doc.confirmed': 'confirmed by vote',
|
||||
'doc.adopted': 'adopted on {date}',
|
||||
'doc.action.vote': 'Voting',
|
||||
|
||||
@@ -123,6 +123,8 @@ export const es: Messages = {
|
||||
'doc.seed.long': 'Entró en vigor sin votación, para no partir de una página en blanco. Sigue en votación, como todo texto.',
|
||||
'doc.seed.votes.one': 'seed a minima · {n} voto',
|
||||
'doc.seed.votes.other': 'seed a minima · {n} votos',
|
||||
'doc.seedProposal': 'Propuesta seed',
|
||||
'doc.seedProposal.long': 'Texto de partida destacado para lanzar la votación. No está en vigor: entra en vigor por la votación, como toda propuesta.',
|
||||
'doc.confirmed': 'confirmado por la votación',
|
||||
'doc.adopted': 'adoptado el {date}',
|
||||
'doc.action.vote': 'Votación',
|
||||
|
||||
@@ -127,6 +127,8 @@ export const fr = {
|
||||
'doc.seed.long': "Entré en vigueur sans vote, pour ne pas partir d'une page blanche. Il reste sous vote, comme tout texte.",
|
||||
'doc.seed.votes.one': 'seed a minima · {n} vote',
|
||||
'doc.seed.votes.other': 'seed a minima · {n} votes',
|
||||
'doc.seedProposal': 'Proposition seed',
|
||||
'doc.seedProposal.long': "Texte de départ mis en avant pour lancer le vote. Il n'est pas en vigueur : il y entre par le vote, comme toute proposition.",
|
||||
'doc.confirmed': 'confirmé par le vote',
|
||||
'doc.adopted': 'adopté le {date}',
|
||||
'doc.action.vote': 'Le vote',
|
||||
|
||||
@@ -107,6 +107,8 @@ export interface Proposal {
|
||||
enteredAt: string | null;
|
||||
confirmedAt: string | null;
|
||||
seedMinimal: boolean;
|
||||
/** A seed proposal (ADR 0011): open, put forward by the seed to start the vote, not in force. */
|
||||
seedProposal?: boolean;
|
||||
tally: Tally;
|
||||
evaluation: Evaluation | null;
|
||||
votedAtInertia: Inertia;
|
||||
|
||||
@@ -192,6 +192,8 @@
|
||||
|
||||
<section class="text" aria-label={t('doc.view.inforce')}>
|
||||
{#if doc.mode === 'inforce' || !doc.preview}
|
||||
<!-- No article in force (the seed proposals are not, ADR 0011): said once, above the orders. -->
|
||||
{#if !doc.inForce.length}<p class="muted">{t('doc.empty')}</p>{/if}
|
||||
{#each doc.inForceBySection as s, i (`${i}:${s.section}`)}
|
||||
{#each s.units as u (u.id)}
|
||||
{#if u.inForce}
|
||||
@@ -224,8 +226,6 @@
|
||||
{#snippet fold()}{@render foldOf(o, op)}{/snippet}
|
||||
</OrderModule>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="muted">{t('doc.empty')}</p>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each doc.preview as pu (pu.unitId)}
|
||||
|
||||
Reference in New Issue
Block a user