forked from yvv/decision
Lot U : C35 à C37 écrits avant le code et vus rouges (poignées et vote, oui pour passer · non pour bloquer, quorum de l'auteur) ; leurs mots en fr, en, es ; needOf ; le vote simulé ne promet plus son effacement à l'ouverture de la chaîne
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,17 @@ export function deepRow(page: Page, title: string) {
|
||||
return page.locator('ul.deep button.row').filter({ has: page.locator('span.title').getByText(title, { exact: true }) });
|
||||
}
|
||||
|
||||
/**
|
||||
* « Propositions en cours » shows its sections folded (lot U): « Tout déplier » opens them
|
||||
* all, so that a line can be clicked.
|
||||
*/
|
||||
export async function unfold(page: Page) {
|
||||
const all = page.getByRole('button', { name: 'Tout déplier' });
|
||||
await expect(all).toBeVisible();
|
||||
await all.click();
|
||||
await expect(page.getByRole('button', { name: 'Tout replier' })).toBeVisible();
|
||||
}
|
||||
|
||||
/** 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;
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import { devices, expect, test, type Page } from '@playwright/test';
|
||||
import { deepRow, DOC, person, propose, seedsInForce, shot, signIn, TEST_DOC, testDoc, unfold } from './helpers';
|
||||
|
||||
// Lot U — the reader (Yvv, 2026-11-02: « la répétition de "ordonnancement" est nulle. Il
|
||||
// faudrait que ce soit en fin de section, qui ajoute des "poignées" aux modules de la section.
|
||||
// Et permette de voter. » ; « combien il faut de oui pour passer et combien de non pour
|
||||
// maintenir l'opposition »). Run after modules.spec (C25 reads the order of the fundamental
|
||||
// commitments as the seed gives it): here it is reordered, voted, and may enter into force.
|
||||
test.beforeAll(async ({ browser }) => seedsInForce(browser));
|
||||
|
||||
const EF = 'Engagements fondamentaux';
|
||||
const E1 = 'Unicité du compte';
|
||||
const E2 = 'Réciprocité';
|
||||
const ORDER = `Ordre des articles · ${EF}`;
|
||||
const CSRF = { 'x-requested-with': 'libredecision' };
|
||||
|
||||
/** Whether a version of a module of the test group is in force now. */
|
||||
async function moduleInForce(page: Page, proposalId: string): Promise<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 [d.modules.title, ...d.modules.orders].some((u) => u?.inForce?.id === proposalId);
|
||||
}
|
||||
|
||||
test('C35 — réordonner une section : les poignées sur ses articles, au glisser, au clavier, sans glisser, puis le vote', async ({ page, browser }) => {
|
||||
await signIn(page, 'Chiara');
|
||||
await page.goto(TEST_DOC);
|
||||
// The order closes its section in one line: its name, its weight, its state — never the list again.
|
||||
const line = page.getByRole('group', { name: ORDER });
|
||||
await expect(line).toBeVisible();
|
||||
await expect(line).toContainText('Ordre de la section');
|
||||
await expect(line).toContainText('inertie 2 / 5 · basse');
|
||||
await expect(line.getByText(E1)).toHaveCount(0);
|
||||
// Nowhere a block that repeats the articles of a section, and no order among the proposals.
|
||||
await expect(page.locator('section.text').getByText('Ordre des articles ·', { exact: false })).toHaveCount(0);
|
||||
await expect(page.locator('ul.deep').getByText('Ordre des articles', { exact: false })).toHaveCount(0);
|
||||
await line.scrollIntoViewIfNeeded();
|
||||
await shot(page, 'c35-1-ligne-ordre');
|
||||
|
||||
// « Réordonner »: the articles of the section fold to their titles, a handle before each, in place.
|
||||
await line.getByRole('button', { name: 'Réordonner' }).click();
|
||||
await expect(page.locator('aside.panel')).toHaveCount(0);
|
||||
const list = page.locator('section.text').getByRole('list', { name: 'Nouvel ordre des articles' });
|
||||
const names = list.locator('button.item .name');
|
||||
await expect(names).toHaveText([EF, E1, E2]);
|
||||
// The articles in force of the section are in the list now, not twice; the one only proposed says so.
|
||||
await expect(page.locator('section.text article.unit', { hasText: E1 })).toHaveCount(0);
|
||||
await expect(list.locator('li', { hasText: EF })).toContainText('proposé');
|
||||
await expect(list.locator('.grip')).toHaveCount(3);
|
||||
const submit = page.getByRole('button', { name: 'Proposer cet ordre' });
|
||||
await expect(submit).toBeDisabled();
|
||||
await shot(page, 'c35-2-poignees');
|
||||
|
||||
// By dragging its handle: Réciprocité onto the place of Unicité du compte.
|
||||
await list.locator('li', { hasText: E2 }).locator('.grip').dragTo(list.locator('li', { hasText: E1 }));
|
||||
await expect(names).toHaveText([EF, E2, E1]);
|
||||
await expect(page.getByText(`« ${E2} » en place 2 sur 3.`)).toBeAttached();
|
||||
await expect(submit).toBeEnabled();
|
||||
// With the keyboard: the article keeps the focus, the arrows move it.
|
||||
const items = list.locator('button.item');
|
||||
await items.nth(1).focus();
|
||||
await page.keyboard.press('ArrowUp');
|
||||
await expect(names).toHaveText([E2, EF, E1]);
|
||||
await expect(items.nth(0)).toBeFocused();
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await expect(names).toHaveText([EF, E2, E1]);
|
||||
// Without dragging: its arrows, or an article then its new place.
|
||||
await list.getByRole('button', { name: `Descendre « ${E2} »` }).click();
|
||||
await expect(names).toHaveText([EF, E1, E2]);
|
||||
await expect(submit).toBeDisabled();
|
||||
await items.nth(2).click();
|
||||
await expect(items.nth(2)).toHaveAttribute('aria-pressed', 'true');
|
||||
await items.nth(1).click();
|
||||
await expect(names).toHaveText([EF, E2, E1]);
|
||||
// « Annuler » gives the section back as it read.
|
||||
await page.getByRole('button', { name: 'Annuler' }).click();
|
||||
await expect(list).toHaveCount(0);
|
||||
await expect(page.locator('section.text article.unit', { hasText: E1 })).toHaveCount(1);
|
||||
await line.getByRole('button', { name: 'Réordonner' }).click();
|
||||
await list.getByRole('button', { name: `Monter « ${E2} »` }).click();
|
||||
await expect(names).toHaveText([EF, E2, E1]);
|
||||
|
||||
// Then the vote: the order goes to the vote, and its vote unfolds at once, in the same place.
|
||||
await submit.click();
|
||||
await expect(page.getByText('Ordre proposé au vote.')).toBeVisible();
|
||||
const fold = line.locator('section.fold');
|
||||
await expect(fold).toHaveAttribute('aria-label', `Le vote de « ${ORDER} »`);
|
||||
await expect(fold.getByRole('list', { name: 'Ordre proposé' }).locator('li')).toHaveText([EF, E2, E1]);
|
||||
await expect(line).toContainText('1 ordre proposé');
|
||||
const pid = ((await fold.getAttribute('id')) ?? '').replace(/^vote-/, '');
|
||||
await fold.getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
await expect(page.getByText('Vote enregistré.')).toBeVisible();
|
||||
await shot(page, 'c35-3-ordre-vote');
|
||||
// Into force, by her voice or a second one: the section reads in its new order.
|
||||
if (!(await moduleInForce(page, pid))) {
|
||||
const yvv = await person(browser, 'Yvv');
|
||||
const r = await yvv.request.post(`/api/proposals/${pid}/vote`, { data: { choice: 'for' }, headers: CSRF });
|
||||
expect(r.ok()).toBe(true);
|
||||
await yvv.context().close();
|
||||
}
|
||||
expect(await moduleInForce(page, pid)).toBe(true);
|
||||
await page.goto(TEST_DOC);
|
||||
const titles = await page.locator('section.text article.unit h2').allTextContents();
|
||||
expect(titles.indexOf(E2)).toBeGreaterThan(-1);
|
||||
expect(titles.indexOf(E2)).toBeLessThan(titles.indexOf(E1));
|
||||
await expect(line).toContainText(/adopté le/);
|
||||
});
|
||||
|
||||
test('C35 — sur un téléphone : réordonner une section où rien n’est en vigueur, la liste tient, les cibles se touchent', async ({ browser }) => {
|
||||
const ctx = await browser.newContext({ ...devices['Pixel 7'], locale: 'fr-FR' });
|
||||
const page = await ctx.newPage();
|
||||
await signIn(page, 'Chiara');
|
||||
await page.goto(DOC);
|
||||
// Nothing of the technical commitments is in the text: their order closes their section in the list.
|
||||
await unfold(page);
|
||||
const line = page.getByRole('group', { name: 'Ordre des articles · Engagements techniques' });
|
||||
await line.getByRole('button', { name: 'Réordonner' }).click();
|
||||
const list = page.getByRole('list', { name: 'Nouvel ordre des articles' });
|
||||
const items = list.locator('button.item');
|
||||
await expect(items).toHaveCount(11);
|
||||
await items.nth(3).click();
|
||||
await items.nth(0).click();
|
||||
await expect(page.getByRole('button', { name: 'Proposer cet ordre' })).toBeEnabled();
|
||||
const holds = await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1);
|
||||
expect(holds).toBe(true);
|
||||
for (const target of [items.nth(0), list.locator('.grip').first(), list.getByRole('button', { name: /^Monter/ }).nth(1)]) {
|
||||
const box = await target.boundingBox();
|
||||
expect(box?.height ?? 0).toBeGreaterThanOrEqual(44);
|
||||
}
|
||||
await list.scrollIntoViewIfNeeded();
|
||||
await shot(page, 'c35-4-poignees-mobile');
|
||||
await page.getByRole('button', { name: 'Annuler' }).click();
|
||||
await ctx.close();
|
||||
});
|
||||
|
||||
test('C36 — combien de oui pour passer, combien de non pour bloquer : sous l’article déplié, et en bref dans la liste', async ({ page }) => {
|
||||
await signIn(page, 'Chiara');
|
||||
// Before any vote (Monnaie libre): the Raison d'être, a seed proposal at level 2, quorum 12.
|
||||
await page.goto(DOC);
|
||||
const i1 = page.locator('section.text article.unit', { hasText: "Raison d'être" }).first();
|
||||
await i1.hover();
|
||||
await i1.getByRole('button', { name: 'Le vote' }).click();
|
||||
const fold = page.getByRole('region', { name: "Le vote de « Raison d'être »" });
|
||||
const need = fold.getByText('9 oui sur 12 votants pour passer', { exact: true });
|
||||
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 ».');
|
||||
// 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');
|
||||
|
||||
// With votes (the evaluation as the server gives it, ADR 0013): 9 « pour », 4 « contre » at
|
||||
// level 2, quorum 12, do not pass — 11 « pour » would; 4 « contre » hold the 9 in check.
|
||||
const title = 'Vérification personnelle de la clé';
|
||||
const doc = await testDoc(page);
|
||||
const target = doc.units.find((u) => u.proposals.some((p) => p.title === title));
|
||||
if (!target) throw new Error(`no « ${title} »`);
|
||||
let shape: 'held' | 'quorum' = 'held';
|
||||
await page.route(/\/api\/docs\/[^/?]+$/, async (route) => {
|
||||
const res = await route.fetch();
|
||||
const body = await res.json();
|
||||
for (const u of body.units) {
|
||||
for (const p of u.proposals) {
|
||||
if (p.title !== title || p.status !== 'open') continue;
|
||||
p.tally = shape === 'held' ? { for: 9, against: 4 } : { for: 3, against: 0 };
|
||||
p.evaluation = {
|
||||
...p.evaluation,
|
||||
cast: shape === 'held' ? 13 : 3,
|
||||
quorum: 12,
|
||||
required: shape === 'held' ? 10 : 9,
|
||||
reached: false,
|
||||
missing: shape === 'held' ? 2 : 6,
|
||||
yesNeeded: shape === 'held' ? 11 : 9,
|
||||
noNeeded: shape === 'held' ? 4 : 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
await route.fulfill({ response: res, json: body });
|
||||
});
|
||||
await page.goto(TEST_DOC);
|
||||
await unfold(page);
|
||||
const row = deepRow(page, title);
|
||||
await expect(row).toContainText('passe à 11 oui · bloquée à 4 non');
|
||||
await row.click();
|
||||
const theirs = page.getByRole('region', { name: `Le vote de « ${title} »` });
|
||||
await expect(theirs.getByText('11 oui pour passer · 4 non pour bloquer', { exact: true })).toBeVisible();
|
||||
await expect(theirs.getByText('Il manque 2 voix pour.')).toBeVisible();
|
||||
await shot(page, 'c36-1-oui-non');
|
||||
// Below the quorum, the voters missing hold it alone: no « non » is needed.
|
||||
shape = 'quorum';
|
||||
await page.reload();
|
||||
await expect(theirs.getByText('9 oui pour passer · le quorum la bloque', { exact: true })).toBeVisible();
|
||||
await expect(deepRow(page, title)).toContainText('passe à 9 oui');
|
||||
await expect(deepRow(page, title)).not.toContainText('bloquée');
|
||||
});
|
||||
|
||||
test('C37 — le quorum d’un article : son auteur le fixe avant le premier vote, revient au défaut, et lit les refus', async ({ page, browser }) => {
|
||||
await signIn(page, 'Chiara');
|
||||
const title = 'Quorum choisi par son autrice';
|
||||
const id = await propose(page, title, 'Je fixe le quorum de cet article avant que quiconque vote.');
|
||||
await page.goto(`${TEST_DOC}?vote=${id}`);
|
||||
const fold = page.getByRole('region', { name: `Le vote de « ${title} »` });
|
||||
const quorum = fold.getByRole('region', { name: /^Quorum/ });
|
||||
await expect(quorum).toBeVisible();
|
||||
// The default of its level, recalled; the test group asks 1.
|
||||
await expect(quorum.getByText('Par défaut, celui de son cran : 1 votant.')).toBeVisible();
|
||||
await expect(quorum.getByRole('button', { name: /^Revenir au défaut/ })).toHaveCount(0);
|
||||
const field = quorum.getByRole('spinbutton', { name: "Quorum de l'article" });
|
||||
await expect(field).toHaveValue('1');
|
||||
await field.fill('3');
|
||||
await quorum.getByRole('button', { name: 'Fixer ce quorum' }).click();
|
||||
await expect(page.getByText('Quorum fixé : 3 votants.')).toBeVisible();
|
||||
await expect(fold.getByText(/oui sur 3 votants pour passer$/)).toBeVisible();
|
||||
await shot(page, 'c37-1-quorum');
|
||||
// Back to the default.
|
||||
await quorum.getByRole('button', { name: 'Revenir au défaut (1)' }).click();
|
||||
await expect(page.getByText('Quorum revenu au défaut : 1 votant.')).toBeVisible();
|
||||
await expect(fold.getByText(/oui sur 1 votant pour passer$/)).toBeVisible();
|
||||
// Out of bounds: refused (400), said in her words.
|
||||
await field.fill('100000');
|
||||
await quorum.getByRole('button', { name: 'Fixer ce quorum' }).click();
|
||||
await expect(quorum.getByRole('alert')).toHaveText(/^Ce quorum sort des bornes/);
|
||||
// Someone else never sees it.
|
||||
const yvv = await person(browser, 'Yvv');
|
||||
await yvv.goto(`${TEST_DOC}?vote=${id}`);
|
||||
await expect(yvv.getByRole('region', { name: `Le vote de « ${title} »` })).toBeVisible();
|
||||
await expect(yvv.getByRole('region', { name: /^Quorum/ })).toHaveCount(0);
|
||||
// A vote arrives while her page is open: refused (409), said in her words; then it is gone.
|
||||
const r = await yvv.request.post(`/api/proposals/${id}/vote`, { data: { choice: 'against' }, headers: CSRF });
|
||||
expect(r.ok()).toBe(true);
|
||||
await field.fill('2');
|
||||
await quorum.getByRole('button', { name: 'Fixer ce quorum' }).click();
|
||||
await expect(quorum.getByRole('alert')).toHaveText("Quelqu'un a déjà voté sur cet article : son quorum ne change plus.");
|
||||
await page.reload();
|
||||
await expect(fold).toBeVisible();
|
||||
await expect(fold.getByRole('region', { name: /^Quorum/ })).toHaveCount(0);
|
||||
await yvv.context().close();
|
||||
});
|
||||
+49
-3
@@ -133,6 +133,9 @@ export const en: Messages = {
|
||||
'doc.basket': 'Vote in bulk',
|
||||
'doc.basket.count': 'Vote in bulk · {n}',
|
||||
'doc.empty': 'No article is in force. The proposals below move up as soon as they reach their threshold.',
|
||||
'doc.empty.seeds': 'No article is in force yet. Start with the seed proposals: they come into force by the vote, like any proposal.',
|
||||
'doc.others.one': '{n} other version open',
|
||||
'doc.others.other': '{n} other versions open',
|
||||
'doc.published': 'Published to the text repository',
|
||||
'doc.publish.pending': 'Publication pending',
|
||||
|
||||
@@ -158,6 +161,11 @@ export const en: Messages = {
|
||||
'deep.versions.other': '{n} versions',
|
||||
'deep.version': 'version {n}',
|
||||
'deep.lead': 'leading: {label}',
|
||||
'deep.section.none': 'No section',
|
||||
'deep.section.count.one': '{n} article proposed',
|
||||
'deep.section.count.other': '{n} articles proposed',
|
||||
'deep.unfold': 'Unfold all',
|
||||
'deep.fold': 'Fold all',
|
||||
'section.introduction': 'Introduction',
|
||||
'section.fondamental': 'Fundamental commitments',
|
||||
'section.technique': 'Technical commitments',
|
||||
@@ -181,19 +189,28 @@ export const en: Messages = {
|
||||
'module.order.rule': 'A new article goes at the end of its section. If it arrives while an order is under vote, it is added at its end, and the votes stay.',
|
||||
'order.propose': 'Propose another order',
|
||||
'order.edit.label': 'New order of the articles',
|
||||
'order.edit.help': 'Click an article, then its new place, or use its arrows. With the keyboard: Enter to choose it, up and down arrows to move it.',
|
||||
'order.edit.help': 'Take an article by its handle and drop it in its place. Without dragging: click an article, then its new place, or use its arrows. With the keyboard: up and down arrows.',
|
||||
'order.edit.up': 'Move “{title}” up',
|
||||
'order.edit.down': 'Move “{title}” down',
|
||||
'order.edit.chosen': '“{title}” chosen: click its new place.',
|
||||
'order.edit.moved': '“{title}” now in place {n} of {total}.',
|
||||
'order.edit.submit': 'Propose this order',
|
||||
'order.edit.unchanged': 'This is the order in force: move at least one article.',
|
||||
'order.edit.exists': 'This order is already proposed: vote for it rather than proposing it twice.',
|
||||
'order.edit.vote': 'Vote on this order',
|
||||
'order.edit.proposed': 'proposed',
|
||||
'order.line': 'Order of the section',
|
||||
'order.reorder': 'Reorder',
|
||||
'order.proposed.one': '{n} order proposed',
|
||||
'order.proposed.other': '{n} orders proposed',
|
||||
'order.edit.note': 'An order is voted at inertia {level}, like an article: unfolded, in the basket, on the Ğ1 chain in Monnaie libre.',
|
||||
'order.submitted': 'Order submitted to vote.',
|
||||
'title.propose': 'Propose another title',
|
||||
'title.propose.label': 'New title',
|
||||
'title.propose.note': 'The title of the document is voted at inertia {level}. Its translations stay editorial: they will read “to review”.',
|
||||
'title.submitted': 'Title submitted to vote.',
|
||||
'title.proposed.one': '{n} title proposed',
|
||||
'title.proposed.other': '{n} titles proposed',
|
||||
|
||||
'tally.short': '{for} for · {against} against',
|
||||
'tally.required': 'Threshold: {n} votes in favor',
|
||||
@@ -230,6 +247,18 @@ export const en: Messages = {
|
||||
'vote.against': 'Against',
|
||||
'vote.retract': 'Withdraw my vote',
|
||||
'vote.saved': 'Vote saved.',
|
||||
'vote.saved.simulated': 'Vote simulated.',
|
||||
'vote.simulated.mark': 'simulated vote',
|
||||
'vote.others': 'Other versions',
|
||||
'vote.need': '{yes} yes to pass · {no} no to block',
|
||||
'vote.need.quorum': '{yes} yes to pass · the quorum blocks it',
|
||||
'vote.need.first.one': '{yes} yes out of {quorum} voter to pass',
|
||||
'vote.need.first.other': '{yes} yes out of {quorum} voters to pass',
|
||||
'vote.need.short': 'passes at {yes} yes · blocked at {no} no',
|
||||
'vote.need.short.quorum': 'passes at {yes} yes',
|
||||
'vote.need.short.first': '{yes} yes out of {quorum}',
|
||||
'vote.need.title.one': 'Quorum: {n} voter. Below it, the missing voters count as “no”.',
|
||||
'vote.need.title.other': 'Quorum: {n} voters. Below it, the missing voters count as “no”.',
|
||||
'vote.retracted': 'Vote withdrawn.',
|
||||
'vote.stale': 'The text has changed in substance since you voted: your vote no longer counts until you reconfirm it.',
|
||||
'vote.reconfirm': 'Reconfirm: {choice}',
|
||||
@@ -256,7 +285,7 @@ export const en: Messages = {
|
||||
'vote.mine.saved': 'saved',
|
||||
'vote.mine.pending': 'Waiting for the Ğ1 chain: {choice}.',
|
||||
'vote.mine.simulated': 'simulated',
|
||||
'vote.simulated.note': 'The Ğ1 chain is not open yet: your vote is simulated. It counts as a vote, marked “simulated”, until it is erased when the chain opens.',
|
||||
'vote.simulated.note': 'The Ğ1 chain is not open yet: your vote is simulated. It counts as a vote, marked “simulated”, until a human decision erases the simulated votes.',
|
||||
'vote.tally.simulated.one': 'of which {n} simulated',
|
||||
'vote.tally.simulated.other': 'of which {n} simulated',
|
||||
'vote.mine.refused': 'Your last vote was not counted: {reason}',
|
||||
@@ -270,6 +299,21 @@ export const en: Messages = {
|
||||
'vote.chain.off': 'Voting through the Ğ1 chain is not open yet: the account that receives the votes is still to be created. You can read, propose and raise an objection.',
|
||||
'vote.members': 'Only members of this group vote here.',
|
||||
'vote.details': 'Typos, translations, objections, history',
|
||||
'quorum.heading.one': 'Quorum · {n} voter',
|
||||
'quorum.heading.other': 'Quorum · {n} voters',
|
||||
'quorum.default.one': 'By default, that of its level: {n} voter.',
|
||||
'quorum.default.other': 'By default, that of its level: {n} voters.',
|
||||
'quorum.explain': 'You set the quorum of this article as long as nobody has voted. Below it, the missing voters count as “no”.',
|
||||
'quorum.field': 'Quorum of the article',
|
||||
'quorum.set': 'Set this quorum',
|
||||
'quorum.reset': 'Back to the default ({n})',
|
||||
'quorum.done.one': 'Quorum set: {n} voter.',
|
||||
'quorum.done.other': 'Quorum set: {n} voters.',
|
||||
'quorum.reset.done.one': 'Quorum back to the default: {n} voter.',
|
||||
'quorum.reset.done.other': 'Quorum back to the default: {n} voters.',
|
||||
'quorum.error.quorum': 'This quorum is out of bounds: at least 2 voters (1 in the test group), at most {w}, the electorate.',
|
||||
'quorum.error.voted': 'Someone has already voted on this article: its quorum no longer changes.',
|
||||
'quorum.error.forbidden': 'Only the person who wrote the article sets its quorum, before the first vote.',
|
||||
'ballot.reason.not_member': 'the wallet that signed is not that of a member of the Ğ1 web of trust.',
|
||||
'ballot.reason.no_identity': 'the wallet that signed is linked to no Ğ1 identity.',
|
||||
'ballot.reason.transfer': 'the transfer to the vote account is missing, or its amount is too low.',
|
||||
@@ -295,6 +339,8 @@ export const en: Messages = {
|
||||
'basket.empty': 'Nothing in the basket yet.',
|
||||
'basket.sent.one': '{n} vote saved.',
|
||||
'basket.sent.other': '{n} votes saved.',
|
||||
'basket.sent.simulated.one': '{n} vote simulated.',
|
||||
'basket.sent.simulated.other': '{n} votes simulated.',
|
||||
'basket.section': 'Vote on this section',
|
||||
'basket.mine': 'your vote: {choice}',
|
||||
'basket.held': 'an objection holds it back: against or withdraw, no “for”',
|
||||
@@ -341,7 +387,7 @@ export const en: Messages = {
|
||||
'ballots.lines.other': '{n} votes',
|
||||
'ballots.status.pending': 'Waiting for the Ğ1 chain',
|
||||
'ballots.status.confirmed': 'Counted for the Ğ1 identity “{name}”',
|
||||
'ballots.status.simulated': 'Simulated: counted for “{name}” until the Ğ1 chain opens',
|
||||
'ballots.status.simulated': 'Simulated: counted for “{name}” until the simulated votes are erased',
|
||||
'ballots.status.refused': 'Not counted: {reason}',
|
||||
'ballots.status.expired': 'Not seen on the Ğ1 chain in time: it does not count',
|
||||
'ballots.block': 'block {n}',
|
||||
|
||||
+49
-3
@@ -133,6 +133,9 @@ export const es: Messages = {
|
||||
'doc.basket': 'Votar en bloque',
|
||||
'doc.basket.count': 'Votar en bloque · {n}',
|
||||
'doc.empty': 'Ningún artículo está en vigor. Las propuestas de abajo suben en cuanto alcanzan su umbral.',
|
||||
'doc.empty.seeds': 'Ningún artículo está aún en vigor. Empieza por las propuestas semilla: entran en vigor por el voto, como cualquier propuesta.',
|
||||
'doc.others.one': '{n} otra versión en curso',
|
||||
'doc.others.other': '{n} otras versiones en curso',
|
||||
'doc.published': 'Publicado en el repositorio de textos',
|
||||
'doc.publish.pending': 'Publicación pendiente',
|
||||
|
||||
@@ -158,6 +161,11 @@ export const es: Messages = {
|
||||
'deep.versions.other': '{n} versiones',
|
||||
'deep.version': 'versión {n}',
|
||||
'deep.lead': 'en cabeza: {label}',
|
||||
'deep.section.none': 'Sin sección',
|
||||
'deep.section.count.one': '{n} artículo propuesto',
|
||||
'deep.section.count.other': '{n} artículos propuestos',
|
||||
'deep.unfold': 'Desplegar todo',
|
||||
'deep.fold': 'Plegar todo',
|
||||
'section.introduction': 'Introducción',
|
||||
'section.fondamental': 'Compromisos fundamentales',
|
||||
'section.technique': 'Compromisos técnicos',
|
||||
@@ -181,19 +189,28 @@ export const es: Messages = {
|
||||
'module.order.rule': 'Un artículo nuevo se coloca al final de su sección. Si llega durante la votación de un orden, se añade al final, y los votos se mantienen.',
|
||||
'order.propose': 'Proponer otro orden',
|
||||
'order.edit.label': 'Nuevo orden de los artículos',
|
||||
'order.edit.help': 'Haz clic en un artículo y luego en su nuevo lugar, o usa sus flechas. Con el teclado: Intro para elegirlo, flechas arriba y abajo para moverlo.',
|
||||
'order.edit.help': 'Toma un artículo por su asa y suéltalo en su lugar. Sin arrastrar: haz clic en un artículo y luego en su nuevo lugar, o usa sus flechas. Con el teclado: flechas arriba y abajo.',
|
||||
'order.edit.up': 'Subir «{title}»',
|
||||
'order.edit.down': 'Bajar «{title}»',
|
||||
'order.edit.chosen': '«{title}» elegido: haz clic en su nuevo lugar.',
|
||||
'order.edit.moved': '«{title}» en el lugar {n} de {total}.',
|
||||
'order.edit.submit': 'Proponer este orden',
|
||||
'order.edit.unchanged': 'Es el orden en vigor: mueve al menos un artículo.',
|
||||
'order.edit.exists': 'Este orden ya está propuesto: vótalo en lugar de proponerlo dos veces.',
|
||||
'order.edit.vote': 'Votar este orden',
|
||||
'order.edit.proposed': 'propuesto',
|
||||
'order.line': 'Orden de la sección',
|
||||
'order.reorder': 'Reordenar',
|
||||
'order.proposed.one': '{n} orden propuesto',
|
||||
'order.proposed.other': '{n} órdenes propuestos',
|
||||
'order.edit.note': 'Un orden se vota con la inercia {level}, como un artículo: desplegado, en la cesta, por la cadena Ğ1 en Monnaie libre.',
|
||||
'order.submitted': 'Orden sometido a votación.',
|
||||
'title.propose': 'Proponer otro título',
|
||||
'title.propose.label': 'Nuevo título',
|
||||
'title.propose.note': 'El título del documento se vota con la inercia {level}. Sus traducciones siguen siendo editoriales: pasarán a «por revisar».',
|
||||
'title.submitted': 'Título sometido a votación.',
|
||||
'title.proposed.one': '{n} título propuesto',
|
||||
'title.proposed.other': '{n} títulos propuestos',
|
||||
|
||||
'tally.short': '{for} a favor · {against} en contra',
|
||||
'tally.required': 'Umbral: {n} votos a favor',
|
||||
@@ -230,6 +247,18 @@ export const es: Messages = {
|
||||
'vote.against': 'En contra',
|
||||
'vote.retract': 'Retirar mi voto',
|
||||
'vote.saved': 'Voto registrado.',
|
||||
'vote.saved.simulated': 'Voto simulado.',
|
||||
'vote.simulated.mark': 'voto simulado',
|
||||
'vote.others': 'Otras versiones',
|
||||
'vote.need': '{yes} sí para aprobar · {no} no para bloquear',
|
||||
'vote.need.quorum': '{yes} sí para aprobar · el quórum la bloquea',
|
||||
'vote.need.first.one': '{yes} sí de {quorum} votante para aprobar',
|
||||
'vote.need.first.other': '{yes} sí de {quorum} votantes para aprobar',
|
||||
'vote.need.short': 'aprueba con {yes} sí · bloqueada con {no} no',
|
||||
'vote.need.short.quorum': 'aprueba con {yes} sí',
|
||||
'vote.need.short.first': '{yes} sí de {quorum}',
|
||||
'vote.need.title.one': 'Quórum: {n} votante. Por debajo, los votantes que faltan cuentan como «no».',
|
||||
'vote.need.title.other': 'Quórum: {n} votantes. Por debajo, los votantes que faltan cuentan como «no».',
|
||||
'vote.retracted': 'Voto retirado.',
|
||||
'vote.stale': 'El texto ha cambiado de fondo desde tu voto: tu voto deja de contar hasta que lo reconfirmes.',
|
||||
'vote.reconfirm': 'Reconfirmar: {choice}',
|
||||
@@ -256,7 +285,7 @@ export const es: Messages = {
|
||||
'vote.mine.saved': 'registrado',
|
||||
'vote.mine.pending': 'Esperando a la cadena Ğ1: {choice}.',
|
||||
'vote.mine.simulated': 'simulado',
|
||||
'vote.simulated.note': 'La cadena Ğ1 aún no está abierta: tu voto es simulado. Cuenta como un voto, marcado «simulado», hasta que se borre al abrirse la cadena.',
|
||||
'vote.simulated.note': 'La cadena Ğ1 aún no está abierta: tu voto es simulado. Cuenta como un voto, marcado «simulado», mientras una decisión humana no borre los votos simulados.',
|
||||
'vote.tally.simulated.one': 'de ellos {n} simulado',
|
||||
'vote.tally.simulated.other': 'de ellos {n} simulados',
|
||||
'vote.mine.refused': 'Tu último voto no se contó: {reason}',
|
||||
@@ -270,6 +299,21 @@ export const es: Messages = {
|
||||
'vote.chain.off': 'La votación por la cadena Ğ1 aún no está abierta: falta crear la cuenta que recibe los votos. Puedes leer, proponer y hacer una objeción.',
|
||||
'vote.members': 'Aquí solo votan los miembros de este grupo.',
|
||||
'vote.details': 'Erratas, traducciones, objeciones, historial',
|
||||
'quorum.heading.one': 'Quórum · {n} votante',
|
||||
'quorum.heading.other': 'Quórum · {n} votantes',
|
||||
'quorum.default.one': 'Por defecto, el de su nivel: {n} votante.',
|
||||
'quorum.default.other': 'Por defecto, el de su nivel: {n} votantes.',
|
||||
'quorum.explain': 'Fijas el quórum de este artículo mientras nadie haya votado. Por debajo, los votantes que faltan cuentan como «no».',
|
||||
'quorum.field': 'Quórum del artículo',
|
||||
'quorum.set': 'Fijar este quórum',
|
||||
'quorum.reset': 'Volver al valor por defecto ({n})',
|
||||
'quorum.done.one': 'Quórum fijado: {n} votante.',
|
||||
'quorum.done.other': 'Quórum fijado: {n} votantes.',
|
||||
'quorum.reset.done.one': 'Quórum de vuelta al valor por defecto: {n} votante.',
|
||||
'quorum.reset.done.other': 'Quórum de vuelta al valor por defecto: {n} votantes.',
|
||||
'quorum.error.quorum': 'Este quórum se sale de los límites: al menos 2 votantes (1 en el grupo de prueba), como mucho {w}, el electorado.',
|
||||
'quorum.error.voted': 'Alguien ya ha votado sobre este artículo: su quórum ya no cambia.',
|
||||
'quorum.error.forbidden': 'Solo la persona que escribió el artículo fija su quórum, antes del primer voto.',
|
||||
'ballot.reason.not_member': 'el monedero que firmó no es el de un miembro de la red de confianza Ğ1.',
|
||||
'ballot.reason.no_identity': 'el monedero que firmó no está vinculado a ninguna identidad Ğ1.',
|
||||
'ballot.reason.transfer': 'falta la transferencia a la cuenta de los votos, o su importe es demasiado bajo.',
|
||||
@@ -295,6 +339,8 @@ export const es: Messages = {
|
||||
'basket.empty': 'Nada en la cesta por ahora.',
|
||||
'basket.sent.one': '{n} voto registrado.',
|
||||
'basket.sent.other': '{n} votos registrados.',
|
||||
'basket.sent.simulated.one': '{n} voto simulado.',
|
||||
'basket.sent.simulated.other': '{n} votos simulados.',
|
||||
'basket.section': 'Votar esta sección',
|
||||
'basket.mine': 'tu voto: {choice}',
|
||||
'basket.held': 'una objeción la retiene: en contra o retirar, sin «a favor»',
|
||||
@@ -341,7 +387,7 @@ export const es: Messages = {
|
||||
'ballots.lines.other': '{n} votos',
|
||||
'ballots.status.pending': 'Esperando a la cadena Ğ1',
|
||||
'ballots.status.confirmed': 'Contado para la identidad Ğ1 «{name}»',
|
||||
'ballots.status.simulated': 'Simulado: contado para «{name}» hasta que se abra la cadena Ğ1',
|
||||
'ballots.status.simulated': 'Simulado: contado para «{name}» mientras no se borren los votos simulados',
|
||||
'ballots.status.refused': 'No contado: {reason}',
|
||||
'ballots.status.expired': 'No visto a tiempo en la cadena Ğ1: no cuenta',
|
||||
'ballots.block': 'bloque {n}',
|
||||
|
||||
+49
-3
@@ -137,6 +137,9 @@ export const fr = {
|
||||
'doc.basket': 'Voter en lot',
|
||||
'doc.basket.count': 'Voter en lot · {n}',
|
||||
'doc.empty': "Aucun article n'est en vigueur. Les propositions ci-dessous remontent dès qu'elles atteignent leur seuil.",
|
||||
'doc.empty.seeds': "Aucun article n'est encore en vigueur. Commence par les Propositions seed : elles y entrent par le vote, comme toute proposition.",
|
||||
'doc.others.one': '{n} autre version en cours',
|
||||
'doc.others.other': '{n} autres versions en cours',
|
||||
'doc.published': 'Publié sur le dépôt des textes',
|
||||
'doc.publish.pending': 'Publication en attente',
|
||||
|
||||
@@ -162,6 +165,11 @@ export const fr = {
|
||||
'deep.versions.other': '{n} versions',
|
||||
'deep.version': 'version {n}',
|
||||
'deep.lead': 'en tête : {label}',
|
||||
'deep.section.none': 'Sans section',
|
||||
'deep.section.count.one': '{n} article proposé',
|
||||
'deep.section.count.other': '{n} articles proposés',
|
||||
'deep.unfold': 'Tout déplier',
|
||||
'deep.fold': 'Tout replier',
|
||||
'section.introduction': 'Introduction',
|
||||
'section.fondamental': 'Engagements fondamentaux',
|
||||
'section.technique': 'Engagements techniques',
|
||||
@@ -185,19 +193,28 @@ export const fr = {
|
||||
'module.order.rule': "Un nouvel article se place à la fin de sa section. S'il arrive pendant le vote d'un ordre, il s'y ajoute à la fin, et les votes restent.",
|
||||
'order.propose': 'Proposer un autre ordre',
|
||||
'order.edit.label': 'Nouvel ordre des articles',
|
||||
'order.edit.help': 'Clique un article, puis sa nouvelle place, ou utilise ses flèches. Au clavier : Entrée pour le choisir, flèches haut et bas pour le déplacer.',
|
||||
'order.edit.help': 'Prends un article par sa poignée et dépose-le à sa place. Sans glisser : clique un article, puis sa nouvelle place, ou utilise ses flèches. Au clavier : flèches haut et bas.',
|
||||
'order.edit.up': 'Monter « {title} »',
|
||||
'order.edit.down': 'Descendre « {title} »',
|
||||
'order.edit.chosen': '« {title} » choisi : clique sa nouvelle place.',
|
||||
'order.edit.moved': '« {title} » en place {n} sur {total}.',
|
||||
'order.edit.submit': 'Proposer cet ordre',
|
||||
'order.edit.unchanged': "C'est l'ordre en vigueur : déplace au moins un article.",
|
||||
'order.edit.exists': 'Cet ordre est déjà proposé : vote-le plutôt que de le proposer deux fois.',
|
||||
'order.edit.vote': 'Voter cet ordre',
|
||||
'order.edit.proposed': 'proposé',
|
||||
'order.line': 'Ordre de la section',
|
||||
'order.reorder': 'Réordonner',
|
||||
'order.proposed.one': '{n} ordre proposé',
|
||||
'order.proposed.other': '{n} ordres proposés',
|
||||
'order.edit.note': "Un ordre se vote à l'inertie {level}, comme un article : déplié, dans le panier, par la chaîne Ğ1 dans Monnaie libre.",
|
||||
'order.submitted': 'Ordre proposé au vote.',
|
||||
'title.propose': 'Proposer un autre titre',
|
||||
'title.propose.label': 'Nouveau titre',
|
||||
'title.propose.note': "Le titre du document se vote à l'inertie {level}. Ses traductions restent éditoriales : elles passeront « à revoir ».",
|
||||
'title.submitted': 'Titre proposé au vote.',
|
||||
'title.proposed.one': '{n} titre proposé',
|
||||
'title.proposed.other': '{n} titres proposés',
|
||||
|
||||
'tally.short': '{for} pour · {against} contre',
|
||||
'tally.required': 'Seuil : {n} voix pour',
|
||||
@@ -234,6 +251,18 @@ export const fr = {
|
||||
'vote.against': 'Contre',
|
||||
'vote.retract': 'Retirer mon vote',
|
||||
'vote.saved': 'Vote enregistré.',
|
||||
'vote.saved.simulated': 'Vote simulé.',
|
||||
'vote.simulated.mark': 'vote simulé',
|
||||
'vote.others': 'Autres versions',
|
||||
'vote.need': '{yes} oui pour passer · {no} non pour bloquer',
|
||||
'vote.need.quorum': '{yes} oui pour passer · le quorum la bloque',
|
||||
'vote.need.first.one': '{yes} oui sur {quorum} votant pour passer',
|
||||
'vote.need.first.other': '{yes} oui sur {quorum} votants pour passer',
|
||||
'vote.need.short': 'passe à {yes} oui · bloquée à {no} non',
|
||||
'vote.need.short.quorum': 'passe à {yes} oui',
|
||||
'vote.need.short.first': '{yes} oui sur {quorum}',
|
||||
'vote.need.title.one': 'Quorum : {n} votant. En dessous, les votants qui manquent comptent comme « non ».',
|
||||
'vote.need.title.other': 'Quorum : {n} votants. En dessous, les votants qui manquent comptent comme « non ».',
|
||||
'vote.retracted': 'Vote retiré.',
|
||||
'vote.stale': "Le texte a changé de fond depuis ton vote : ton vote ne compte plus tant que tu ne l'as pas reconfirmé.",
|
||||
'vote.reconfirm': 'Reconfirmer : {choice}',
|
||||
@@ -260,7 +289,7 @@ export const fr = {
|
||||
'vote.mine.saved': 'enregistré',
|
||||
'vote.mine.pending': 'En attente de la chaîne Ğ1 : {choice}.',
|
||||
'vote.mine.simulated': 'simulé',
|
||||
'vote.simulated.note': "La chaîne Ğ1 n'est pas encore ouverte : ton vote est simulé. Il compte comme un vote, marqué « simulé », jusqu'à son effacement à l'ouverture de la chaîne.",
|
||||
'vote.simulated.note': "La chaîne Ğ1 n'est pas encore ouverte : ton vote est simulé. Il compte comme un vote, marqué « simulé », tant qu'une décision humaine n'efface pas les votes simulés.",
|
||||
'vote.tally.simulated.one': 'dont {n} simulé',
|
||||
'vote.tally.simulated.other': 'dont {n} simulés',
|
||||
'vote.mine.refused': "Ton dernier vote n'a pas été compté : {reason}",
|
||||
@@ -274,6 +303,21 @@ export const fr = {
|
||||
'vote.chain.off': "Le vote par la chaîne Ğ1 n'est pas encore ouvert : le compte qui reçoit les votes reste à créer. Tu peux lire, proposer et faire une objection.",
|
||||
'vote.members': 'Seuls les membres de ce groupe votent ici.',
|
||||
'vote.details': 'Coquilles, traductions, objections, historique',
|
||||
'quorum.heading.one': 'Quorum · {n} votant',
|
||||
'quorum.heading.other': 'Quorum · {n} votants',
|
||||
'quorum.default.one': 'Par défaut, celui de son cran : {n} votant.',
|
||||
'quorum.default.other': 'Par défaut, celui de son cran : {n} votants.',
|
||||
'quorum.explain': "Tu fixes le quorum de cet article tant que personne n'a voté. En dessous, les votants qui manquent comptent comme « non ».",
|
||||
'quorum.field': "Quorum de l'article",
|
||||
'quorum.set': 'Fixer ce quorum',
|
||||
'quorum.reset': 'Revenir au défaut ({n})',
|
||||
'quorum.done.one': 'Quorum fixé : {n} votant.',
|
||||
'quorum.done.other': 'Quorum fixé : {n} votants.',
|
||||
'quorum.reset.done.one': 'Quorum revenu au défaut : {n} votant.',
|
||||
'quorum.reset.done.other': 'Quorum revenu au défaut : {n} votants.',
|
||||
'quorum.error.quorum': "Ce quorum sort des bornes : au moins 2 votants (1 dans le groupe d'essai), au plus {w}, l'électorat.",
|
||||
'quorum.error.voted': "Quelqu'un a déjà voté sur cet article : son quorum ne change plus.",
|
||||
'quorum.error.forbidden': "Seule la personne qui a écrit l'article fixe son quorum, avant le premier vote.",
|
||||
'ballot.reason.not_member': "le portefeuille qui a signé n'est pas celui d'un membre de la toile de confiance Ğ1.",
|
||||
'ballot.reason.no_identity': "le portefeuille qui a signé n'est rattaché à aucune identité Ğ1.",
|
||||
'ballot.reason.transfer': 'le virement vers le compte des votes manque, ou son montant est trop faible.',
|
||||
@@ -299,6 +343,8 @@ export const fr = {
|
||||
'basket.empty': "Rien dans le panier pour l'instant.",
|
||||
'basket.sent.one': '{n} vote enregistré.',
|
||||
'basket.sent.other': '{n} votes enregistrés.',
|
||||
'basket.sent.simulated.one': '{n} vote simulé.',
|
||||
'basket.sent.simulated.other': '{n} votes simulés.',
|
||||
'basket.section': 'Voter cette section',
|
||||
'basket.mine': 'ton vote : {choice}',
|
||||
'basket.held': 'une objection la retient : contre ou retrait, pas de « pour »',
|
||||
@@ -345,7 +391,7 @@ export const fr = {
|
||||
'ballots.lines.other': '{n} votes',
|
||||
'ballots.status.pending': 'En attente de la chaîne Ğ1',
|
||||
'ballots.status.confirmed': "Compté pour l'identité Ğ1 « {name} »",
|
||||
'ballots.status.simulated': "Simulé : compté pour « {name} » jusqu'à l'ouverture de la chaîne Ğ1",
|
||||
'ballots.status.simulated': 'Simulé : compté pour « {name} » tant que les votes simulés ne sont pas effacés',
|
||||
'ballots.status.refused': 'Pas compté : {reason}',
|
||||
'ballots.status.expired': 'Pas vu sur la chaîne Ğ1 à temps : il ne compte pas',
|
||||
'ballots.block': 'bloc {n}',
|
||||
|
||||
@@ -53,3 +53,25 @@ export function orderNote(p: Proposal): string[] {
|
||||
return notes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* How many « oui » pass a version, how many « non » block it (Yvv, 2026-11-02: « combien il
|
||||
* faut de oui pour passer et combien de non pour maintenir l'opposition »), from what the
|
||||
* server says (ADR 0013): `yesNeeded`, the total of *for* that passes with the *against* cast
|
||||
* now; `noNeeded`, the total of *against* that holds the *for* cast now in check — 0 when
|
||||
* the quorum holds them alone. Before any vote: « 9 oui sur 12 votants ». The quorum goes
|
||||
* to the tooltip (`title`), never to the title of a line.
|
||||
*/
|
||||
export function needOf(p: Proposal): { long: string; short: string; title: string } | null {
|
||||
const e = p.evaluation;
|
||||
if (!e || e.yesNeeded === undefined || e.noNeeded === undefined || e.quorum === undefined) return null;
|
||||
const yes = e.yesNeeded;
|
||||
const no = e.noNeeded;
|
||||
const quorum = e.quorum;
|
||||
const title = i18n.plural('vote.need.title', quorum);
|
||||
if (e.cast === 0) {
|
||||
return { long: i18n.plural('vote.need.first', quorum, { yes, quorum }), short: t('vote.need.short.first', { yes, quorum }), title };
|
||||
}
|
||||
if (no === 0) return { long: t('vote.need.quorum', { yes }), short: t('vote.need.short.quorum', { yes }), title };
|
||||
return { long: t('vote.need', { yes, no }), short: t('vote.need.short', { yes, no }), title };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// « Combien il faut de oui pour passer, combien de non pour bloquer » (Yvv, 2026-11-02),
|
||||
// told from what the server says (ADR 0013): yesNeeded, noNeeded, the quorum.
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { needOf } from '../../src/lib/labels';
|
||||
import type { Evaluation, Proposal } from '../../src/lib/types';
|
||||
|
||||
function version(tally: { for: number; against: number }, e: Partial<Evaluation> | null): Proposal {
|
||||
const evaluation: Evaluation | null = e
|
||||
? { cast: tally.for + tally.against, required: 0, reached: false, missing: 0, requiredRatio: 0, wellRated: false, ...e }
|
||||
: null;
|
||||
return { tally, evaluation } as unknown as Proposal;
|
||||
}
|
||||
|
||||
describe('the yes to pass, the no to block', () => {
|
||||
it('before any vote: the yes asked of the quorum', () => {
|
||||
const need = needOf(version({ for: 0, against: 0 }, { quorum: 12, yesNeeded: 9, noNeeded: 0 }));
|
||||
expect(need?.long).toBe('9 oui sur 12 votants pour passer');
|
||||
expect(need?.short).toBe('9 oui sur 12');
|
||||
expect(need?.title).toBe('Quorum : 12 votants. En dessous, les votants qui manquent comptent comme « non ».');
|
||||
});
|
||||
it('one voter of quorum: singular', () => {
|
||||
expect(needOf(version({ for: 0, against: 0 }, { quorum: 1, yesNeeded: 1, noNeeded: 0 }))?.long).toBe('1 oui sur 1 votant pour passer');
|
||||
});
|
||||
it('9 for and 4 against at level 2, quorum 12: 11 yes pass, 4 no block', () => {
|
||||
const need = needOf(version({ for: 9, against: 4 }, { quorum: 12, yesNeeded: 11, noNeeded: 4 }));
|
||||
expect(need?.long).toBe('11 oui pour passer · 4 non pour bloquer');
|
||||
expect(need?.short).toBe('passe à 11 oui · bloquée à 4 non');
|
||||
});
|
||||
it('below the quorum, the voters missing hold it alone: no « non » to count', () => {
|
||||
const need = needOf(version({ for: 3, against: 0 }, { quorum: 12, yesNeeded: 9, noNeeded: 0 }));
|
||||
expect(need?.long).toBe('9 oui pour passer · le quorum la bloque');
|
||||
expect(need?.short).toBe('passe à 9 oui');
|
||||
});
|
||||
it('nothing said without the numbers of the server', () => {
|
||||
expect(needOf(version({ for: 0, against: 0 }, null))).toBeNull();
|
||||
expect(needOf(version({ for: 0, against: 0 }, { quorum: 12 }))).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user