Fusion du lot F : objections redit / contredit, coquilles validées par leur auteur, messagerie, délai de 3 jours

Conflits avec B2 : docs.rs garde shown_tally / shown_evaluation et votable ;
proposals.rs sert les totaux montrés et « votable » ; CONTROLES réunit C24
(« faire une objection », le panier par son adresse), C28–C33 et les deux passages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-31 05:57:21 +01:00
co-authored by Claude Opus 5.5
55 changed files with 5435 additions and 456 deletions
+43
View File
@@ -81,6 +81,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('/');