forked from yvv/decision
v3 : C32 et C33 votent « contre » sur des propositions qu'aucun autre contrôle ne lit
Le texte de Monnaie libre reste tel que C20 et C23 l'attendent (ils passent après dans l'ordre des fichiers) ; rien n'entre en vigueur, rien ne devient bien coté. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,8 +37,8 @@ pour une personne, ils coûtent 0,1 Ğ1 réels : c'est un acte « paye » d'Yvv.
|
|||||||
| C22 | Le vote groupé | groupe d'essai : « Propositions en cours » → « Engagements techniques » → « Voter cette section » ; « Tout pour » ; dans « Connaissance suffisante », « Pour » sur la variante 1.0.0 ; recharger ; « Voter (n) » | le panier s'ouvre sur la section ; « Tout pour » remplit les articles à une version, pas « Connaissance suffisante » ni « Rencontre physique… » (« Plusieurs versions : choisis la tienne. », et le compte des articles laissés) ; le panier reste après rechargement (« Voter en lot · n ») ; un seul envoi : « n votes enregistrés. », le panier vide, la variante choisie à 1 pour |
|
| C22 | Le vote groupé | groupe d'essai : « Propositions en cours » → « Engagements techniques » → « Voter cette section » ; « Tout pour » ; dans « Connaissance suffisante », « Pour » sur la variante 1.0.0 ; recharger ; « Voter (n) » | le panier s'ouvre sur la section ; « Tout pour » remplit les articles à une version, pas « Connaissance suffisante » ni « Rencontre physique… » (« Plusieurs versions : choisis la tienne. », et le compte des articles laissés) ; le panier reste après rechargement (« Voter en lot · n ») ; un seul envoi : « n votes enregistrés. », le panier vide, la variante choisie à 1 pour |
|
||||||
| C23 | Le vote par la chaîne Ğ1 | Monnaie libre, un portefeuille dans le navigateur : « Voter en lot », « Pour » sur Unicité du compte et Réciprocité, « Signer et envoyer (2) » ; puis sous Réciprocité, « Contre » (l'indexeur en retard) puis « Retirer mon vote » ; puis sans portefeuille : « Pour » → « Passer par Ğecko » | une seule signature pour les deux : 0,1 Ğ1 vers le compte des votes et une remarque `ld1:` + 64 chiffres, rien du vote en clair ; « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « Compté pour l'identité Ğ1 « Alice ». » ; sous l'article « Ton vote : pour · inscrit sur la chaîne Ğ1 », 1 pour ; « En attente de la chaîne Ğ1 : contre. » ; le retrait, plus récent sur la chaîne, fait foi même quand le « contre » est confirmé après lui (0 pour · 0 contre) ; Ğecko : un QR code et le lien `g1://<compte>?amount=0.1&comment=ld1:…`, puis la confirmation dès que la chaîne la montre |
|
| C23 | Le vote par la chaîne Ğ1 | Monnaie libre, un portefeuille dans le navigateur : « Voter en lot », « Pour » sur Unicité du compte et Réciprocité, « Signer et envoyer (2) » ; puis sous Réciprocité, « Contre » (l'indexeur en retard) puis « Retirer mon vote » ; puis sans portefeuille : « Pour » → « Passer par Ğecko » | une seule signature pour les deux : 0,1 Ğ1 vers le compte des votes et une remarque `ld1:` + 64 chiffres, rien du vote en clair ; « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « Compté pour l'identité Ğ1 « Alice ». » ; sous l'article « Ton vote : pour · inscrit sur la chaîne Ğ1 », 1 pour ; « En attente de la chaîne Ğ1 : contre. » ; le retrait, plus récent sur la chaîne, fait foi même quand le « contre » est confirmé après lui (0 pour · 0 contre) ; Ğecko : un QR code et le lien `g1://<compte>?amount=0.1&comment=ld1:…`, puis la confirmation dès que la chaîne la montre |
|
||||||
| C24 | Sans compte des votes | Monnaie libre sur un serveur sans `LD_VOTE_ACCOUNT`, puis le groupe d'essai | pas de « Voter en lot » ni « Voter cette section » ; sous l'article : « 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 signaler une contradiction. », pas de bouton, les totaux et le seuil restent ; le panier ouvert par son adresse (`?onglet=panier`) dit la même chose, son bouton éteint, et **jamais** « Tes votes comptent dès l'envoi » ; dans le groupe d'essai, le panier et les boutons sont là |
|
| C24 | Sans compte des votes | Monnaie libre sur un serveur sans `LD_VOTE_ACCOUNT`, puis le groupe d'essai | pas de « Voter en lot » ni « Voter cette section » ; sous l'article : « 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 signaler une contradiction. », pas de bouton, les totaux et le seuil restent ; le panier ouvert par son adresse (`?onglet=panier`) dit la même chose, son bouton éteint, et **jamais** « Tes votes comptent dès l'envoi » ; dans le groupe d'essai, le panier et les boutons sont là |
|
||||||
| C32 | Lâcher ou garder un bulletin | Monnaie libre, un portefeuille dans le navigateur (Eve, chaîne simulée) : Réciprocité → « Contre » le nœud éteint ; puis « Contre », fermer la fenêtre pendant que le portefeuille demande, refuser dans le portefeuille ; puis « Contre », fermer, signer dans le portefeuille ; une autre personne, sans portefeuille : « Pour » → « Passer par Ğecko », fermer, payer depuis un téléphone | nœud éteint : « La chaîne Ğ1 ne répond pas. Réessaie dans un moment. », le bulletin lâché aussitôt, rien « en attente » sous l'article ; fenêtre fermée puis refus : lâché ; fenêtre fermée puis signature : gardé, et la chaîne le confirme sans la page (« Ton vote : contre · inscrit sur la chaîne Ğ1 ») ; Ğecko, fenêtre fermée : « En attente de la chaîne Ğ1 : pour. », puis confirmé sans la page |
|
| C32 | Lâcher ou garder un bulletin | Monnaie libre, un portefeuille dans le navigateur (Eve, chaîne simulée), « Propositions en cours » → Document de révocation : « Contre » le nœud éteint ; puis « Contre », fermer la fenêtre pendant que le portefeuille demande, refuser dans le portefeuille ; puis « Contre », fermer, signer dans le portefeuille ; une autre personne, sans portefeuille : « Contre » → « Passer par Ğecko », fermer, payer depuis un téléphone | nœud éteint : « La chaîne Ğ1 ne répond pas. Réessaie dans un moment. », le bulletin lâché aussitôt, rien « en attente » sous l'article ; fenêtre fermée puis refus : lâché ; fenêtre fermée puis signature : gardé, et la chaîne le confirme sans la page (« Ton vote : contre · inscrit sur la chaîne Ğ1 ») ; Ğecko, fenêtre fermée : « En attente de la chaîne Ğ1 : contre. », puis confirmé sans la page |
|
||||||
| C33 | Le reçu de ton vote | Monnaie libre, un portefeuille : « Pour » sur Unicité du compte, « Télécharger ton reçu », « Tes bulletins » ; sur « Tes bulletins », « Fichier du reçu » : le reçu ; puis coller le reçu avec un choix changé, « Vérifier » ; une autre personne ouvre « Tes bulletins » | le reçu `recu-vote-<date>-<id>.json` : les votes, le sel, la remarque `ld1:…`, « confirmed », l'identité « Alice » ; « Tes bulletins de vote » : « Compté pour l'identité Ğ1 « Alice » » et la remarque ; « Le reçu tient : ses votes et son sel donnent bien ce commentaire. », « Unicité du compte : pour » ; changé : « Le reçu ne tient pas » ; l'autre personne : « Tu n'as encore envoyé aucun bulletin par la chaîne Ğ1. », et le reçu lui est refusé |
|
| C33 | Le reçu de ton vote | Monnaie libre, un portefeuille : « Contre » sur Ne jamais certifier seul (« Propositions en cours »), « Télécharger ton reçu », « Tes bulletins » ; sur « Tes bulletins », « Fichier du reçu » : le reçu ; puis coller le reçu avec un choix changé, « Vérifier » ; une autre personne ouvre « Tes bulletins » | le reçu `recu-vote-<date>-<id>.json` : les votes, le sel, la remarque `ld1:…`, « confirmed », l'identité « Alice » ; « Tes bulletins de vote » : « Compté pour l'identité Ğ1 « Alice » » et la remarque ; « Le reçu tient : ses votes et son sel donnent bien ce commentaire. », « Ne jamais certifier seul : contre » ; changé : « Le reçu ne tient pas » ; l'autre personne : « Tu n'as encore envoyé aucun bulletin par la chaîne Ğ1. », et le reçu lui est refusé |
|
||||||
|
|
||||||
## Au-delà de la liste
|
## Au-delà de la liste
|
||||||
|
|
||||||
|
|||||||
+20
-15
@@ -5,18 +5,23 @@ import { ALICE, chain, DOC, EVE, person, shot, signIn, VOTE_ACCOUNT, wallet } fr
|
|||||||
// Lot B2 — the vote on the Ğ1 chain, after its review: a ballot nothing left for
|
// Lot B2 — the vote on the Ğ1 chain, after its review: a ballot nothing left for
|
||||||
// is let go; signed, or shown as a Ğecko code, it is kept and the server confirms
|
// is let go; signed, or shown as a Ğecko code, it is kept and the server confirms
|
||||||
// it without the page; the receipt goes to the one who asked, who checks it here.
|
// it without the page; the receipt goes to the one who asked, who checks it here.
|
||||||
// The chain is simulated (e2e/chain-mock.mjs): never a real transaction.
|
// The chain is simulated (e2e/chain-mock.mjs): never a real transaction. The votes go
|
||||||
|
// to open proposals no other control reads, and only « contre »: nothing enters into
|
||||||
|
// force, nothing becomes well rated, the other controls find the text as they expect.
|
||||||
|
|
||||||
type Entry = { id: string; status: string; comment: string };
|
type Entry = { id: string; status: string; comment: string };
|
||||||
const mine = async (page: Page): Promise<Entry[]> => (await (await page.request.get('/api/ballots')).json()).ballots;
|
const mine = async (page: Page): Promise<Entry[]> => (await (await page.request.get('/api/ballots')).json()).ballots;
|
||||||
|
|
||||||
|
/** Unfolds the vote of an open proposal, from « Propositions en cours ». */
|
||||||
async function unfold(page: Page, title: string) {
|
async function unfold(page: Page, title: string) {
|
||||||
const unit = page.locator('section.text article.unit', { hasText: title });
|
// By its title only: the excerpt of another proposal may name it.
|
||||||
await unit.hover();
|
await page.locator('ul.deep button.row').filter({ has: page.locator('span.title').getByText(title, { exact: true }) }).click();
|
||||||
await unit.getByRole('button', { name: 'Le vote' }).click();
|
|
||||||
return page.getByRole('region', { name: `Le vote de « ${title} »` });
|
return page.getByRole('region', { name: `Le vote de « ${title} »` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REVOCATION = 'Document de révocation';
|
||||||
|
const NEVER_ALONE = 'Ne jamais certifier seul';
|
||||||
|
|
||||||
const answer = (page: Page, yes: boolean) =>
|
const answer = (page: Page, yes: boolean) =>
|
||||||
page.evaluate((y) => (window as unknown as { __ldAnswer: (yes: boolean) => void }).__ldAnswer(y), yes);
|
page.evaluate((y) => (window as unknown as { __ldAnswer: (yes: boolean) => void }).__ldAnswer(y), yes);
|
||||||
|
|
||||||
@@ -34,7 +39,7 @@ test('C32 — rien n’est parti : le bulletin est lâché ; signé ou par Ğeck
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const deleted = () => asked.filter((a) => a.startsWith('DELETE')).length;
|
const deleted = () => asked.filter((a) => a.startsWith('DELETE')).length;
|
||||||
const fold = await unfold(page, 'Réciprocité');
|
const fold = await unfold(page, REVOCATION);
|
||||||
const dialog = page.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' });
|
const dialog = page.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' });
|
||||||
|
|
||||||
// The node does not answer before anything is signed: nothing left, the ballot is let go.
|
// The node does not answer before anything is signed: nothing left, the ballot is let go.
|
||||||
@@ -69,18 +74,18 @@ test('C32 — rien n’est parti : le bulletin est lâché ; signé ou par Ğeck
|
|||||||
await expect.poll(async () => (await mine(page)).map((b) => b.status), { timeout: 30_000 }).toEqual(['confirmed']);
|
await expect.poll(async () => (await mine(page)).map((b) => b.status), { timeout: 30_000 }).toEqual(['confirmed']);
|
||||||
expect(deleted()).toBe(2);
|
expect(deleted()).toBe(2);
|
||||||
await page.goto(DOC);
|
await page.goto(DOC);
|
||||||
await expect((await unfold(page, 'Réciprocité')).locator('.voted')).toHaveText('Ton vote : contre · inscrit sur la chaîne Ğ1');
|
await expect((await unfold(page, REVOCATION)).locator('.voted')).toHaveText('Ton vote : contre · inscrit sur la chaîne Ğ1');
|
||||||
|
|
||||||
// Ğecko: the code shown, the window closed — the ballot is kept; paid from a phone, confirmed by the server.
|
// Ğecko: the code shown, the window closed — the ballot is kept; paid from a phone, confirmed by the server.
|
||||||
const ugo = await person(browser, 'Ugo');
|
const ugo = await person(browser, 'Ugo');
|
||||||
await ugo.goto(DOC);
|
await ugo.goto(DOC);
|
||||||
const theirs = await unfold(ugo, 'Réciprocité');
|
const theirs = await unfold(ugo, REVOCATION);
|
||||||
await theirs.getByRole('button', { name: 'Pour', exact: true }).click();
|
await theirs.getByRole('button', { name: 'Contre', exact: true }).click();
|
||||||
const sheet = ugo.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' });
|
const sheet = ugo.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' });
|
||||||
await sheet.getByRole('button', { name: 'Passer par Ğecko' }).click();
|
await sheet.getByRole('button', { name: 'Passer par Ğecko' }).click();
|
||||||
const link = (await sheet.getByRole('link', { name: 'Ouvrir dans Ğecko' }).getAttribute('href')) ?? '';
|
const link = (await sheet.getByRole('link', { name: 'Ouvrir dans Ğecko' }).getAttribute('href')) ?? '';
|
||||||
await sheet.getByRole('button', { name: 'Fermer' }).click();
|
await sheet.getByRole('button', { name: 'Fermer' }).click();
|
||||||
await expect(theirs.getByText('En attente de la chaîne Ğ1 : pour.')).toBeVisible();
|
await expect(theirs.getByText('En attente de la chaîne Ğ1 : contre.')).toBeVisible();
|
||||||
const { node } = await chain(ugo);
|
const { node } = await chain(ugo);
|
||||||
const fields = { address: ALICE.address, dest: VOTE_ACCOUNT, amount: '10', remark: link.split('comment=')[1] ?? '', signature: '0x01' };
|
const fields = { address: ALICE.address, dest: VOTE_ACCOUNT, amount: '10', remark: link.split('comment=')[1] ?? '', signature: '0x01' };
|
||||||
await ugo.request.post(node, {
|
await ugo.request.post(node, {
|
||||||
@@ -88,7 +93,7 @@ test('C32 — rien n’est parti : le bulletin est lâché ; signé ou par Ğeck
|
|||||||
});
|
});
|
||||||
await expect.poll(async () => (await mine(ugo)).map((b) => b.status), { timeout: 30_000 }).toEqual(['confirmed']);
|
await expect.poll(async () => (await mine(ugo)).map((b) => b.status), { timeout: 30_000 }).toEqual(['confirmed']);
|
||||||
await ugo.goto(DOC);
|
await ugo.goto(DOC);
|
||||||
await expect((await unfold(ugo, 'Réciprocité')).locator('.voted')).toHaveText('Ton vote : pour · inscrit sur la chaîne Ğ1');
|
await expect((await unfold(ugo, REVOCATION)).locator('.voted')).toHaveText('Ton vote : contre · inscrit sur la chaîne Ğ1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('C33 — le reçu de ton vote : téléchargé, retrouvé dans tes bulletins, vérifié ici ; jamais chez les autres', async ({ page, browser }) => {
|
test('C33 — le reçu de ton vote : téléchargé, retrouvé dans tes bulletins, vérifié ici ; jamais chez les autres', async ({ page, browser }) => {
|
||||||
@@ -96,8 +101,8 @@ test('C33 — le reçu de ton vote : téléchargé, retrouvé dans tes bulletins
|
|||||||
await signIn(page, 'Ines');
|
await signIn(page, 'Ines');
|
||||||
await wallet(page, [ALICE]);
|
await wallet(page, [ALICE]);
|
||||||
await page.goto(DOC);
|
await page.goto(DOC);
|
||||||
const fold = await unfold(page, 'Unicité du compte');
|
const fold = await unfold(page, NEVER_ALONE);
|
||||||
await fold.getByRole('button', { name: 'Pour', exact: true }).click();
|
await fold.getByRole('button', { name: 'Contre', exact: true }).click();
|
||||||
const dialog = page.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' });
|
const dialog = page.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' });
|
||||||
await expect(dialog.getByText('Ton vote compte : il est inscrit sur la chaîne Ğ1.')).toBeVisible({ timeout: 20_000 });
|
await expect(dialog.getByText('Ton vote compte : il est inscrit sur la chaîne Ğ1.')).toBeVisible({ timeout: 20_000 });
|
||||||
const [download] = await Promise.all([page.waitForEvent('download'), dialog.getByRole('link', { name: 'Télécharger ton reçu' }).click()]);
|
const [download] = await Promise.all([page.waitForEvent('download'), dialog.getByRole('link', { name: 'Télécharger ton reçu' }).click()]);
|
||||||
@@ -107,7 +112,7 @@ test('C33 — le reçu de ton vote : téléchargé, retrouvé dans tes bulletins
|
|||||||
expect(receipt).toMatchObject({ format: 'ld1', status: 'confirmed', voter: 'Alice', author: ALICE.address });
|
expect(receipt).toMatchObject({ format: 'ld1', status: 'confirmed', voter: 'Alice', author: ALICE.address });
|
||||||
expect(receipt.comment).toMatch(/^ld1:[0-9a-f]{64}$/);
|
expect(receipt.comment).toMatch(/^ld1:[0-9a-f]{64}$/);
|
||||||
expect(receipt.salt).toMatch(/^[0-9a-f]{32}$/);
|
expect(receipt.salt).toMatch(/^[0-9a-f]{32}$/);
|
||||||
expect(receipt.lines).toEqual([expect.objectContaining({ choice: 'for', title: 'Unicité du compte' })]);
|
expect(receipt.lines).toEqual([expect.objectContaining({ choice: 'against', title: NEVER_ALONE })]);
|
||||||
|
|
||||||
// The same ballot, in her ballots.
|
// The same ballot, in her ballots.
|
||||||
await dialog.getByRole('link', { name: 'Tes bulletins' }).click();
|
await dialog.getByRole('link', { name: 'Tes bulletins' }).click();
|
||||||
@@ -121,9 +126,9 @@ test('C33 — le reçu de ton vote : téléchargé, retrouvé dans tes bulletins
|
|||||||
// Checked here, in the browser, without the server.
|
// Checked here, in the browser, without the server.
|
||||||
await page.getByLabel('Fichier du reçu').setInputFiles(file);
|
await page.getByLabel('Fichier du reçu').setInputFiles(file);
|
||||||
await expect(page.getByText('Le reçu tient : ses votes et son sel donnent bien ce commentaire.')).toBeVisible();
|
await expect(page.getByText('Le reçu tient : ses votes et son sel donnent bien ce commentaire.')).toBeVisible();
|
||||||
await expect(page.getByText('Unicité du compte : pour')).toBeVisible();
|
await expect(page.getByText(`${NEVER_ALONE} : contre`)).toBeVisible();
|
||||||
// One choice changed: it no longer holds.
|
// One choice changed: it no longer holds.
|
||||||
const changed = { ...receipt, lines: receipt.lines.map((l: object) => ({ ...l, choice: 'against' })) };
|
const changed = { ...receipt, lines: receipt.lines.map((l: object) => ({ ...l, choice: 'for' })) };
|
||||||
await page.getByLabel('Ou colle le reçu').fill(JSON.stringify(changed));
|
await page.getByLabel('Ou colle le reçu').fill(JSON.stringify(changed));
|
||||||
await page.getByRole('button', { name: 'Vérifier', exact: true }).click();
|
await page.getByRole('button', { name: 'Vérifier', exact: true }).click();
|
||||||
await expect(page.getByText('Le reçu ne tient pas : ses votes et son sel ne donnent pas son commentaire.')).toBeVisible();
|
await expect(page.getByText('Le reçu ne tient pas : ses votes et son sel ne donnent pas son commentaire.')).toBeVisible();
|
||||||
|
|||||||
Reference in New Issue
Block a user