Files
decision/web/tests/unit/basket.test.ts
T
YvvandClaude Opus 5.5 83f8ca0981 v3 : interface des objections, des coquilles validées et de la messagerie ; contrôles C28–C31
Une version retenue par une objection le dit sous l'article, à chacun
selon son rôle : la personne qui vote voit qu'elle ne se vote plus (les
votes faits restent) ; l'auteur, quoi faire (« Modifier l'article visé »
pour une redite, « Reformuler » pour une contradiction) ; la personne qui
objecte, lever ou réitérer avant l'échéance. Le panier la montre sans
choix et ne l'envoie jamais.

« Détails » : faire une objection (redit / contredit), les objections et
où elles en sont, les coquilles proposées par d'autres avec leur diff
resserré, valider / refuser / revenir, l'historique dit qui a validé.
Une coquille se propose sur toute version ouverte ou en vigueur.

Boîte « Messages » dans le menu, avec le nombre de non-lus (pastille sur
le bouton du menu en mobile) : objet, texte, gestes, échéance ; un message
traité n'a plus de geste. Répondre à une objection lit son message.

Trilingue (fr, en, es). C6 suit la règle : la coquille d'un administrateur
sur un texte du seed passe tout de suite ; C28–C31 couvrent le reste.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 04:21:20 +01:00

98 lines
5.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { clearScope, counts, fill, lines, sections, votableIds, 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, votable: true, objections: 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 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 });
});
it('a version held back by an objection is shown apart: no grouped gesture fills it, nothing is sent for it', () => {
// E5 has the text in force and a rival held back; E6 is a new article, held back.
const e5 = unit('E5', 'technique', prop('e5', 'E5', { status: 'in_force' }), [
prop('e5r', 'E5', { blocked: true, votable: false }),
]);
const e6 = unit('E6', 'technique', null, [prop('e6', 'E6', { blocked: true, votable: false })]);
const s = sections([e5, e6]);
const [slotE5, slotE6] = s[0]?.slots ?? [];
expect(slotE5?.versions.map((p) => p.id)).toEqual(['e5']);
expect(slotE5?.held.map((p) => p.id)).toEqual(['e5r']);
// A held article stays in sight, with nothing to choose.
expect(slotE6?.versions).toEqual([]);
expect(slotE6?.held.map((p) => p.id)).toEqual(['e6']);
const all = s.flatMap((x) => x.slots);
// The held rival does not compete now: the text in force is the only choice.
expect(fill(all, 'for', {})).toEqual({ e5: 'for' });
expect(waiting(all, {})).toEqual([]);
// A choice kept from before the objection is not sent.
expect(lines({ e5: 'for', e5r: 'for' }, votableIds(s))).toEqual([{ proposalId: 'e5', choice: 'for' }]);
expect(clearScope(all, { e5: 'for', e5r: 'against', i1: 'for' })).toEqual({ i1: 'for' });
});
});