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>
This commit is contained in:
Yvv
2026-10-31 04:21:20 +01:00
co-authored by Claude Opus 5.5
parent 3f1f968f16
commit 83f8ca0981
31 changed files with 1732 additions and 197 deletions
+43
View File
@@ -69,6 +69,49 @@ export async function shot(page: Page, name: string) {
if (dir) await page.screenshot({ path: `${dir}/${name}.png` });
}
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 }[] }[];
}> {
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 };
}
/** 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);
const r = await page.request.post(`/api/docs/${doc.id}/proposals`, {
data: { kind: 'text', unitId: null, title, body, submit: true },
headers: CSRF,
});
if (!r.ok()) throw new Error(`propose ${r.status()} ${await r.text()}`);
return (await r.json()).id;
}
/** Moves the server's clock forward (dev login only) and passes the deadlines at once. */
export async function advanceClock(page: Page, seconds: number): Promise<{ lifted: number; applied: number; stale: number }> {
const r = await page.request.post('/api/dev/clock', { data: { seconds }, headers: CSRF });
if (!r.ok()) throw new Error(`clock ${r.status()}`);
return r.json();
}
/** Screenshots on a desktop and on a phone, when `LD_SHOTS` names a directory (never in the loop). */
export async function shots(page: Page, name: string) {
if (!process.env.LD_SHOTS) return;
const size = page.viewportSize();
await page.waitForTimeout(400);
await shot(page, `${name}-bureau`);
await page.setViewportSize({ width: 412, height: 915 });
await page.waitForTimeout(200);
await shot(page, `${name}-mobile`);
if (size) await page.setViewportSize(size);
await page.waitForTimeout(200);
}
/** The control door of the simulated chain, found where the page finds its node. */
export async function chain(page: Page, docPath = DOC) {
const [, , group] = docPath.split('/');