forked from yvv/decision
Chaque section du lecteur finit par l'ordre de ses articles (inertie 2) : ses articles numérotés, son vote déplié, « Proposer un autre ordre » — une liste où l'on clique un article puis sa place, ses flèches, ou le clavier. Le titre du document (inertie 4) se vote et se propose sous le titre, « traduction à revoir » quand il a changé. Les deux sont dans la liste des propositions, le panier et la chaîne Ğ1 ; un nouvel article se propose dans une section, à sa fin. La liste du groupe compte les propositions ouvertes des modules. Restes mineurs : la participation ramenée se dit en deux temps (la mesure, puis ce que le groupe demande), les lignes « habituelle » des réglages resserrées, le sélecteur d'inertie se parcourt aux flèches (le cran actuel atteignable, jamais choisi), la marque « seed a minima » porte le vrai nombre de votes. Contrôles : C25 (ordre, dont la chaîne et le téléphone), C26 (titre), C27 (participation ramenée) ; C3, C17 (41 modules), C20 mis à jour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
5.6 KiB
TypeScript
96 lines
5.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { clearScope, counts, fill, lines, sections, waiting } from '../../src/lib/basket';
|
|
import type { Proposal, Unit } from '../../src/lib/types';
|
|
|
|
function prop(id: string, unitId: string, over: Partial<Proposal> = {}): Proposal {
|
|
return {
|
|
id, unitId, kind: 'text', inertiaTarget: null, title: unitId, body: `texte ${id}`, status: 'open', origin: 'seed',
|
|
label: null, author: null, mine: false, revision: 1, createdAt: '', updatedAt: '', submittedAt: null, enteredAt: null,
|
|
confirmedAt: null, seedMinimal: false, tally: { for: 0, against: 0 }, evaluation: null, votedAtInertia: 3,
|
|
blocked: false, signals: 0, myVote: null, myVoteStale: false, myVoteChain: false, myBallot: null, translations: {},
|
|
...over,
|
|
};
|
|
}
|
|
|
|
function unit(id: string, section: string | null, inForce: Proposal | null, proposals: Proposal[]): Unit {
|
|
return { id, code: id, section, position: 0, inertia: 3, inertiaChanges: [], inForce, proposals };
|
|
}
|
|
|
|
// The certification act in small: a text in force, a seed with a rival and a removal, two
|
|
// open articles, one with two variants, a change of inertia.
|
|
const I1 = unit('I1', 'introduction', prop('i1', 'I1', { status: 'in_force' }), []);
|
|
const E1 = unit('E1', 'fondamental', prop('e1', 'E1', { status: 'in_force' }), [
|
|
prop('e1r', 'E1'),
|
|
prop('e1x', 'E1', { kind: 'remove' }),
|
|
prop('e1k', 'E1', { kind: 'inertia', inertiaTarget: 5 }),
|
|
]);
|
|
const E2 = unit('E2', 'fondamental', prop('e2', 'E2', { status: 'in_force' }), [prop('e2k', 'E2', { kind: 'inertia', inertiaTarget: 5 })]);
|
|
const E3 = unit('E3', 'technique', null, [prop('e3a', 'E3', { label: '1.0.0' }), prop('e3b', 'E3', { label: '0.4.0' })]);
|
|
const E4 = unit('E4', 'technique', null, [prop('e4', 'E4'), prop('e4d', 'E4', { status: 'draft' })]);
|
|
const EMPTY = unit('E9', 'technique', null, []);
|
|
const DOC = [I1, E1, E2, E3, E4, EMPTY];
|
|
|
|
describe('the basket of a vote', () => {
|
|
it('sections in the order of the document; an article holds its versions in competition', () => {
|
|
const s = sections(DOC);
|
|
expect(s.map((x) => x.key)).toEqual(['introduction', 'fondamental', 'technique']);
|
|
const [, fond, tech] = s;
|
|
// The text in force, its rival and its removal compete; a change of inertia is apart.
|
|
expect(fond?.slots[0]?.versions.map((p) => p.id)).toEqual(['e1', 'e1r', 'e1x']);
|
|
expect(fond?.slots[0]?.inertia.map((p) => p.id)).toEqual(['e1k']);
|
|
expect(fond?.slots[1]?.versions.map((p) => p.id)).toEqual(['e2']);
|
|
// Drafts are nobody's to vote; an empty slot is left out.
|
|
expect(tech?.slots.map((x) => x.unit.id)).toEqual(['E3', 'E4']);
|
|
expect(tech?.slots[1]?.versions.map((p) => p.id)).toEqual(['e4']);
|
|
});
|
|
|
|
it('a grouped gesture fills the articles of a single version, never one with several', () => {
|
|
const all = sections(DOC).flatMap((x) => x.slots);
|
|
const picks = fill(all, 'for', {});
|
|
expect(picks).toEqual({ i1: 'for', e2: 'for', e4: 'for' });
|
|
// Not the change of inertia of E2, not E1 (three versions), not E3 (two variants).
|
|
expect(waiting(all, picks).map((x) => x.unit.id)).toEqual(['E1', 'E3']);
|
|
// Choosing a version settles the article.
|
|
const chosen = { ...picks, e3b: 'against' as const };
|
|
expect(waiting(all, chosen).map((x) => x.unit.id)).toEqual(['E1']);
|
|
// A section only: the others stay as they were.
|
|
const tech = sections(DOC)[2]?.slots ?? [];
|
|
expect(fill(tech, 'against', { i1: 'for', e3a: 'for' })).toEqual({ i1: 'for', e3a: 'for', e4: 'against' });
|
|
});
|
|
|
|
it('emptying a section leaves the rest', () => {
|
|
const fond = sections(DOC)[1]?.slots ?? [];
|
|
expect(clearScope(fond, { i1: 'for', e2: 'for', e1k: 'against', e4: 'none' })).toEqual({ i1: 'for', e4: 'none' });
|
|
});
|
|
|
|
it('the modules are voted like articles: the title first, each order at the end of its section', () => {
|
|
// ADR 0009: the title of the document, the order of each section; an order of one article is left out.
|
|
const title = { ...unit('T', null, prop('t', 'T', { kind: 'title', status: 'in_force' }), [prop('t2', 'T', { kind: 'title' })]), kind: 'title' as const };
|
|
const order = (id: string, section: string, moot = false) => ({
|
|
...unit(id, section, prop(`${id}-v`, id, { kind: 'order', status: 'in_force' }), []),
|
|
kind: 'order' as const,
|
|
moot,
|
|
});
|
|
const s = sections(DOC, { title, orders: [order('O-intro', 'introduction', true), order('O-fond', 'fondamental'), order('O-tech', 'technique')] });
|
|
expect(s.map((x) => x.key)).toEqual([null, 'introduction', 'fondamental', 'technique']);
|
|
expect(s[0]?.slots.map((x) => x.unit.id)).toEqual(['T']);
|
|
expect(s[0]?.slots[0]?.versions.map((p) => p.id)).toEqual(['t', 't2']);
|
|
expect(s[1]?.slots.map((x) => x.unit.id)).toEqual(['I1']);
|
|
expect(s[2]?.slots.map((x) => x.unit.id)).toEqual(['E1', 'E2', 'O-fond']);
|
|
expect(s[3]?.slots.map((x) => x.unit.id)).toEqual(['E3', 'E4', 'O-tech']);
|
|
// « Tout pour » a section fills its order too (one version), never the title (two).
|
|
expect(fill(s[2]?.slots ?? [], 'for', {})).toEqual({ e2: 'for', 'O-fond-v': 'for' });
|
|
expect(waiting(s.flatMap((x) => x.slots), {}).map((x) => x.unit.id)).toEqual(['T', 'E1', 'E3']);
|
|
});
|
|
|
|
it('the lines sent: a withdrawal is a null choice; a version gone from the document is left out', () => {
|
|
const known = new Set(['i1', 'e2', 'e4']);
|
|
expect(lines({ i1: 'for', e2: 'none', e4: 'against', gone: 'for' }, known)).toEqual([
|
|
{ proposalId: 'i1', choice: 'for' },
|
|
{ proposalId: 'e2', choice: null },
|
|
{ proposalId: 'e4', choice: 'against' },
|
|
]);
|
|
expect(counts({ i1: 'for', e2: 'none', e4: 'against', e5: 'for' })).toEqual({ for: 2, against: 1, none: 1, total: 4 });
|
|
});
|
|
});
|