v3 : le vote déplié sous l'article, le panier, la signature par le portefeuille Ğ1
- Le vote se déplie sous l'article, façon accordéon, l'URL pour état (?vote=) :
niveau d'eau, totaux, seuil et voix manquantes, ton vote et où il en est
(enregistré, inscrit sur la chaîne, en attente, à reconfirmer, non compté),
inertie et formule. Le reste d'une version (coquilles, traductions,
signalements, historique) passe dans l'onglet « Détails » du panneau.
- Le panier : article par article, « tout pour / tout contre » par section ou
pour tout le document, jamais pour un article à plusieurs versions ; un seul
geste (une écriture, ou une transaction).
- La signature : le portefeuille de la page (injectedWeb3), polkadot-api chargé
au moment de signer ; sans portefeuille, Ğecko par lien et QR ; la page
attend la confirmation. Un bulletin que le portefeuille n'a pas signé est
lâché aussitôt (DELETE /api/ballots/{id}).
- Pastilles d'ambiance : flèches dans le groupe, anneau de focus pointillé à
la couleur d'accent de la page (3:1 au moins), distinct du choix.
- e2e : chaîne simulée (indexeur et nœud), faux portefeuille ; C21–C24, et
C4, C5, C6, C11, C17, C19, C20, M1 mis à jour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,13 @@ import type { Browser, Page } from '@playwright/test';
|
||||
export const DOC = '/g/monnaie-libre/docs/acte-engagement-certification';
|
||||
export const TEST_DOC = '/g/essai/docs/acte-engagement-certification';
|
||||
|
||||
/** The vote account of the e2e server (serve.sh): Dave's development key. */
|
||||
export const VOTE_ACCOUNT = 'g1L5EYvvSBaYuQCPW78bHxYcvHfmWAqHYe3Gsrp9SV4kWZaEM';
|
||||
/** Wallets of the simulated chain (chain-mock.mjs): Alice and Charlie are members, Bob is not. */
|
||||
export const ALICE = { address: 'g1Pn1geRZnHcVdWdKkoYwQrAARDTXK1D7H3zuXj7GNayT1thu', name: 'Alice' };
|
||||
export const BOB = { address: 'g1NCrQaFReZcbUW8pKsazfWFCKNSdrTomnDraUMybJc4DFF5g', name: 'Bob' };
|
||||
export const CHARLIE = { address: 'g1NFWUkPTyZWE77YUrfBKfAhV9W81qmgdD17v1NGhb1EjYFqi', name: 'Charlie' };
|
||||
|
||||
export async function signIn(page: Page, name: string, admin = false) {
|
||||
const r = await page.request.post('/api/auth/dev', {
|
||||
data: { name, admin },
|
||||
@@ -17,3 +24,61 @@ export async function person(browser: Browser, name: string, admin = false): Pro
|
||||
await signIn(page, name, admin);
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* A fake wallet extension, as duniter-connect offers itself to pages
|
||||
* (`window.injectedWeb3`): it lists `accounts` and signs whatever it is given,
|
||||
* recording it in `window.__ldSigned`; `window.__ldRefuse = true` makes it refuse.
|
||||
* It signs nothing real: the page, in test mode, sends to the simulated node.
|
||||
*/
|
||||
export async function wallet(page: Page, accounts: { address: string; name: string }[]) {
|
||||
await page.addInitScript((list) => {
|
||||
const w = window as unknown as Record<string, unknown> & { __ldSigned: unknown[]; __ldRefuse?: boolean };
|
||||
w.__ldSigned = [];
|
||||
w.injectedWeb3 = {
|
||||
'duniter-connect': {
|
||||
version: 'e2e',
|
||||
enable: async () => ({
|
||||
accounts: { get: async () => list, subscribe: () => () => undefined },
|
||||
signer: {
|
||||
signPayload: async (payload: Record<string, unknown>) => {
|
||||
if (w.__ldRefuse) throw new Error('Cancelled');
|
||||
w.__ldSigned.push(payload);
|
||||
return { id: w.__ldSigned.length, signature: `0x${'01'.repeat(64)}` };
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
}, accounts);
|
||||
}
|
||||
|
||||
/** What the fake wallet signed, read back (the test extrinsic is JSON as hex). */
|
||||
export async function signed(page: Page): Promise<{ address: string; dest: string; amount: string; remark: string }[]> {
|
||||
return page.evaluate(() =>
|
||||
((window as unknown as { __ldSigned: { method: string }[] }).__ldSigned ?? []).map((p) => {
|
||||
const bytes = (p.method.slice(2).match(/../g) ?? []).map((b) => parseInt(b, 16));
|
||||
return JSON.parse(new TextDecoder().decode(Uint8Array.from(bytes)));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** A screenshot for the visual check, when `LD_SHOTS` names a directory (never in the loop). */
|
||||
export async function shot(page: Page, name: string) {
|
||||
const dir = process.env.LD_SHOTS;
|
||||
if (dir) await page.screenshot({ path: `${dir}/${name}.png` });
|
||||
}
|
||||
|
||||
/** 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('/');
|
||||
const g = await (await page.request.get(`/api/groups/${group}`)).json();
|
||||
const d = await (await page.request.get(`/api/docs/${g.docs[0].id}`)).json();
|
||||
const node: string = d.vote.chain.testNode;
|
||||
const base = node.replace(/\/rpc$/, '');
|
||||
return {
|
||||
doc: d as { doc: { id: string }; units: { code: string; inForce: { id: string } | null; proposals: { id: string }[] }[] },
|
||||
node,
|
||||
control: async (what: 'hide' | 'show' | 'release' | 'state' | 'silent') => (await page.request.post(`${base}/control/${what}`)).json(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user