Lot U : contrôles repris sans les affaiblir (C3, C4, C6, C19 à C21, C23 à C26, C34, M1) ; CONTROLES.md (C35 à C37) ; retouches vues aux captures (ligne d'ordre discrète, en-tête plus court sur téléphone, liste à poignées)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 08:08:28 +01:00
co-authored by Claude Opus 5.5
parent 4cf4e47ee9
commit 5bbcfcdfd5
15 changed files with 208 additions and 151 deletions
+16 -13
View File
@@ -4,7 +4,7 @@ Chaque contrôle existe en deux versions : **automatique** (Playwright, `web/e2e
`npm run e2e`) et **humaine** (Chiara, sur la production ou en local). Une remarque de Chiara `npm run e2e`) et **humaine** (Chiara, sur la production ou en local). Une remarque de Chiara
sur un contrôle vaut échec du contrôle tant qu'elle n'est pas traitée. sur un contrôle vaut échec du contrôle tant qu'elle n'est pas traitée.
Où tester : les contrôles qui écrivent (C5, C6, C7, C21, C22, C25, C26, C28–C31) se font dans le **groupe Où tester : les contrôles qui écrivent (C5, C6, C7, C21, C22, C25, C26, C28–C31, C35, C37) se font dans le **groupe
d'essai** (caché, quorum 1 à chaque cran — une voix seule y passe, ADR 0013 —, jamais publié) — jamais sur le texte de la communauté. Au départ, rien d'essai** (caché, quorum 1 à chaque cran — une voix seule y passe, ADR 0013 —, jamais publié) — jamais sur le texte de la communauté. Au départ, rien
n'y est en vigueur (ADR 0011) : les contrôles qui lisent un texte en vigueur (C4 à C6, C20, C21, C24, n'y est en vigueur (ADR 0011) : les contrôles qui lisent un texte en vigueur (C4 à C6, C20, C21, C24,
C28 à C31) y font d'abord entrer Raison d'être, Unicité du compte et Réciprocité par le vote (une voix C28 à C31) y font d'abord entrer Raison d'être, Unicité du compte et Réciprocité par le vote (une voix
@@ -17,31 +17,31 @@ pour une personne, ils coûtent 0,1 Ğ1 réels : c'est un acte « paye » d'Yvv.
|---|---|---|---| |---|---|---|---|
| C1 | L'accueil, sans session | ouvrir `/` sans être connectée | la clé de voûte en grand, jamais inclinée ; sous l'arche, centrés et dans cet ordre : « boîte à outils », « contexte et cadrage », « processus et modalités » ; puis le titre centré sur deux lignes, « Rendre possible » / « la décision collective » ; « Se connecter avec sa clé Ğ1 » et sa note ; pas de piliers ni de groupes ; le menu à gauche avec les quatre outils, trois éteints | | C1 | L'accueil, sans session | ouvrir `/` sans être connectée | la clé de voûte en grand, jamais inclinée ; sous l'arche, centrés et dans cet ordre : « boîte à outils », « contexte et cadrage », « processus et modalités » ; puis le titre centré sur deux lignes, « Rendre possible » / « la décision collective » ; « Se connecter avec sa clé Ğ1 » et sa note ; pas de piliers ni de groupes ; le menu à gauche avec les quatre outils, trois éteints |
| C2 | L'accueil, avec session | se connecter : on arrive sur `/` | le titre, le leitmotiv, puis directement le choix du groupe : Monnaie libre en grand, Axiom, Librodrome, Groupe local Vallée Drôme Crestois ; **pas** l'Atelier du canal ; `/groupes` et « Mes groupes » (menu) ramènent ici | | C2 | L'accueil, avec session | se connecter : on arrive sur `/` | le titre, le leitmotiv, puis directement le choix du groupe : Monnaie libre en grand, Axiom, Librodrome, Groupe local Vallée Drôme Crestois ; **pas** l'Atelier du canal ; `/groupes` et « Mes groupes » (menu) ramènent ici |
| C3 | Le texte au départ : les Propositions seed | Monnaie libre → Docs de référence → Acte d'engagement Certification | aucun article en vigueur (ADR 0011) : « Aucun article n'est en vigueur. Les propositions ci-dessous remontent dès qu'elles atteignent leur seuil. », sous le titre du document, puis à la fin de chaque section ses modules (C25, C26) ; en dessous, « Propositions en cours » (33 lignes, une par article), dont Raison d'être, Unicité du compte et Réciprocité marquées « Proposition seed » (au survol : « Texte de départ mis en avant pour lancer le vote… »), « 0 pour · 0 contre », « inertie 2 / 5 · basse » | | C3 | Le texte au départ : les Propositions seed | Monnaie libre → Docs de référence → Acte d'engagement Certification | aucun article en vigueur (ADR 0011) : « Aucun article n'est encore en vigueur. Commence par les Propositions seed : elles y entrent par le vote, comme toute proposition. » ; puis, **en tête du texte**, en entier, Raison d'être (sous « Introduction »), Unicité du compte et Réciprocité (sous « Engagements fondamentaux »), chacune marquée « Proposition seed » (au survol : « Texte de départ mis en avant pour lancer le vote… »), sur un lavis d'eau, avec « 0 pour · 0 contre · 9 oui sur 12 » et « basse » (au lecteur d'écran : « inertie 2 / 5 · basse ») ; chaque section lue finit par une ligne d'ordre (C25) ; en dessous, « Propositions en cours » **repliées par section** (8 lignes : le nom, « 11 articles », un petit niveau d'eau par article) ; « Tout déplier » : 30 lignes, une par article (les trois Propositions seed ne s'y répètent pas) |
| C4 | Gestes contextuels | groupe d'essai, où la Raison d'être est entrée en vigueur par le vote : la survoler → « Le vote » | le vote se **déplie sous l'article**, sans fenêtre : le seuil et les voix qui manquent ; « La formule du seuil » se déplie : « M — majorité à pleine participation : 50 % — inertie 2 / 5 · basse », et le lien « Réglages de la formule » ; l'URL garde le vote déplié (`?vote=`) ; « Le vote » de nouveau le replie | | C4 | Gestes contextuels | groupe d'essai, où la Raison d'être est entrée en vigueur par le vote : la survoler → « Le vote » | deux gestes sous l'article, « Le vote » et « Proposer une modification » (ses autres versions se lisent dans le vote) ; le vote se **déplie sous l'article**, sans fenêtre : « N oui sur Q votants pour passer » et les voix qui manquent ; « La formule du seuil » se déplie : « M — majorité à pleine participation : 50 % — inertie 2 / 5 · basse », et le lien « Réglages de la formule » ; l'URL garde le vote déplié (`?vote=`) ; « Le vote » de nouveau le replie |
| C5 | Entrée en vigueur | groupe d'essai (Raison d'être, Unicité du compte et Réciprocité entrées en vigueur par le vote) : déplier le vote de « Les deux garanties réciproques », « Pour » ; une deuxième personne si cette voix ne suffit pas | à la voix qui suffit, « … est entré en vigueur » ; le texte en vigueur compte quatre articles, le vote reste déplié sous le nouveau texte (autant de « pour » que de voix, 0 contre) | | C5 | Entrée en vigueur | groupe d'essai (Raison d'être, Unicité du compte et Réciprocité entrées en vigueur par le vote) : déplier le vote de « Les deux garanties réciproques », « Pour » ; une deuxième personne si cette voix ne suffit pas | à la voix qui suffit, « … est entré en vigueur » ; le texte en vigueur compte quatre articles, le vote reste déplié sous le nouveau texte (autant de « pour » que de voix, 0 contre) |
| C6 | Coquille sans vote | groupe d'essai, **en administratrice** (un texte du seed n'a pas d'auteur) : Unicité du compte → « Le vote » → « Coquilles, traductions, objections, historique » (onglet « Détails ») → « Corriger une coquille », remplacer les apostrophes droites par des apostrophes typographiques (« Je m’engage sur l’honneur ») | « Coquille corrigée, sans toucher aux votes. » ; un changement de sens est refusé : « Ce changement touche au sens : propose-le comme modification. » (sans être administratrice, la correction attend : C31) | | C6 | Coquille sans vote | groupe d'essai, **en administratrice** (un texte du seed n'a pas d'auteur) : Unicité du compte → « Le vote » → « Coquilles, traductions, objections, historique » (le panneau « Détails », sans onglets) → « Corriger une coquille », remplacer les apostrophes droites par des apostrophes typographiques (« Je m’engage sur l’honneur ») | « Coquille corrigée, sans toucher aux votes. » ; un changement de sens est refusé : « Ce changement touche au sens : propose-le comme modification. » (sans être administratrice, la correction attend : C31) |
| C7 | Une décision par proposition | groupe d'essai : « Proposer un nouvel article », deux paragraphes « Je m'engage à… » | l'assistant : « Ce texte semble porter 2 décisions » ; « Découper en 2 brouillons » ; « Tes brouillons » apparaît | | C7 | Une décision par proposition | groupe d'essai : « Proposer un nouvel article », deux paragraphes « Je m'engage à… » | l'assistant : « Ce texte semble porter 2 décisions » ; « Découper en 2 brouillons » ; « Tes brouillons » apparaît |
| C8 | Trois langues | choisir EN puis ES pour le texte | « Certification Commitment Act », « Acta de compromiso de Certificación » ; l'interface suit | | C8 | Trois langues | choisir EN puis ES pour le texte | « Certification Commitment Act », « Acta de compromiso de Certificación » ; l'interface suit |
| C9 | Les aperçus | « Si tout passe », puis « Si les bien cotées passent » | 33 articles avec la mention d'aperçu, puis aucun (rien en vigueur dans Monnaie libre, rien de bien coté sans « pour ») | | C9 | Les aperçus | « Si tout passe », puis « Si les bien cotées passent » | 33 articles avec la mention d'aperçu, puis aucun (rien en vigueur dans Monnaie libre, rien de bien coté sans « pour ») |
| C10 | trustinWallet | ouvrir `/acces` sans être administratrice | « La gestion des accès est réservée aux administrateurs de trustinWallet. » (côté admin : créer un lien d'invitation) | | C10 | trustinWallet | ouvrir `/acces` sans être administratrice | « La gestion des accès est réservée aux administrateurs de trustinWallet. » (côté admin : créer un lien d'invitation) |
| C11 | Une personne, une voix | avec un lien d'invitation et un portefeuille dont le compte n'est pas membre de la toile (Bob, chaîne simulée), voter « Pour » sur un article de Monnaie libre | le vote dit que la voix est celle de l'identité Ğ1 du portefeuille qui signe ; après signature : « Ton vote n'a pas été compté : le portefeuille qui a signé n'est pas celui d'un membre de la toile de confiance Ğ1. », et sous l'article « Ton dernier vote n'a pas été compté… », 0 pour ; dans le groupe d'essai, la même personne vote directement, sans portefeuille | | C11 | Une personne, une voix | avec un lien d'invitation et un portefeuille dont le compte n'est pas membre de la toile (Bob, chaîne simulée), voter « Pour » sur un article de Monnaie libre | le vote dit que la voix est celle de l'identité Ğ1 du portefeuille qui signe ; après signature : « Ton vote n'a pas été compté : le portefeuille qui a signé n'est pas celui d'un membre de la toile de confiance Ğ1. », et sous l'article « Ton dernier vote n'a pas été compté… », 0 pour ; dans le groupe d'essai, la même personne vote directement, sans portefeuille |
| C12 | Lien d'équipe | administrateur : `/acces` → « Liens d'équipe », cocher le groupe d'essai, créer ; l'ouvrir dans deux navigateurs, sous deux noms | chacun arrive sur ses groupes sous son nom ; dans `/acces`, « 2 / 30 personnes » et deux identités « venue par » le lien ; un nom déjà pris est refusé, même autrement écrit (« luna » pour « Luna ») ; sur un navigateur où quelqu'un est connecté, « Ce n'est pas moi » le déconnecte ; « Révoquer » ferme le lien | | C12 | Lien d'équipe | administrateur : `/acces` → « Liens d'équipe », cocher le groupe d'essai, créer ; l'ouvrir dans deux navigateurs, sous deux noms | chacun arrive sur ses groupes sous son nom ; dans `/acces`, « 2 / 30 personnes » et deux identités « venue par » le lien ; un nom déjà pris est refusé, même autrement écrit (« luna » pour « Luna ») ; sur un navigateur où quelqu'un est connecté, « Ce n'est pas moi » le déconnecte ; « Révoquer » ferme le lien |
| M1 | Mobile | téléphone (ou fenêtre étroite) | le logo dans la barre du haut ; l'accueil sous la voûte tient à 412 et à 360 de large (les trois points dans l'ouverture, rien ne déborde) et montre les groupes ; menu en tiroir (le nombre à côté de « Docs de référence », les quatre pastilles d'ambiance) ; **au clavier, les pastilles** : un seul arrêt de Tab, les flèches gauche / droite passent d'une ambiance à l'autre et la choisissent ; l'anneau de focus, en pointillé, se lit sur la page (contraste 3:1 au moins, dans les quatre ambiances) et ne se confond pas avec l'anneau plein de l'ambiance choisie ; les trois gestes visibles sans survol ; le vote se déplie sous l'article ; « Réglages de la formule » depuis le vote : le panneau en feuille, l'onglet « Réglages » visible, le tableau des cinq crans tient dans la feuille ; « Voter en lot » : le panier tient dans la feuille, son bouton « Signer et envoyer » reste en vue | | M1 | Mobile | téléphone (ou fenêtre étroite) | le logo dans la barre du haut ; l'accueil sous la voûte tient à 412 et à 360 de large (les trois points dans l'ouverture, rien ne déborde) et montre les groupes ; menu en tiroir (le nombre à côté de « Docs de référence », les quatre pastilles d'ambiance) ; **au clavier, les pastilles** : un seul arrêt de Tab, les flèches gauche / droite passent d'une ambiance à l'autre et la choisissent ; l'anneau de focus, en pointillé, se lit sur la page (contraste 3:1 au moins, dans les quatre ambiances) et ne se confond pas avec l'anneau plein de l'ambiance choisie ; les gestes visibles sans survol, « Le vote » d'au moins 44 px ; le vote de la Raison d'être (en tête) se déplie sous elle : « 9 oui sur 12 votants pour passer » ; « Réglages de la formule » depuis le vote : le panneau en feuille, nommé « Réglages de la formule » en tête, le tableau des cinq crans tient dans la feuille ; « Voter en lot » : le panier tient dans la feuille, son bouton « Signer et envoyer » reste en vue |
| C13 | Logo et ambiances | ouvrir `/` (appareil en clair, puis en sombre) ; choisir chaque pastille d'ambiance ; recharger | le logo « Clé de voûte » (signe, *libre*, **Decision**) droit, sans tampon ; le sceau 井 en bas à droite ; première visite : pierre (clair) ou nuit (sombre) ; les quatre pastilles (Pierre, Calcaire, Nuit, Ardoise) changent l'ambiance, qui reste après rechargement ; favicon, icône Apple et manifeste servis | | C13 | Logo et ambiances | ouvrir `/` (appareil en clair, puis en sombre) ; choisir chaque pastille d'ambiance ; recharger | le logo « Clé de voûte » (signe, *libre*, **Decision**) droit, sans tampon ; le sceau 井 en bas à droite ; première visite : pierre (clair) ou nuit (sombre) ; les quatre pastilles (Pierre, Calcaire, Nuit, Ardoise) changent l'ambiance, qui reste après rechargement ; favicon, icône Apple et manifeste servis |
| C14 | La page d'un groupe | Monnaie libre | le nom ; le pavé des quatre piliers (Docs de référence « ouvert », les trois autres « à venir ») ; « En cours » : l'Acte d'engagement Certification, qui ouvre son lecteur | | C14 | La page d'un groupe | Monnaie libre | le nom ; le pavé des quatre piliers (Docs de référence « ouvert », les trois autres « à venir ») ; « En cours » : l'Acte d'engagement Certification, qui ouvre son lecteur |
| C15 | Le menu | survoler les outils ; ouvrir Docs, puis Mandats ; revenir à l'accueil | « 1 » à côté de « Docs de référence » ; aucun outil surligné sur la page du groupe, seul celui où l'on est ensuite ; au survol, seul Docs réagit ; Mandats s'ouvre quand même (ses « À venir ») ; hors d'un groupe, les outils mènent au dernier groupe ouvert (ou à l'accueil s'il n'y en a pas) | | C15 | Le menu | survoler les outils ; ouvrir Docs, puis Mandats ; revenir à l'accueil | « 1 » à côté de « Docs de référence » ; aucun outil surligné sur la page du groupe, seul celui où l'on est ensuite ; au survol, seul Docs réagit ; Mandats s'ouvre quand même (ses « À venir ») ; hors d'un groupe, les outils mènent au dernier groupe ouvert (ou à l'accueil s'il n'y en a pas) |
| C16 | En cours, à venir | Monnaie libre → Docs, Mandats, Protocoles ; les anciennes adresses `/decisions`, `/mandats`, `/protocoles` | Docs : « En cours » (Acte d'engagement Certification) et « À venir » (Acte d'engagement forgerons) ; Mandats : Administration forum Duniter, Administration forum Monnaie libre ; Protocoles : Runtime upgrade — décision on-chain ; les « À venir » sont éteints, sans lien ; ils suivent la langue ; les anciennes adresses mènent aux outils du groupe courant | | C16 | En cours, à venir | Monnaie libre → Docs, Mandats, Protocoles ; les anciennes adresses `/decisions`, `/mandats`, `/protocoles` | Docs : « En cours » (Acte d'engagement Certification) et « À venir » (Acte d'engagement forgerons) ; Mandats : Administration forum Duniter, Administration forum Monnaie libre ; Protocoles : Runtime upgrade — décision on-chain ; les « À venir » sont éteints, sans lien ; ils suivent la langue ; les anciennes adresses mènent aux outils du groupe courant |
| C17 | Décisions | Monnaie libre → Décisions ; voter (par la chaîne : bulletin, transaction, confirmation) sur une version d'un article, recharger | une ligne par doc : « Acte d'engagement Certification · 41 modules à voter · dont 41 sans ton vote » (33 articles, le titre, l'ordre de 7 sections), puis « dont 40 » ; la ligne ouvre le lecteur | | C17 | Décisions | Monnaie libre → Décisions ; voter (par la chaîne : bulletin, transaction, confirmation) sur une version d'un article, recharger | une ligne par doc : « Acte d'engagement Certification · 41 modules à voter · dont 41 sans ton vote » (33 articles, le titre, l'ordre de 7 sections), puis « dont 40 » ; la ligne ouvre le lecteur |
| C18 | La Raison d'être | Monnaie libre → Acte d'engagement Certification → « Si tout passe », puis EN, puis ES | en tête du texte, « Raison d'être » : la première phrase en italique, puis deux paragraphes ; « Purpose », « Razón de ser » | | C18 | La Raison d'être | Monnaie libre → Acte d'engagement Certification → « Si tout passe », puis EN, puis ES | en tête du texte, « Raison d'être » : la première phrase en italique, puis deux paragraphes ; « Purpose », « Razón de ser » |
| C19 | Les versions d'un article | Monnaie libre → « Propositions en cours » → « Connaissance suffisante » (au clavier : Tab jusqu'à la ligne, Entrée) | une seule ligne, « 2 versions » ; dépliée : « variante 1.0.0 » et « variante 0.4.0-draft », chacune avec son niveau et ce qui la distingue ; un clic sur une version déplie son vote sous elle ; un nouveau clic sur la ligne la replie | | C19 | Les versions d'un article | Monnaie libre → « Propositions en cours » → « Tout déplier » → « Connaissance suffisante » (au clavier : Tab jusqu'à la ligne, Entrée) | une seule ligne, « 2 versions » ; dépliée : « variante 1.0.0 » et « variante 0.4.0-draft », chacune avec son niveau et ce qui la distingue ; un clic sur une version déplie son vote sous elle (cran 3, quorum 50 : « 39 oui sur 50 votants pour passer ») ; un nouveau clic sur la ligne la replie |
| C20 | Réglages de la formule et cinq crans | Monnaie libre → Acte d'engagement : « Réglages de la formule » (en-tête) ; puis, dans « Propositions en cours », Unicité du compte → son vote → « Réglages de la formule » ; dans le groupe d'essai, Réciprocité (entrée en vigueur par le vote) → « Le vote » → « Proposer un changement d'inertie » → 5 → « Soumettre au vote » | W 5 745 (toile de confiance Ğ1), B 0,1 ; « 100 votants sur 5 745 (1,74 %), deux fois 50, le quorum de référence : la participation de référence… (0 pour l'instant) » ; le tableau des cinq crans : très basse, basse, moyenne, haute, très haute ; M 50, 50, 55, 60, 66 % ; part exigée 60, 67, 75, 83, 90 % ; quorum 12, 12, 50, 50, 50 (ADR 0013) ; voix pour à 100 votants 60, 67, 75, 83, 90, « Voix pour : à 100 votants, comme d'habitude. », « Part exigée : la part de « pour » demandée, à 100 votants comme d'habitude. », « Quorum : en dessous, les votants qui manquent comptent comme « non ». … » ; depuis le vote déplié : « Inertie · 2 / 5 · basse », la ligne du cran 2 marquée « cet article », à 0 votant 8, 9, 39, 43, 46 voix, « Voix pour : à 0 votant, la participation de cette version. » ; le sélecteur montre les cinq crans, 2 « actuelle », atteignable au clavier mais jamais choisi ; un seul arrêt de tabulation ; les flèches passent d'un cran à l'autre et le choisissent, en boucle ; le vote de la proposition se déplie : « inertie vers 5 / 5 · très haute », votée à « inertie 2 / 5 · basse » | | C20 | Réglages de la formule et cinq crans | Monnaie libre → Acte d'engagement : « Réglages de la formule » (en-tête) ; puis, en tête du texte, Unicité du compte (Proposition seed) → « Le vote » → « Réglages de la formule » ; dans le groupe d'essai, Réciprocité (entrée en vigueur par le vote) → « Le vote » → « Proposer un changement d'inertie » → 5 → « Soumettre au vote » | W 5 745 (toile de confiance Ğ1), B 0,1 ; « 100 votants sur 5 745 (1,74 %), deux fois 50, le quorum de référence : la participation de référence… (0 pour l'instant) » ; le tableau des cinq crans : très basse, basse, moyenne, haute, très haute ; M 50, 50, 55, 60, 66 % ; part exigée 60, 67, 75, 83, 90 % ; quorum 12, 12, 50, 50, 50 (ADR 0013) ; voix pour à 100 votants 60, 67, 75, 83, 90, « Voix pour : à 100 votants, comme d'habitude. », « Part exigée : la part de « pour » demandée, à 100 votants comme d'habitude. », « Quorum : en dessous, les votants qui manquent comptent comme « non ». … » ; depuis le vote déplié : « Inertie · 2 / 5 · basse », la ligne du cran 2 marquée « cet article », à 0 votant 8, 9, 39, 43, 46 voix, « Voix pour : à 0 votant, la participation de cette version. » ; le sélecteur montre les cinq crans, 2 « actuelle », atteignable au clavier mais jamais choisi ; un seul arrêt de tabulation ; les flèches passent d'un cran à l'autre et le choisissent, en boucle ; le vote de la proposition se déplie : « inertie vers 5 / 5 · très haute », votée à « inertie 2 / 5 · basse » |
| C21 | Le vote déplié | groupe d'essai : Unicité du compte → « Le vote », puis « Pour » ; une autre personne ouvre le même vote | sous l'article : le niveau d'eau, « 0 pour · 0 contre », « Seuil : … voix pour », « Il manque … », « Tu n'as pas voté sur cette version. » ; après « Pour » : « Ton vote : pour · enregistré », « 1 pour · 0 contre », le bouton enfoncé, « Ton vote ne se montre à personne : seuls les totaux sont visibles. » ; l'autre personne voit 1 pour, jamais qui ; « Le vote » replie ; « Retirer mon vote » remet 0 | | C21 | Le vote déplié | groupe d'essai : Unicité du compte → « Le vote », puis « Pour » ; une autre personne ouvre le même vote | sous l'article : le niveau d'eau, « 0 pour · 0 contre », « N oui sur Q votants pour passer » (le quorum en infobulle), « Il manque … », les crans de l'article, « Tu n'as pas voté sur cette version. » ; après « Pour » : « Ton vote : pour · enregistré », « 1 pour · 0 contre », le bouton enfoncé, « Ton vote ne se montre à personne : seuls les totaux sont visibles. » ; l'autre personne voit 1 pour, jamais qui ; « Le vote » replie ; « Retirer mon vote » remet 0 |
| 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é (Propositions seed), « Signer et envoyer (2) » ; puis, dans « Propositions en cours », 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 la proposition « 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é (Propositions seed), « Signer et envoyer (2) » ; puis, en tête du texte, sous Réciprocité, « Contre » (l'indexeur en retard) puis « Retirer mon vote » ; puis sans portefeuille : la Raison d'être → « 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 la proposition « 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 la première proposition : « 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 faire une objection. », 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 la première Proposition seed : « 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 faire une objection. », pas de bouton, les totaux et « 9 oui sur 12 votants pour passer » 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à |
| C25 | L'ordre des articles | groupe d'essai : en fin de « Conclusion », « Proposer un autre ordre » ; cliquer « Monnaie G1 » puis la place 1 ; au clavier, flèche bas puis haut ; « Proposer cet ordre » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas (dès 16 membres, une voix suffit dans le groupe d'essai : ADR 0007) ; puis, dans Monnaie libre, sur un téléphone, avec un portefeuille : « Voter en lot », « Pour » sur « Ordre des articles · Engagements fondamentaux », « Signer et envoyer (1) » | chaque section finit par « Ordre des articles · <section> » (7, aucun pour l'Ordonnancement, un seul article), ses articles numérotés, « 2 articles · inertie 2 / 5 · basse », « seed a minima · 0 vote » ; le vote d'un ordre en vigueur déplié montre la liste entière ; la liste où l'on clique : l'article choisi marqué, puis à sa place, annoncé (« « Monnaie G1 » en place 1 sur 2. »), « Proposer cet ordre » éteint tant que c'est l'ordre en vigueur ; le vote de l'ordre proposé se déplie dans la liste des propositions, l'ordre proposé en clair ; il est dans le panier, à la fin de sa section ; à la voix qui suffit : « Le nouvel ordre de « Conclusion » est entré en vigueur. », la section se lit dans le nouvel ordre ; par la chaîne : « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « seed a minima · 1 vote » ; sur un téléphone, rien ne déborde, chaque ligne fait au moins 44 px | | C25 | L'ordre des articles | groupe d'essai : « Tout déplier » ; en fin de « Conclusion », la ligne « Ordre de la section » → « Réordonner » ; cliquer « Monnaie G1 » puis la place 1 ; au clavier, flèche bas puis haut ; « Proposer cet ordre » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas (quorum 1 dans le groupe d'essai : ADR 0013) ; puis, dans Monnaie libre, sur un téléphone, avec un portefeuille : « Voter en lot », « Pour » sur « Ordre des articles · Engagements fondamentaux », « Signer et envoyer (1) » | chaque section finit par **une ligne** « Ordre de la section » (7, aucune pour l'Ordonnancement, un seul article) : la poignée, les crans « basse », « seed a minima · 0 vote », **jamais la liste de ses articles** ; à la fin du texte quand la section s'y lit, sinon à la fin de sa section dans « Propositions en cours » ; le vote d'un ordre en vigueur déplié sous sa ligne montre la liste entière ; « Réordonner » : les articles de la section, sur place, sans panneau ; l'article choisi marqué, puis à sa place, annoncé (« « Monnaie G1 » en place 1 sur 2. »), « Proposer cet ordre » éteint tant que c'est l'ordre en vigueur ; le vote de l'ordre proposé se déplie sous la ligne, l'ordre proposé en clair, « Ordre en vigueur » parmi ses « Autres versions », « 1 ordre proposé » sur la ligne ; aucune ligne d'ordre parmi les propositions ; il est dans le panier, à la fin de sa section ; à la voix qui suffit : « Le nouvel ordre de « Conclusion » est entré en vigueur. », la section se lit dans le nouvel ordre ; par la chaîne : « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « seed a minima · 1 vote » ; sur un téléphone, les gestes du titre sans survol (réordonner sur un téléphone : C35) |
| C26 | Le titre du document | groupe d'essai : sous le titre, « Proposer un autre titre » ; « Acte d'engagement de la certification » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas ; lire en anglais | sous le titre : « Titre du document · inertie 4 / 5 · haute · seed a minima · 0 vote » et les trois gestes ; le titre proposé se vote dans la liste des propositions (« Titre du document ») ; à la voix qui suffit : « Le titre « … » est entré en vigueur. », le document change de titre, son vote reste déplié sous le titre ; en anglais, l'ancien titre traduit, marqué « translation needs review » | | C26 | Le titre du document | groupe d'essai : sous le titre, « Proposer un autre titre » ; « Acte d'engagement de la certification » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas ; lire en anglais | sous le titre, une ligne : « Titre du document · seed a minima · 0 vote », les crans « inertie 4 / 5 · haute », deux gestes ; le panneau nommé « Proposer un autre titre » ; le titre proposé se vote **sous le titre** (« 1 titre proposé »), jamais parmi les propositions ; à la voix qui suffit : « Le titre « … » est entré en vigueur. », le document change de titre, son vote reste déplié sous le titre ; en anglais, l'ancien titre traduit, marqué « translation needs review » |
| C27 | La participation ramenée dans ses bornes | un groupe de 4 dont la médiane est 2,5 votants, ramenée à 50 % (l'automate fait répondre ces chiffres au serveur) : « Réglages de la formule » | « 3 votants sur 4 (62,5 %) : la médiane des 12 propositions votées depuis un an. » — la part de la mesure elle-même, jamais 3 sur 4 arrondi d'abord ; puis « Ramenée entre 5 % et 50 % de l'électorat : le groupe demande ses voix pour comme si 2 personnes votaient (50 %). » ; « Part exigée : la part de « pour » demandée, à 2 votants comme d'habitude. » | | C27 | La participation ramenée dans ses bornes | un groupe de 4 dont la médiane est 2,5 votants, ramenée à 50 % (l'automate fait répondre ces chiffres au serveur) : « Réglages de la formule » | « 3 votants sur 4 (62,5 %) : la médiane des 12 propositions votées depuis un an. » — la part de la mesure elle-même, jamais 3 sur 4 arrondi d'abord ; puis « Ramenée entre 5 % et 50 % de l'électorat : le groupe demande ses voix pour comme si 2 personnes votaient (50 %). » ; « Part exigée : la part de « pour » demandée, à 2 votants comme d'habitude. » |
| C28 | Objection « redit » ; sur la version retenue, « contre » et retrait, jamais « pour » | groupe d'essai, à trois (Aude, Oscar, Vera) : Aude propose un nouvel article qui répète « Unicité du compte » ; Oscar, dans « Détails » de cette proposition → « Faire une objection » → « Redit », « En vigueur — Unicité du compte », un motif → « Envoyer l'objection » ; Vera déplie son vote → « Contre », puis « Retirer mon vote » ; Vera ouvre « Voter en lot » → « Tout le document » : « Tout pour », puis « Tout contre », puis « Vider » ; Aude ouvre « Messages » → « Modifier l'article visé », complète le texte et soumet ; Oscar ouvre « Messages » → « Lever l'objection » ; Vera déplie de nouveau son vote | « Objection enregistrée : jusqu'à sa levée, personne ne peut plus voter pour la dernière arrivée des deux versions. » ; chez Oscar, « « … » redit « Unicité du compte » », « personne ne peut plus voter pour cette version », « Lever l'objection » ; chez Vera, « Une objection retient cette version : jusqu'à sa levée, personne ne peut plus voter pour. Tu peux voter contre ou retirer ton vote ; à la levée, elle n'entre en vigueur qu'avec les voix encore là. », « Elle redit « Unicité du compte ». », pas de « Pour », mais « Contre », la ligne marquée « retenue par une objection » ; après « Contre » : « Vote enregistré. », « Ton vote : contre », « 0 pour · 1 contre », et « Retirer mon vote » ; après le retrait : « Vote retiré. », « 0 pour · 0 contre », toujours pas de « Pour » ; dans le panier, la version retenue montre « Contre » sans « Pour », avec « une objection la retient : contre ou retrait, pas de « pour » » ; « Tout pour » la laisse de côté et le dit (« Tu as tout mis pour, sauf 1 version qu'une objection retient : sur elle, tu peux voter contre ou retirer ton vote. », le nombre suivant les versions retenues du document) ; « Tout contre » la prend (« Contre » enfoncé) ; chez Aude, « Messages, 1 non lu », le message « Oscar fait une objection : ta version « … » redit « Unicité du compte »… Pour répondre, modifie l'article visé. », puis le panneau « Proposer » sur Unicité du compte, partant de son texte, avec « Cette proposition répond à une objection… » ; chez Oscar, « Ton objection sur « … » a reçu une réponse. », une échéance, « Lever l'objection » et « Réitérer l'objection » ; levée : « Objection levée. », le message dit « objection levée » sans plus aucun geste ; Vera peut de nouveau voter, « Pour » compris | | C28 | Objection « redit » ; sur la version retenue, « contre » et retrait, jamais « pour » | groupe d'essai, à trois (Aude, Oscar, Vera) : Aude propose un nouvel article qui répète « Unicité du compte » ; Oscar, dans « Détails » de cette proposition → « Faire une objection » → « Redit », « En vigueur — Unicité du compte », un motif → « Envoyer l'objection » ; Vera déplie son vote → « Contre », puis « Retirer mon vote » ; Vera ouvre « Voter en lot » → « Tout le document » : « Tout pour », puis « Tout contre », puis « Vider » ; Aude ouvre « Messages » → « Modifier l'article visé », complète le texte et soumet ; Oscar ouvre « Messages » → « Lever l'objection » ; Vera déplie de nouveau son vote | « Objection enregistrée : jusqu'à sa levée, personne ne peut plus voter pour la dernière arrivée des deux versions. » ; chez Oscar, « « … » redit « Unicité du compte » », « personne ne peut plus voter pour cette version », « Lever l'objection » ; chez Vera, « Une objection retient cette version : jusqu'à sa levée, personne ne peut plus voter pour. Tu peux voter contre ou retirer ton vote ; à la levée, elle n'entre en vigueur qu'avec les voix encore là. », « Elle redit « Unicité du compte ». », pas de « Pour », mais « Contre », la ligne marquée « retenue par une objection » ; après « Contre » : « Vote enregistré. », « Ton vote : contre », « 0 pour · 1 contre », et « Retirer mon vote » ; après le retrait : « Vote retiré. », « 0 pour · 0 contre », toujours pas de « Pour » ; dans le panier, la version retenue montre « Contre » sans « Pour », avec « une objection la retient : contre ou retrait, pas de « pour » » ; « Tout pour » la laisse de côté et le dit (« Tu as tout mis pour, sauf 1 version qu'une objection retient : sur elle, tu peux voter contre ou retirer ton vote. », le nombre suivant les versions retenues du document) ; « Tout contre » la prend (« Contre » enfoncé) ; chez Aude, « Messages, 1 non lu », le message « Oscar fait une objection : ta version « … » redit « Unicité du compte »… Pour répondre, modifie l'article visé. », puis le panneau « Proposer » sur Unicité du compte, partant de son texte, avec « Cette proposition répond à une objection… » ; chez Oscar, « Ton objection sur « … » a reçu une réponse. », une échéance, « Lever l'objection » et « Réitérer l'objection » ; levée : « Objection levée. », le message dit « objection levée » sans plus aucun geste ; Vera peut de nouveau voter, « Pour » compris |
| C29 | Objection « contredit », levée à l'échéance | groupe d'essai : Aude propose un article qui contredit « Réciprocité » ; Oscar fait une objection « Contredit » ; Aude déplie le vote de sa proposition → « Reformuler », change le fond, « Enregistrer la correction » ; Oscar ne fait rien ; trois jours passent (l'automate avance l'horloge du serveur : `POST /api/dev/clock`, connexion de développement seulement) | chez Aude : « Elle contredit « Réciprocité ». », « Pour qu'elle se vote à nouveau, reformule-la… », « Reformuler » ouvre « Détails » avec le formulaire de modification ; « Nouvelle version enregistrée : les votants devront reconfirmer. », puis « Une réponse est arrivée : Oscar lève ou réitère l'objection d'ici le … » ; trois jours après : l'objection dit « levée : 3 jours sans suite », la proposition se vote de nouveau, le message d'Oscar dit « objection levée : 3 jours sans suite », sans geste | | C29 | Objection « contredit », levée à l'échéance | groupe d'essai : Aude propose un article qui contredit « Réciprocité » ; Oscar fait une objection « Contredit » ; Aude déplie le vote de sa proposition → « Reformuler », change le fond, « Enregistrer la correction » ; Oscar ne fait rien ; trois jours passent (l'automate avance l'horloge du serveur : `POST /api/dev/clock`, connexion de développement seulement) | chez Aude : « Elle contredit « Réciprocité ». », « Pour qu'elle se vote à nouveau, reformule-la… », « Reformuler » ouvre « Détails » avec le formulaire de modification ; « Nouvelle version enregistrée : les votants devront reconfirmer. », puis « Une réponse est arrivée : Oscar lève ou réitère l'objection d'ici le … » ; trois jours après : l'objection dit « levée : 3 jours sans suite », la proposition se vote de nouveau, le message d'Oscar dit « objection levée : 3 jours sans suite », sans geste |
@@ -49,7 +49,10 @@ pour une personne, ils coûtent 0,1 Ğ1 réels : c'est un acte « paye » d'Yvv.
| C31 | Coquille sur un texte en vigueur | groupe d'essai : une administratrice (Ada) ouvre le groupe ; Vera, sur « Raison d'être » → « Le vote » → « Coquilles, traductions, objections, historique » → « Corriger une coquille », « C'est le seul » → « C’est le seul » ; Ada ouvre « Messages » → « Valider » | chez Vera : « Ta correction attend un administrateur, ce texte venant du seed… » ; le texte en vigueur ne change pas (« C'est le seul ») ; la correction en attente se lit dans « Détails » ; chez Ada : « Vera propose une correction de coquille dans « Raison d'être », un texte du seed : un administrateur la valide. » ; après « Valider », le texte en vigueur porte « C’est le seul » et l'historique « validée par Ada » | | C31 | Coquille sur un texte en vigueur | groupe d'essai : une administratrice (Ada) ouvre le groupe ; Vera, sur « Raison d'être » → « Le vote » → « Coquilles, traductions, objections, historique » → « Corriger une coquille », « C'est le seul » → « C’est le seul » ; Ada ouvre « Messages » → « Valider » | chez Vera : « Ta correction attend un administrateur, ce texte venant du seed… » ; le texte en vigueur ne change pas (« C'est le seul ») ; la correction en attente se lit dans « Détails » ; chez Ada : « Vera propose une correction de coquille dans « Raison d'être », un texte du seed : un administrateur la valide. » ; après « Valider », le texte en vigueur porte « C’est le seul » et l'historique « validée par Ada » |
| 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 | | 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 : « 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é | | 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é |
| C34 | Le vote simulé, la chaîne fermée (ADR 0012) | Monnaie libre sur un serveur sans compte des votes (l'automate ferme la chaîne du serveur jetable, puis la rouvre) : « Propositions en cours » → Logiciel auditable → « Contre », sans portefeuille ; puis « Tes bulletins » | « Voter en lot » est là ; aucune fenêtre de portefeuille ; « Ton vote : contre » aussitôt, et un « contre » de plus dans les totaux ; dans l'API, `myVoteState` « simulated » et `tallySimulated` ; dans « Tes bulletins » : « Simulé : compté pour « Sacha » jusqu'à l'ouverture de la chaîne Ğ1 », sans bloc | | C34 | Le vote simulé, la chaîne fermée (ADR 0012) | Monnaie libre sur un serveur sans compte des votes (l'automate ferme la chaîne du serveur jetable, puis la rouvre) : « Propositions en cours » → Logiciel auditable → « Contre », sans portefeuille ; puis « Voter en lot », « Contre » encore, « Voter (1) » ; puis « Tes bulletins » | « Voter en lot » est là, marqué « vote simulé » ; aucune fenêtre de portefeuille ; « Vote simulé. », **jamais** « Vote enregistré. » ; « Ton vote : contre · simulé » aussitôt, et un « contre » de plus dans les totaux ; dans l'API, `myVoteState` « simulated » et `tallySimulated` ; le panier : « 1 vote simulé. », jamais « enregistré » ; dans « Tes bulletins » : « Simulé : compté pour « Sacha » tant que les votes simulés ne sont pas effacés », sans bloc (l'ouverture de la chaîne n'efface rien : c'est un acte humain, sans date promise) |
| C35 | Réordonner une section, par poignées, puis voter (lot U) | groupe d'essai : à la fin des « Engagements fondamentaux », la ligne « Ordre de la section » → « Réordonner » ; glisser la poignée de Réciprocité sur Unicité du compte ; au clavier, flèche haut puis bas ; « Descendre « Réciprocité » » ; cliquer Réciprocité puis la place 2 ; « Annuler », puis « Réordonner » et « Monter « Réciprocité » » ; « Proposer cet ordre » ; « Pour » (une deuxième voix si la sienne ne suffit pas) ; puis, sur un téléphone, dans Monnaie libre : « Tout déplier », « Ordre des articles · Engagements techniques » → « Réordonner » | la ligne ne répète aucun article ; « Réordonner » : les articles de la section, en vigueur ou seulement proposés (« Engagements fondamentaux » marqué « proposé »), sur place (pas de panneau), une poignée devant chacun, les articles en vigueur de la section repliés dans la liste (pas deux fois) ; chaque geste annonce la place (« « Réciprocité » en place 2 sur 3. ») ; « Proposer cet ordre » éteint sur l'ordre en vigueur ; « Annuler » rend la section telle qu'elle se lisait ; « Ordre proposé au vote. », son vote déplié aussitôt sous la ligne, l'ordre proposé en clair, « 1 ordre proposé » ; « Vote enregistré. » ; en vigueur, la section se lit dans le nouvel ordre, « adopté le … » ; sur un téléphone : 11 articles, rien ne déborde de la page, l'article, sa poignée et ses flèches font au moins 44 px |
| C36 | Combien de oui pour passer, combien de non pour bloquer (ADR 0013 ; Yvv, 02/11) | Monnaie libre : la Raison d'être (en tête) → « Le vote » ; « Tout déplier » ; puis, l'automate donnant à une proposition du groupe d'essai les totaux 9 pour · 4 contre (cran 2, quorum 12), puis 3 pour · 0 contre | avant tout vote, sous l'article : « 9 oui sur 12 votants pour passer », au survol « Quorum : 12 votants. En dessous, les votants qui manquent comptent comme « non ». » ; dans la liste, en bref : « 39 oui sur 50 » (cran 3) ; à 9 pour · 4 contre : « 11 oui pour passer · 4 non pour bloquer » sous l'article, « passe à 11 oui · bloquée à 4 non » dans la liste, « Il manque 2 voix pour. » ; à 3 pour · 0 contre : « 9 oui pour passer · le quorum la bloque », « passe à 9 oui » |
| C37 | Le quorum d'un article, par son autrice, avant le premier vote (ADR 0013) | groupe d'essai : proposer un article, puis son vote → « Quorum » : 3 → « Fixer ce quorum » ; « Revenir au défaut (1) » ; 100000 → « Fixer ce quorum » ; une autre personne ouvre le même vote, puis vote « contre » ; l'autrice tente 2 | le réglage n'apparaît qu'à l'autrice (`canSetQuorum`) : « Quorum · 1 votant », « Par défaut, celui de son cran : 1 votant. », le champ « Quorum de l'article » ; « Quorum fixé : 3 votants. », la ligne dit « … oui sur 3 votants pour passer » ; « Quorum revenu au défaut : 1 votant. » ; hors bornes (400) : « Ce quorum sort des bornes : au moins 2 votants (1 dans le groupe d'essai), au plus … » ; l'autre personne ne voit pas le réglage ; après son vote (409) : « Quelqu'un a déjà voté sur cet article : son quorum ne change plus. », puis le réglage disparaît |
## Au-delà de la liste ## Au-delà de la liste
+3 -1
View File
@@ -12,8 +12,10 @@ import { ALICE, chain, DOC, EVE, person, shot, signIn, VOTE_ACCOUNT, wallet } fr
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 ». */ /** Unfolds the vote of an open proposal, from « Propositions en cours » (its sections folded: lot U). */
async function unfold(page: Page, title: string) { async function unfold(page: Page, title: string) {
const all = page.getByRole('button', { name: 'Tout déplier' });
if (await all.isVisible()) await all.click();
// By its title only: the excerpt of another proposal may name it. // By its title only: the excerpt of another proposal may name it.
await page.locator('ul.deep button.row').filter({ has: page.locator('span.title').getByText(title, { exact: true }) }).click(); await page.locator('ul.deep button.row').filter({ has: page.locator('span.title').getByText(title, { exact: true }) }).click();
return page.getByRole('region', { name: `Le vote de « ${title} »` }); return page.getByRole('region', { name: `Le vote de « ${title} »` });
+9 -5
View File
@@ -80,14 +80,18 @@ test('M1 — mobile : logo dans la barre, l’accueil sous la voûte, menu en ti
await drawer.getByRole('button', { name: 'Fermer' }).first().click(); await drawer.getByRole('button', { name: 'Fermer' }).first().click();
// The vote unfolds under the article, on a phone too (a seed proposal, ADR 0011: nothing is // The vote unfolds under the article, on a phone too (a seed proposal, ADR 0011: nothing is
// in force at the start, its vote unfolds in the list of proposals). // in force at the start, the seed proposals open the text — lot U).
await page.locator('ul.deep button.row').first().click(); const first = page.locator('section.text article.unit').first();
const fold = page.locator('ul.deep section.fold'); const toggle = first.getByRole('button', { name: 'Le vote' });
await expect(fold.getByText('Seuil : 9 voix pour')).toBeVisible(); // A finger's target, and nothing to hover with: the gestures are in sight.
expect((await toggle.boundingBox())?.height ?? 0).toBeGreaterThanOrEqual(44);
await toggle.click();
const fold = page.locator('section.text section.fold');
await expect(fold.getByText('9 oui sur 12 votants pour passer', { exact: true })).toBeVisible();
// The settings of the formula read on a phone: the table of the five levels holds in the sheet. // The settings of the formula read on a phone: the table of the five levels holds in the sheet.
await fold.getByRole('button', { name: 'Réglages de la formule' }).click(); await fold.getByRole('button', { name: 'Réglages de la formule' }).click();
const panel = page.locator('aside.panel'); const panel = page.locator('aside.panel');
await expect(panel.getByRole('tab', { name: 'Réglages' })).toBeInViewport(); await expect(panel.getByRole('heading', { name: 'Réglages de la formule' })).toBeInViewport();
await expect(panel.locator('table tbody tr')).toHaveCount(5); await expect(panel.locator('table tbody tr')).toHaveCount(5);
const fits = await panel.locator('.scroll').evaluate((el) => el.scrollWidth <= el.clientWidth + 1); const fits = await panel.locator('.scroll').evaluate((el) => el.scrollWidth <= el.clientWidth + 1);
expect(fits).toBe(true); expect(fits).toBe(true);
+54 -62
View File
@@ -1,22 +1,18 @@
import { devices, expect, test, type Browser, type Page } from '@playwright/test'; import { devices, expect, test, type Browser, type Page } from '@playwright/test';
import { ALICE, DOC, person, shot, signIn, TEST_DOC, wallet } from './helpers'; import { ALICE, DOC, person, shot, signIn, TEST_DOC, unfold, wallet } from './helpers';
// Lot D — the modules (Yvv, 2026-10-30; ADR 0009): at the end of each section, the // Lot D — the modules (Yvv, 2026-10-30; ADR 0009): at the end of each section, the
// order of its articles (inertia 2), proposed with a list one clicks; at the head of // order of its articles (inertia 2); at the head of the document, its title (inertia 4).
// the document, its title (inertia 4). Both voted like an article: unfolded, in the // Both voted like an article: unfolded, in the basket, on the Ğ1 chain in Monnaie libre
// basket, on the Ğ1 chain in Monnaie libre (simulated here: e2e/chain-mock.mjs). // (simulated here: e2e/chain-mock.mjs). Lot U (Yvv, 2026-11-02): the order is one line at
// The server is shared with the specs that follow: the test group keeps its articles // the end of its section, never the list of its articles again; « Réordonner » puts handles
// and their votes; its conclusion is reordered and its title changes. // on them in place (C35); every version of a module is voted at its place, never among the
// proposals. The server is shared with the specs that follow: the test group keeps its
// articles and their votes; its conclusion is reordered and its title changes.
const K1 = 'Règles abrégées de la toile de confiance'; const K1 = 'Règles abrégées de la toile de confiance';
const K2 = 'Monnaie G1'; const K2 = 'Monnaie G1';
/** A vote unfolded in the list of proposals: « Pour », then the news. */
async function voteFor(page: Page, row: string) {
await page.locator('ul.deep button.row', { hasText: row }).click();
await page.locator('ul.deep section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
}
/** Whether a version of a module of the test group is in force now. */ /** Whether a version of a module of the test group is in force now. */
async function inForce(page: Page, proposalId: string): Promise<boolean> { async function inForce(page: Page, proposalId: string): Promise<boolean> {
const g = await (await page.request.get('/api/groups/essai')).json(); const g = await (await page.request.get('/api/groups/essai')).json();
@@ -27,37 +23,39 @@ async function inForce(page: Page, proposalId: string): Promise<boolean> {
/** /**
* The voices that bring a module version of the test group into force: hers, then a * The voices that bring a module version of the test group into force: hers, then a
* second one if hers was not enough. Every dev sign-in joins the test group: from 16 * second one if hers was not enough. Every dev sign-in joins the test group: from 16
* members B^W vanishes and, C being 1, one voice may suffice (ADR 0007). Returns the * members B^W vanishes and, the quorum being 1, one voice may suffice (ADR 0013). Returns
* number of voices for it. * the number of voices for it. The second one votes at the module, by its address.
*/ */
async function secondVoice(page: Page, browser: Browser, proposalId: string, row: string, news: string): Promise<number> { async function secondVoice(page: Page, browser: Browser, proposalId: string, fold: string, news: string): Promise<number> {
if (await inForce(page, proposalId)) { if (await inForce(page, proposalId)) {
await expect(page.getByText(news)).toBeVisible(); await expect(page.getByText(news)).toBeVisible();
return 1; return 1;
} }
const yvv = await person(browser, 'Yvv'); const yvv = await person(browser, 'Yvv');
await yvv.goto(TEST_DOC); await yvv.goto(`${TEST_DOC}?vote=${proposalId}`);
await voteFor(yvv, row); await yvv.getByRole('region', { name: fold }).getByRole('button', { name: 'Pour', exact: true }).click();
await expect(yvv.getByText(news)).toBeVisible(); await expect(yvv.getByText(news)).toBeVisible();
return 2; return 2;
} }
test('C25 — l’ordre des articles : en fin de section, proposé en cliquant, au clavier, voté, en vigueur', async ({ page, browser }) => { test('C25 — l’ordre des articles : une ligne en fin de section, réordonnée sur place, votée là, en vigueur', async ({ page, browser }) => {
await signIn(page, 'Chiara'); await signIn(page, 'Chiara');
await page.goto(TEST_DOC); await page.goto(TEST_DOC);
// Each section ends with its order; a section of one article has nothing to decide. await unfold(page);
const modules = page.locator('section.text [role="group"]'); // Each section ends with its order, in one line; a section of one article has nothing to decide.
const modules = page.getByRole('group', { name: /^Ordre des articles · / });
await expect(modules).toHaveCount(7); await expect(modules).toHaveCount(7);
await expect(page.getByRole('group', { name: 'Ordre des articles · Ordonnancement' })).toHaveCount(0); await expect(page.getByRole('group', { name: 'Ordre des articles · Ordonnancement' })).toHaveCount(0);
const order = page.getByRole('group', { name: 'Ordre des articles · Conclusion' }); const order = page.getByRole('group', { name: 'Ordre des articles · Conclusion' });
await expect(order.locator(':scope > ol li')).toHaveText([`1 ${K1}`, `2 ${K2}`]); await expect(order).toContainText('Ordre de la section');
await expect(order).toContainText('2 articles · inertie 2 / 5 · basse'); await expect(order).toContainText('inertie 2 / 5 · basse');
await expect(order.getByText('seed a minima · 0 vote')).toBeVisible(); await expect(order.getByText('seed a minima · 0 vote')).toBeVisible();
// Never the list of its articles again: it is read in its vote, or while reordering.
await expect(order.getByText(K1)).toHaveCount(0);
await order.scrollIntoViewIfNeeded(); await order.scrollIntoViewIfNeeded();
await shot(page, 'c25-1-ordre-lecture'); await shot(page, 'c25-1-ordre-lecture');
// Its vote unfolds under it, like an article's, the list whole (the mark's real number of // Its vote unfolds under its line, like an article's, the list whole.
// votes is seen in Monnaie libre, below: here one voice may confirm a seed).
const fond = page.getByRole('group', { name: 'Ordre des articles · Engagements fondamentaux' }); const fond = page.getByRole('group', { name: 'Ordre des articles · Engagements fondamentaux' });
await fond.hover(); await fond.hover();
await fond.getByRole('button', { name: 'Le vote' }).click(); await fond.getByRole('button', { name: 'Le vote' }).click();
@@ -66,22 +64,21 @@ test('C25 — l’ordre des articles : en fin de section, proposé en cliquant,
await expect(fondVote.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText(['Engagements fondamentaux', 'Unicité du compte', 'Réciprocité']); await expect(fondVote.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText(['Engagements fondamentaux', 'Unicité du compte', 'Réciprocité']);
await fond.getByRole('button', { name: 'Le vote' }).click(); await fond.getByRole('button', { name: 'Le vote' }).click();
// Another order: the article, then its new place. // Another order: « Réordonner », the article, then its new place — in place, no panel.
await order.hover(); await order.hover();
await order.getByRole('button', { name: 'Proposer un autre ordre' }).click(); await order.getByRole('button', { name: 'Réordonner' }).click();
await expect(page).toHaveURL(/onglet=proposer/); await expect(page.locator('aside.panel')).toHaveCount(0);
const panel = page.locator('aside.panel'); const list = order.getByRole('list', { name: 'Nouvel ordre des articles' });
const list = panel.getByRole('list', { name: 'Nouvel ordre des articles' });
const items = list.locator('button.item'); const items = list.locator('button.item');
const names = list.locator('button.item .name'); const names = list.locator('button.item .name');
await expect(names).toHaveText([K1, K2]); await expect(names).toHaveText([K1, K2]);
const submit = panel.getByRole('button', { name: 'Proposer cet ordre' }); const submit = order.getByRole('button', { name: 'Proposer cet ordre' });
await expect(submit).toBeDisabled(); await expect(submit).toBeDisabled();
await items.nth(1).click(); await items.nth(1).click();
await expect(items.nth(1)).toHaveAttribute('aria-pressed', 'true'); await expect(items.nth(1)).toHaveAttribute('aria-pressed', 'true');
await items.nth(0).click(); await items.nth(0).click();
await expect(names).toHaveText([K2, K1]); await expect(names).toHaveText([K2, K1]);
await expect(panel.getByText(`« ${K2} » en place 1 sur 2.`)).toBeAttached(); await expect(order.getByText(`« ${K2} » en place 1 sur 2.`)).toBeAttached();
await expect(submit).toBeEnabled(); await expect(submit).toBeEnabled();
// With the keyboard: the moved article keeps the focus, the arrows move it. // With the keyboard: the moved article keeps the focus, the arrows move it.
await expect(items.nth(0)).toBeFocused(); await expect(items.nth(0)).toBeFocused();
@@ -91,38 +88,42 @@ test('C25 — l’ordre des articles : en fin de section, proposé en cliquant,
await page.keyboard.press('ArrowUp'); await page.keyboard.press('ArrowUp');
await expect(names).toHaveText([K2, K1]); await expect(names).toHaveText([K2, K1]);
// Or its arrows. // Or its arrows.
await panel.getByRole('button', { name: `Descendre « ${K2} »` }).click(); await order.getByRole('button', { name: `Descendre « ${K2} »` }).click();
await panel.getByRole('button', { name: `Monter « ${K2} »` }).click(); await order.getByRole('button', { name: `Monter « ${K2} »` }).click();
await expect(names).toHaveText([K2, K1]); await expect(names).toHaveText([K2, K1]);
await shot(page, 'c25-2-ordre-propose'); await shot(page, 'c25-2-ordre-propose');
await submit.click(); await submit.click();
await expect(page.getByText('Ordre proposé au vote.')).toBeVisible(); await expect(page.getByText('Ordre proposé au vote.')).toBeVisible();
// Its vote unfolds in the list of proposals, the list it proposes in sight. // Its vote unfolds at the line, the list it proposes in sight; never a line among the proposals.
const fold = page.locator('ul.deep section.fold'); const fold = order.locator('section.fold');
await expect(fold).toHaveAttribute('aria-label', 'Le vote de « Ordre des articles · Conclusion »'); await expect(fold).toHaveAttribute('aria-label', 'Le vote de « Ordre des articles · Conclusion »');
await expect(fold.locator('ol.order li')).toHaveText([K2, K1]); await expect(fold.getByRole('list', { name: 'Ordre proposé' }).locator('li')).toHaveText([K2, K1]);
await expect(page.locator('ul.deep li.section', { hasText: 'Conclusion' })).toBeVisible(); await expect(order).toContainText('1 ordre proposé');
await expect(page.locator('ul.deep button.row', { hasText: 'Ordre des articles · Conclusion' })).toContainText(`${K2} · ${K1}`); await expect(page.locator('ul.deep button.row', { hasText: 'Ordre des articles' })).toHaveCount(0);
// The order in force is read from it, among its other versions.
await expect(fold.getByRole('region', { name: 'Autres versions' })).toContainText('Ordre en vigueur');
const pid = ((await fold.getAttribute('id')) ?? '').replace(/^vote-/, ''); const pid = ((await fold.getAttribute('id')) ?? '').replace(/^vote-/, '');
await fold.getByRole('button', { name: 'Pour', exact: true }).click(); await fold.getByRole('button', { name: 'Pour', exact: true }).click();
await expect(page.getByText('Vote enregistré.')).toBeVisible(); await expect(page.getByText('Vote enregistré.')).toBeVisible();
await shot(page, 'c25-3-ordre-vote'); await shot(page, 'c25-3-ordre-vote');
// Into force (her voice, or a second one): the section reads in its new order. // Into force (her voice, or a second one): the section reads in its new order.
const voices = await secondVoice(page, browser, pid, 'Ordre des articles · Conclusion', 'Le nouvel ordre de « Conclusion » est entré en vigueur.'); const voices = await secondVoice(page, browser, pid, 'Le vote de « Ordre des articles · Conclusion »', 'Le nouvel ordre de « Conclusion » est entré en vigueur.');
// In the basket too: at the end of its section. // In the basket too: at the end of its section.
await page.getByRole('button', { name: /^Voter en lot/ }).click(); await page.getByRole('button', { name: /^Voter en lot/ }).click();
const panel = page.locator('aside.panel');
const basket = panel.getByRole('region', { name: 'Conclusion' }); const basket = panel.getByRole('region', { name: 'Conclusion' });
await expect(basket.locator('li.slot').last()).toContainText('Ordre des articles · Conclusion'); await expect(basket.locator('li.slot').last()).toContainText('Ordre des articles · Conclusion');
await panel.getByRole('button', { name: 'Fermer le panneau' }).click(); await panel.getByRole('button', { name: 'Fermer le panneau' }).click();
await page.reload(); await page.reload();
await expect(order.locator(':scope > ol li')).toHaveText([`1 ${K2}`, `2 ${K1}`]); // In force now: its vote stays unfolded under its line, the list whole, in the new order.
// In force now: its vote stays unfolded, under the module, the list whole.
await expect(order.locator('section.fold')).toContainText(`${voices} pour · 0 contre`); await expect(order.locator('section.fold')).toContainText(`${voices} pour · 0 contre`);
await expect(order.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText([K2, K1]);
await order.scrollIntoViewIfNeeded(); await order.scrollIntoViewIfNeeded();
await shot(page, 'c25-3b-ordre-en-vigueur'); await shot(page, 'c25-3b-ordre-en-vigueur');
await expect(order).toContainText(/adopté le/); await expect(order).toContainText(/adopté le/);
await unfold(page);
const rows = await page.locator('ul.deep button.row .title').allTextContents(); const rows = await page.locator('ul.deep button.row .title').allTextContents();
expect(rows.indexOf(K2)).toBeLessThan(rows.indexOf(K1)); expect(rows.indexOf(K2)).toBeLessThan(rows.indexOf(K1));
}); });
@@ -131,12 +132,14 @@ test('C26 — le titre du document : proposé et voté en tête, ses traductions
await signIn(page, 'Chiara'); await signIn(page, 'Chiara');
await page.goto(TEST_DOC); await page.goto(TEST_DOC);
const head = page.locator('header.head'); const head = page.locator('header.head');
const bar = head.locator('.title-unit'); const bar = head.getByRole('group', { name: 'Titre du document' });
await expect(bar).toContainText('Titre du document · inertie 4 / 5 · haute · seed a minima · 0 vote'); await expect(bar).toContainText('Titre du document · seed a minima · 0 vote');
await expect(bar).toContainText('inertie 4 / 5 · haute');
await head.hover(); await head.hover();
await bar.getByRole('button', { name: 'Proposer un autre titre' }).click(); await bar.getByRole('button', { name: 'Proposer un autre titre' }).click();
await expect(page).toHaveURL(/onglet=proposer/); await expect(page).toHaveURL(/onglet=proposer/);
const panel = page.locator('aside.panel'); const panel = page.locator('aside.panel');
await expect(panel.getByRole('heading', { name: 'Proposer un autre titre' })).toBeVisible();
const input = panel.getByRole('textbox', { name: 'Nouveau titre' }); const input = panel.getByRole('textbox', { name: 'Nouveau titre' });
await expect(input).toHaveValue("Acte d'engagement Certification"); await expect(input).toHaveValue("Acte d'engagement Certification");
const submit = panel.getByRole('button', { name: 'Proposer un autre titre' }); const submit = panel.getByRole('button', { name: 'Proposer un autre titre' });
@@ -147,13 +150,15 @@ test('C26 — le titre du document : proposé et voté en tête, ses traductions
await shot(page, 'c26-1-titre-propose'); await shot(page, 'c26-1-titre-propose');
await submit.click(); await submit.click();
await expect(page.getByText('Titre proposé au vote.')).toBeVisible(); await expect(page.getByText('Titre proposé au vote.')).toBeVisible();
const fold = page.locator('ul.deep section.fold'); // Its vote unfolds under the title, where it is read; never a line among the proposals.
const fold = bar.locator('section.fold');
await expect(fold).toHaveAttribute('aria-label', `Le vote de « ${next} »`); await expect(fold).toHaveAttribute('aria-label', `Le vote de « ${next} »`);
await expect(page.locator('ul.deep button.row', { hasText: next })).toContainText('Titre du document'); await expect(bar).toContainText('1 titre proposé');
await expect(page.locator('ul.deep').getByText(next)).toHaveCount(0);
const pid = ((await fold.getAttribute('id')) ?? '').replace(/^vote-/, ''); const pid = ((await fold.getAttribute('id')) ?? '').replace(/^vote-/, '');
await fold.getByRole('button', { name: 'Pour', exact: true }).click(); await fold.getByRole('button', { name: 'Pour', exact: true }).click();
await expect(page.getByText('Vote enregistré.')).toBeVisible(); await expect(page.getByText('Vote enregistré.')).toBeVisible();
const voices = await secondVoice(page, browser, pid, next, `Le titre « ${next} » est entré en vigueur.`); const voices = await secondVoice(page, browser, pid, `Le vote de « ${next} »`, `Le titre « ${next} » est entré en vigueur.`);
// The document changes title; its translations stay editorial, to review. // The document changes title; its translations stay editorial, to review.
await page.reload(); await page.reload();
@@ -194,7 +199,7 @@ test('C27 — la participation ramenée dans ses bornes : la mesure, puis ce que
await shot(page, 'c27-participation-bornee'); await shot(page, 'c27-participation-bornee');
}); });
test('C25 — sur la chaîne Ğ1 et sur un téléphone : l’ordre se vote dans le panier, la liste tient', async ({ browser }) => { test('C25 — sur la chaîne Ğ1 et sur un téléphone : l’ordre se vote dans le panier, puis se lit à sa ligne', async ({ browser }) => {
// The chain is waited for: the server reads the indexer every 10 seconds. // The chain is waited for: the server reads the indexer every 10 seconds.
test.setTimeout(90_000); test.setTimeout(90_000);
const ctx = await browser.newContext({ ...devices['Pixel 7'], locale: 'fr-FR' }); const ctx = await browser.newContext({ ...devices['Pixel 7'], locale: 'fr-FR' });
@@ -219,23 +224,10 @@ test('C25 — sur la chaîne Ğ1 et sur un téléphone : l’ordre se vote dans
); );
await fond.scrollIntoViewIfNeeded(); await fond.scrollIntoViewIfNeeded();
await shot(page, 'c25-4-ordre-mobile'); await shot(page, 'c25-4-ordre-mobile');
// The title on a phone: its gestures in sight, without a pointer to hover with. // The title on a phone: its gestures in sight, without a pointer to hover with. Reordering on a
// phone: C35.
await page.goto(TEST_DOC); await page.goto(TEST_DOC);
await expect(page.locator('.title-unit .links')).toHaveCSS('opacity', '1'); await expect(page.locator('.title-unit .links')).toHaveCSS('opacity', '1');
await shot(page, 'c26-4-titre-mobile'); await shot(page, 'c26-4-titre-mobile');
// The list one clicks, on a phone: nothing overflows, the buttons are big enough to touch.
const tech = page.getByRole('group', { name: 'Ordre des articles · Engagements techniques' });
await tech.getByRole('button', { name: 'Proposer un autre ordre' }).click();
const items = panel.getByRole('list', { name: 'Nouvel ordre des articles' }).locator('button.item');
await expect(items).toHaveCount(11);
await items.nth(3).click();
await items.nth(0).click();
await expect(panel.getByRole('button', { name: 'Proposer cet ordre' })).toBeEnabled();
const holds = await panel.locator('.body').evaluate((el) => el.scrollWidth <= el.clientWidth + 1);
expect(holds).toBe(true);
const box = await items.nth(0).boundingBox();
expect(box?.height ?? 0).toBeGreaterThanOrEqual(44);
await shot(page, 'c25-5-ordre-propose-mobile');
await page.getByRole('heading', { level: 1 }).scrollIntoViewIfNeeded();
await ctx.close(); await ctx.close();
}); });
+2 -1
View File
@@ -28,7 +28,8 @@ async function openBox(page: Page, unread: number) {
async function details(page: Page, id: string) { async function details(page: Page, id: string) {
await page.goto(`${TEST_DOC}?onglet=details&version=${id}`); await page.goto(`${TEST_DOC}?onglet=details&version=${id}`);
const panel = page.locator('aside.panel'); const panel = page.locator('aside.panel');
await expect(panel.getByRole('tab', { name: 'Détails' })).toHaveAttribute('aria-selected', 'true'); // One thing at a time in the panel (lot U), named at its head.
await expect(panel.getByRole('heading', { name: 'Détails', exact: true })).toBeVisible();
return panel; return panel;
} }
+32 -18
View File
@@ -1,5 +1,5 @@
import { expect, test, type Page } from '@playwright/test'; import { expect, test, type Page } from '@playwright/test';
import { BOB, deepRow, DOC, expectWelcome, person, seedsInForce, signIn, TEST_DOC, testDoc, wallet } from './helpers'; import { BOB, DOC, expectWelcome, person, seedsInForce, signIn, TEST_DOC, testDoc, unfold, wallet } from './helpers';
// The seed proposals (ADR 0011) are not in force at the start: the controls that read a // The seed proposals (ADR 0011) are not in force at the start: the controls that read a
// text in force find I1, E1 and E2 brought in by the vote in the test group. // text in force find I1, E1 and E2 brought in by the vote in the test group.
@@ -37,22 +37,29 @@ test('C2 — l’accueil avec session : le choix du groupe, ceux de la même cl
await expect(page).toHaveURL((u) => u.pathname === '/'); await expect(page).toHaveURL((u) => u.pathname === '/');
}); });
test('C3 — au départ, aucun texte en vigueur : la raison d’être et les deux engagements sont des Propositions seed', async ({ page }) => { test('C3 — au départ, aucun texte en vigueur : la raison d’être et les deux engagements, Propositions seed, ouvrent le texte', async ({ page }) => {
await signIn(page, 'Chiara'); await signIn(page, 'Chiara');
await page.goto(DOC); await page.goto(DOC);
await expect(page.getByRole('heading', { level: 1 })).toHaveText("Acte d'engagement Certification"); await expect(page.getByRole('heading', { level: 1 })).toHaveText("Acte d'engagement Certification");
// Nothing in force (Yvv, 2026-11-02): said once, above the orders of the sections. // Nothing in force (Yvv, 2026-11-02): the page says where to begin, once — the seed proposals,
await expect(page.locator('section.text article.unit')).toHaveCount(0); // read whole at the head of the text (lot U), each marked: never the bare page of a text in force.
await expect(page.getByText("Aucun article n'est en vigueur. Les propositions ci-dessous remontent dès qu'elles atteignent leur seuil.")).toBeVisible(); await expect(page.getByText("Aucun article n'est encore en vigueur. Commence par les Propositions seed : elles y entrent par le vote, comme toute proposition.")).toBeVisible();
await expect(page.getByRole('heading', { name: 'Propositions en cours' })).toBeVisible(); const seeds = page.locator('section.text article.unit');
// One line per article: the 33, E3 and E7 holding their two variants inside. await expect(seeds.locator('h2')).toHaveText(["Raison d'être", 'Unicité du compte', 'Réciprocité']);
await expect(page.locator('ul.deep button.row')).toHaveCount(33); await expect(seeds.locator('.tag', { hasText: 'Proposition seed' })).toHaveCount(3);
// The three seed proposals, marked, at the low level they start at.
const seeds = page.locator('ul.deep button.row', { has: page.locator('.tag', { hasText: 'Proposition seed' }) });
await expect(seeds.locator('.title')).toHaveText(["Raison d'être", 'Unicité du compte', 'Réciprocité']);
await expect(seeds.getByTitle("Texte de départ mis en avant pour lancer le vote. Il n'est pas en vigueur : il y entre par le vote, comme toute proposition.")).toHaveCount(3); await expect(seeds.getByTitle("Texte de départ mis en avant pour lancer le vote. Il n'est pas en vigueur : il y entre par le vote, comme toute proposition.")).toHaveCount(3);
for (const row of await seeds.all()) await expect(row).toContainText('0 pour · 0 contre'); // At the low level they start at, how far each is from passing.
for (const row of await seeds.all()) await expect(row).toContainText('inertie 2 / 5 · basse'); for (const seed of await seeds.all()) await expect(seed).toContainText('0 pour · 0 contre · 9 oui sur 12');
for (const seed of await seeds.all()) await expect(seed).toContainText('inertie 2 / 5 · basse');
// Below, the open proposals, folded by section; the seed proposals are not said twice.
await expect(page.getByRole('heading', { name: 'Propositions en cours' })).toBeVisible();
await expect(page.locator('ul.deep li.section')).toHaveCount(8);
await expect(page.locator('ul.deep button.row')).toHaveCount(0);
await expect(page.locator('ul.deep li.section', { hasText: 'Engagements techniques' })).toContainText('11 articles');
await unfold(page);
// One line per article: the 33 less the 3 at the head, E3 and E7 holding their two variants inside.
await expect(page.locator('ul.deep button.row')).toHaveCount(30);
await expect(page.locator('ul.deep .tag', { hasText: 'Proposition seed' })).toHaveCount(0);
}); });
test('C4 — les actions sont contextuelles, le vote se déplie sous l’article', async ({ page }) => { test('C4 — les actions sont contextuelles, le vote se déplie sous l’article', async ({ page }) => {
@@ -66,7 +73,8 @@ test('C4 — les actions sont contextuelles, le vote se déplie sous l’article
await toggle.click(); await toggle.click();
await expect(toggle).toHaveAttribute('aria-expanded', 'true'); await expect(toggle).toHaveAttribute('aria-expanded', 'true');
const fold = first.locator('section.fold'); const fold = first.locator('section.fold');
await expect(fold.getByText(/^Seuil : \d+ voix pour$/)).toBeVisible(); // How many « oui » pass it, at its quorum (ADR 0013): in force at no vote, the quorum of the test group.
await expect(fold.getByText(/^\d+ oui sur \d+ votants? pour passer$/)).toBeVisible();
await expect(fold.getByText(/^Il manque \d+ voix pour\.$/)).toBeVisible(); await expect(fold.getByText(/^Il manque \d+ voix pour\.$/)).toBeVisible();
await fold.getByText('La formule du seuil').click(); await fold.getByText('La formule du seuil').click();
await expect(fold.getByText(/W — électorat/)).toBeVisible(); await expect(fold.getByText(/W — électorat/)).toBeVisible();
@@ -93,6 +101,7 @@ test('C5 — une proposition remonte en vigueur quand elle atteint son seuil (gr
for (const name of ['Yvv', 'Chiara']) { for (const name of ['Yvv', 'Chiara']) {
last = await person(browser, name); last = await person(browser, name);
await last.goto(TEST_DOC); await last.goto(TEST_DOC);
await unfold(last);
await last.locator('ul.deep button.row', { hasText: title }).click(); await last.locator('ul.deep button.row', { hasText: title }).click();
await last.locator('section.fold').getByRole('button', { name: 'Pour', exact: true }).click(); await last.locator('section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
await expect(last.getByText('Vote enregistré.')).toBeVisible(); await expect(last.getByText('Vote enregistré.')).toBeVisible();
@@ -118,7 +127,7 @@ test('C6 — une coquille se corrige sans vote', async ({ page }) => {
// What is not the vote lives in the panel, tab « Détails ». // What is not the vote lives in the panel, tab « Détails ».
await unicity.getByRole('button', { name: 'Coquilles, traductions, objections, historique' }).click(); await unicity.getByRole('button', { name: 'Coquilles, traductions, objections, historique' }).click();
const panel = page.locator('aside.panel'); const panel = page.locator('aside.panel');
await expect(panel.getByRole('tab', { name: 'Détails' })).toHaveAttribute('aria-selected', 'true'); await expect(panel.getByRole('heading', { name: 'Détails', exact: true })).toBeVisible();
await panel.getByText('Corriger une coquille').click(); await panel.getByText('Corriger une coquille').click();
const body = panel.getByLabel('Texte', { exact: true }); const body = panel.getByLabel('Texte', { exact: true });
// Typographic apostrophes: the letters and the signs stay, only their style changes. // Typographic apostrophes: the letters and the signs stay, only their style changes.
@@ -195,6 +204,7 @@ test('C11 — une personne, une voix : dans Monnaie libre, seuls les membres de
await invited.goto(`/invitation#${link.split('#')[1]}`); await invited.goto(`/invitation#${link.split('#')[1]}`);
await expect(invited).toHaveURL((u) => u.pathname === '/'); await expect(invited).toHaveURL((u) => u.pathname === '/');
await invited.goto(DOC); await invited.goto(DOC);
await unfold(invited);
await invited.locator('ul.deep button.row', { hasText: 'Vérification personnelle de la clé' }).click(); await invited.locator('ul.deep button.row', { hasText: 'Vérification personnelle de la clé' }).click();
const fold = invited.locator('section.fold'); const fold = invited.locator('section.fold');
// An invitation gives no voice: the voice is that of the Ğ1 identity of the wallet that signs. // An invitation gives no voice: the voice is that of the Ğ1 identity of the wallet that signs.
@@ -209,6 +219,7 @@ test('C11 — une personne, une voix : dans Monnaie libre, seuls les membres de
await expect(fold.getByText('0 pour · 0 contre')).toBeVisible(); await expect(fold.getByText('0 pour · 0 contre')).toBeVisible();
// In the test group, the same person votes, directly. // In the test group, the same person votes, directly.
await invited.goto(TEST_DOC); await invited.goto(TEST_DOC);
await unfold(invited);
await invited.locator('ul.deep button.row', { hasText: 'Vérification personnelle de la clé' }).click(); await invited.locator('ul.deep button.row', { hasText: 'Vérification personnelle de la clé' }).click();
await expect(invited.locator('section.fold').getByRole('button', { name: 'Pour', exact: true })).toBeVisible(); await expect(invited.locator('section.fold').getByRole('button', { name: 'Pour', exact: true })).toBeVisible();
await expect(invited.locator('section.fold').getByText('Ta voix compte', { exact: false })).toHaveCount(0); await expect(invited.locator('section.fold').getByText('Ta voix compte', { exact: false })).toHaveCount(0);
@@ -268,6 +279,7 @@ test('C18 — la Raison d’être ouvre le texte, en trois langues', async ({ pa
test('C19 — un article, une ligne : ses versions se déplient', async ({ page }) => { test('C19 — un article, une ligne : ses versions se déplient', async ({ page }) => {
await signIn(page, 'Chiara'); await signIn(page, 'Chiara');
await page.goto(DOC); await page.goto(DOC);
await unfold(page);
const e3 = page.locator('ul.deep button.row', { hasText: 'Connaissance suffisante' }); const e3 = page.locator('ul.deep button.row', { hasText: 'Connaissance suffisante' });
await expect(e3).toHaveCount(1); await expect(e3).toHaveCount(1);
await expect(e3).toContainText('2 versions'); await expect(e3).toContainText('2 versions');
@@ -284,7 +296,7 @@ test('C19 — un article, une ligne : ses versions se déplient', async ({ page
await versions.nth(1).click(); await versions.nth(1).click();
await expect(versions.nth(1)).toHaveAttribute('aria-expanded', 'true'); await expect(versions.nth(1)).toHaveAttribute('aria-expanded', 'true');
// Level 3, the quorum 50 (ADR 0013): 39 « pour » at no vote, the voters missing counted as « non ». // Level 3, the quorum 50 (ADR 0013): 39 « pour » at no vote, the voters missing counted as « non ».
await expect(page.locator('section.fold').getByText('Seuil : 39 voix pour')).toBeVisible(); await expect(page.locator('section.fold').getByText('39 oui sur 50 votants pour passer', { exact: true })).toBeVisible();
await expect(page).toHaveURL(/vote=/); await expect(page).toHaveURL(/vote=/);
await e3.click(); await e3.click();
await expect(e3).toHaveAttribute('aria-expanded', 'false'); await expect(e3).toHaveAttribute('aria-expanded', 'false');
@@ -318,8 +330,10 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => {
await expect(panel.getByText(/^Quorum : en dessous, les votants qui manquent comptent comme « non »\./)).toBeVisible(); await expect(panel.getByText(/^Quorum : en dessous, les votants qui manquent comptent comme « non »\./)).toBeVisible();
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
// From the vote of an article: Unicité du compte, a seed proposal, is at 2 / 5, its row is marked. // From the vote of an article: Unicité du compte, a seed proposal at the head, is at 2 / 5, its row is marked.
await deepRow(page, 'Unicité du compte').click(); const unicity = page.locator('section.text article.unit', { hasText: 'Unicité du compte' });
await unicity.hover();
await unicity.getByRole('button', { name: 'Le vote' }).click();
const e1 = page.getByRole('region', { name: 'Le vote de « Unicité du compte »' }); const e1 = page.getByRole('region', { name: 'Le vote de « Unicité du compte »' });
await expect(e1.getByRole('heading', { name: 'Inertie · 2 / 5 · basse' })).toBeVisible(); await expect(e1.getByRole('heading', { name: 'Inertie · 2 / 5 · basse' })).toBeVisible();
await e1.getByRole('button', { name: 'Réglages de la formule' }).click(); await e1.getByRole('button', { name: 'Réglages de la formule' }).click();
+28 -14
View File
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { ALICE, chain, CHARLIE, deepRow, DOC, person, seedsInForce, shot, signed, signIn, TEST_DOC, VOTE_ACCOUNT, wallet } from './helpers'; import { ALICE, chain, CHARLIE, deepRow, DOC, person, seedsInForce, shot, signed, signIn, TEST_DOC, unfold, VOTE_ACCOUNT, wallet } from './helpers';
// Lot B — the vote (Yvv, 2026-10-30): unfolded under the article, grouped in a // Lot B — the vote (Yvv, 2026-10-30): unfolded under the article, grouped in a
// basket, and on the Ğ1 chain where the electorate is the web of trust. The chain // basket, and on the Ğ1 chain where the electorate is the web of trust. The chain
@@ -21,7 +21,7 @@ test('C21 — le vote déplié sous l’article : ton vote, les totaux, le seuil
await expect(toggle).toHaveAttribute('aria-controls', (await fold.getAttribute('id')) ?? ''); await expect(toggle).toHaveAttribute('aria-controls', (await fold.getAttribute('id')) ?? '');
// How far the threshold is: the water, the totals, the votes asked and those missing. // How far the threshold is: the water, the totals, the votes asked and those missing.
await expect(fold.getByRole('img', { name: /Niveau : \d+ % du seuil/ })).toBeVisible(); await expect(fold.getByRole('img', { name: /Niveau : \d+ % du seuil/ })).toBeVisible();
await expect(fold.getByText(/^Seuil : \d+ voix pour$/)).toBeVisible(); await expect(fold.getByText(/^\d+ oui sur \d+ votants? pour passer$/)).toBeVisible();
await expect(fold.getByText(/^Il manque \d+ voix pour\.$/)).toBeVisible(); await expect(fold.getByText(/^Il manque \d+ voix pour\.$/)).toBeVisible();
await expect(fold.getByText("Tu n'as pas voté sur cette version.")).toBeVisible(); await expect(fold.getByText("Tu n'as pas voté sur cette version.")).toBeVisible();
await expect(fold.getByText('0 pour · 0 contre')).toBeVisible(); await expect(fold.getByText('0 pour · 0 contre')).toBeVisible();
@@ -119,8 +119,9 @@ test('C23 — le vote par la chaîne Ğ1 : un geste, confirmé par l’indexeur
await expect(panel.getByText("Rien dans le panier pour l'instant.")).toBeVisible(); await expect(panel.getByText("Rien dans le panier pour l'instant.")).toBeVisible();
await panel.getByRole('button', { name: 'Fermer le panneau' }).click(); await panel.getByRole('button', { name: 'Fermer le panneau' }).click();
// Under the seed proposal: her vote, from the chain, and the totals. // Under the seed proposal, at the head of the text: her vote, from the chain, and the totals.
const e1 = deepRow(page, 'Unicité du compte'); const seed = (title: string) => page.locator('section.text article.unit', { hasText: title }).getByRole('button', { name: 'Le vote' });
const e1 = seed('Unicité du compte');
await e1.click(); await e1.click();
const fold1 = page.getByRole('region', { name: 'Le vote de « Unicité du compte »' }); const fold1 = page.getByRole('region', { name: 'Le vote de « Unicité du compte »' });
await expect(fold1.locator('.voted')).toHaveText('Ton vote : pour · inscrit sur la chaîne Ğ1'); await expect(fold1.locator('.voted')).toHaveText('Ton vote : pour · inscrit sur la chaîne Ğ1');
@@ -128,7 +129,7 @@ test('C23 — le vote par la chaîne Ğ1 : un geste, confirmé par l’indexeur
// The last one stands, in the order of the chain: a « contre » included first, but seen late // The last one stands, in the order of the chain: a « contre » included first, but seen late
// by the indexer, never overrides the withdrawal included after it. // by the indexer, never overrides the withdrawal included after it.
const e2 = deepRow(page, 'Réciprocité'); const e2 = seed('Réciprocité');
await e2.click(); await e2.click();
const fold2 = page.getByRole('region', { name: 'Le vote de « Réciprocité »' }); const fold2 = page.getByRole('region', { name: 'Le vote de « Réciprocité »' });
await control('hide'); await control('hide');
@@ -155,8 +156,8 @@ test('C23 — le vote par la chaîne Ğ1 : un geste, confirmé par l’indexeur
// Without a wallet in the browser: Ğecko, a link and a QR code; the page waits for the chain. // Without a wallet in the browser: Ğecko, a link and a QR code; the page waits for the chain.
const other = await person(browser, 'Maëlle'); const other = await person(browser, 'Maëlle');
await other.goto(DOC); await other.goto(DOC);
await deepRow(other, "Raison d'être").click(); await other.locator('section.text article.unit', { hasText: "Raison d'être" }).getByRole('button', { name: 'Le vote' }).click();
await other.locator('ul.deep section.fold').getByRole('button', { name: 'Pour', exact: true }).click(); await other.locator('section.text section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
const sheet = other.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' }); const sheet = other.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' });
await expect(sheet.getByText("Aucun portefeuille n'est branché à ce navigateur", { exact: false })).toBeVisible(); await expect(sheet.getByText("Aucun portefeuille n'est branché à ce navigateur", { exact: false })).toBeVisible();
await sheet.getByRole('button', { name: 'Passer par Ğecko' }).click(); await sheet.getByRole('button', { name: 'Passer par Ğecko' }).click();
@@ -191,13 +192,12 @@ test('C24 — sans compte de vote, le vote par la chaîne est fermé et le dit ;
await page.goto(DOC); await page.goto(DOC);
await expect(page.getByRole('button', { name: /^Voter en lot/ })).toHaveCount(0); await expect(page.getByRole('button', { name: /^Voter en lot/ })).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Voter cette section' })).toHaveCount(0); await expect(page.getByRole('button', { name: 'Voter cette section' })).toHaveCount(0);
await page.locator('ul.deep button.row').first().click(); await page.locator('section.text article.unit').first().getByRole('button', { name: 'Le vote' }).click();
const fold = page.locator('ul.deep section.fold'); const fold = page.locator('section.text section.fold');
await expect(fold.getByText("Le vote par la chaîne Ğ1 n'est pas encore ouvert : le compte qui reçoit les votes reste à créer.", { exact: false })).toBeVisible(); await expect(fold.getByText("Le vote par la chaîne Ğ1 n'est pas encore ouvert : le compte qui reçoit les votes reste à créer.", { exact: false })).toBeVisible();
await expect(fold.getByRole('button', { name: 'Pour', exact: true })).toHaveCount(0); await expect(fold.getByRole('button', { name: 'Pour', exact: true })).toHaveCount(0);
// The totals and the threshold stay in sight. // The totals and how far it is stay in sight: Raison d'être, level 2, the quorum 12 (ADR 0013).
// Raison d'être, level 2, the quorum 12 (ADR 0013). await expect(fold.getByText('9 oui sur 12 votants pour passer', { exact: true })).toBeVisible();
await expect(fold.getByText('Seuil : 9 voix pour')).toBeVisible();
await fold.getByText('Ton vote ne se montre à personne', { exact: false }).scrollIntoViewIfNeeded(); await fold.getByText('Ton vote ne se montre à personne', { exact: false }).scrollIntoViewIfNeeded();
await shot(page, '6-vote-ferme'); await shot(page, '6-vote-ferme');
// The basket, opened by its address, says the vote is closed — never « Tes votes comptent dès l'envoi ». // The basket, opened by its address, says the vote is closed — never « Tes votes comptent dès l'envoi ».
@@ -239,11 +239,16 @@ test('C34 — la chaîne fermée, Monnaie libre vote quand même : le vote simul
const before = await version(page, title); const before = await version(page, title);
expect(before.vote).toMatchObject({ way: 'server', open: true, simulated: true }); expect(before.vote).toMatchObject({ way: 'server', open: true, simulated: true });
await page.goto(DOC); await page.goto(DOC);
// The basket is there, without a wallet: the vote leaves by the server. // The basket is there, without a wallet: the vote leaves by the server, marked « vote simulé ».
await expect(page.getByRole('button', { name: /^Voter en lot/ })).toBeVisible(); await expect(page.getByRole('button', { name: /^Voter en lot/ })).toBeVisible();
await expect(page.locator('header.head .tag', { hasText: 'vote simulé' })).toBeVisible();
await unfold(page);
await deepRow(page, title).click(); await deepRow(page, title).click();
const fold = page.getByRole('region', { name: `Le vote de « ${title} »` }); const fold = page.getByRole('region', { name: `Le vote de « ${title} »` });
await fold.getByRole('button', { name: 'Contre', exact: true }).click(); await fold.getByRole('button', { name: 'Contre', exact: true }).click();
// Said simulated at once, never « enregistré » (review of M6).
await expect(page.getByText('Vote simulé.', { exact: true })).toBeVisible();
await expect(page.getByText('Vote enregistré.')).toHaveCount(0);
await expect(page.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' })).toHaveCount(0); await expect(page.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' })).toHaveCount(0);
await expect(fold.locator('.voted')).toHaveText(/^Ton vote : contre/); await expect(fold.locator('.voted')).toHaveText(/^Ton vote : contre/);
await expect(fold.getByText(`0 pour · ${before.p.tally.against + 1} contre`)).toBeVisible(); await expect(fold.getByText(`0 pour · ${before.p.tally.against + 1} contre`)).toBeVisible();
@@ -257,10 +262,19 @@ test('C34 — la chaîne fermée, Monnaie libre vote quand même : le vote simul
const after = await version(page, title); const after = await version(page, title);
expect(after.p.myVoteState).toBe('simulated'); expect(after.p.myVoteState).toBe('simulated');
expect(after.p.tallySimulated.against).toBe(before.p.tallySimulated.against + 1); expect(after.p.tallySimulated.against).toBe(before.p.tallySimulated.against + 1);
// The basket too says « simulé », never « enregistrés » (review of M6): « contre » again, one line.
await page.getByRole('button', { name: /^Voter en lot/ }).click();
const basket = page.locator('aside.panel');
await basket.getByRole('group', { name: `Ton choix pour « ${title} »` }).getByRole('button', { name: 'Contre' }).click();
await basket.getByRole('button', { name: 'Voter (1)' }).click();
await expect(page.getByText('1 vote simulé.', { exact: true })).toBeVisible();
await expect(page.getByText('enregistré', { exact: false })).toHaveCount(0);
await basket.getByRole('button', { name: 'Fermer le panneau' }).click();
// In her ballots: simulated, counted for her session, never a block of the chain. // In her ballots: simulated, counted for her session, never a block of the chain.
await page.goto('/bulletins'); await page.goto('/bulletins');
const ballot = page.locator('li.ballot').first(); const ballot = page.locator('li.ballot').first();
await expect(ballot.locator('.state')).toHaveText("Simulé : compté pour « Sacha » jusqu'à l'ouverture de la chaîne Ğ1"); // Until a human act erases the simulated votes — never « until the chain opens » (review of M6).
await expect(ballot.locator('.state')).toHaveText('Simulé : compté pour « Sacha » tant que les votes simulés ne sont pas effacés');
await expect(ballot.getByText('bloc', { exact: false })).toHaveCount(0); await expect(ballot.getByText('bloc', { exact: false })).toHaveCount(0);
} finally { } finally {
// The next controls need the chain. // The next controls need the chain.
+6 -2
View File
@@ -250,7 +250,7 @@
.toggle { .toggle {
flex: 1 1 16rem; flex: 1 1 16rem;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: nowrap;
align-items: center; align-items: center;
gap: 0.25rem 0.7rem; gap: 0.25rem 0.7rem;
min-height: 3rem; min-height: 3rem;
@@ -271,14 +271,18 @@
color: var(--ink); color: var(--ink);
} }
.count { .count {
flex: none;
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 750; font-weight: 750;
color: var(--water); color: var(--water);
white-space: nowrap;
} }
.waters { .waters {
display: inline-flex; display: inline-flex;
flex-wrap: wrap;
align-items: flex-end; align-items: flex-end;
gap: 3px; gap: 3px;
min-width: 0;
} }
.head .chevron { .head .chevron {
display: inline-flex; display: inline-flex;
@@ -296,7 +300,7 @@
font-size: 0.84rem; font-size: 0.84rem;
padding: 0.3rem 0.7rem; padding: 0.3rem 0.7rem;
min-height: 2.25rem; min-height: 2.25rem;
margin-right: 0.6rem; margin: 0 0.6rem 0 auto;
border-radius: var(--r-pill); border-radius: var(--r-pill);
} }
.vote-section:hover { .vote-section:hover {
+16 -2
View File
@@ -46,7 +46,7 @@
<div class="bar"> <div class="bar">
<span class="mark" aria-hidden="true"><Icon name="grip" size={16} /></span> <span class="mark" aria-hidden="true"><Icon name="grip" size={16} /></span>
<span class="name">{t('order.line')}</span> <span class="name">{t('order.line')}</span>
<Crans level={unit.inertia} /> <Crans level={unit.inertia} short />
{#if p?.seedMinimal}<span class="faint" title={t('doc.seed.long')}>{i18n.plural('doc.seed.votes', votes)}</span> {#if p?.seedMinimal}<span class="faint" title={t('doc.seed.long')}>{i18n.plural('doc.seed.votes', votes)}</span>
{:else if p?.enteredAt}<span class="faint">{t('doc.adopted', { date: i18n.date(p.enteredAt) })}</span>{/if} {:else if p?.enteredAt}<span class="faint">{t('doc.adopted', { date: i18n.date(p.enteredAt) })}</span>{/if}
{#if proposed.length && !reordering} {#if proposed.length && !reordering}
@@ -112,11 +112,24 @@
min-height: 2rem; min-height: 2rem;
border-radius: var(--r-pill); border-radius: var(--r-pill);
} }
/* Discreet as under an article: the gestures show when the line is pointed at. */
.links { .links {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.2rem; gap: 0.2rem;
margin-left: auto; margin-left: auto;
opacity: 0;
transition: opacity 0.15s ease;
}
.order-line:hover .links,
.order-line:focus-within .links,
.order-line.unfolded .links {
opacity: 1;
}
@media (hover: none) {
.links {
opacity: 1;
}
} }
.links button { .links button {
border: 0; border: 0;
@@ -141,7 +154,8 @@
} }
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.order-line { .order-line,
.links {
transition: none; transition: none;
} }
} }
+12 -4
View File
@@ -175,8 +175,10 @@
</span> </span>
<button type="button" class="item" data-id={id} aria-pressed={chosen === id} disabled={busy} onclick={() => pick(id)} onkeydown={(e) => key(e, id)}> <button type="button" class="item" data-id={id} aria-pressed={chosen === id} disabled={busy} onclick={() => pick(id)} onkeydown={(e) => key(e, id)}>
<span class="n" aria-hidden="true">{i + 1}</span> <span class="n" aria-hidden="true">{i + 1}</span>
<span class="name">{name}</span> <span class="what">
{#if proposed(id)}<span class="tag">{t('order.edit.proposed')}</span>{/if} <span class="name">{name}</span>
{#if proposed(id)}<span class="tag">{t('order.edit.proposed')}</span>{/if}
</span>
</button> </button>
<span class="arrows"> <span class="arrows">
<button type="button" class="arrow up" aria-label={t('order.edit.up', { title: name })} disabled={i === 0 || busy} onclick={() => shift(id, -1)}><Icon name="chevron" size={16} /></button> <button type="button" class="arrow up" aria-label={t('order.edit.up', { title: name })} disabled={i === 0 || busy} onclick={() => shift(id, -1)}><Icon name="chevron" size={16} /></button>
@@ -252,9 +254,8 @@
.item { .item {
flex: 1; flex: 1;
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
gap: 0.3rem 0.6rem; gap: 0.6rem;
min-width: 0; min-width: 0;
min-height: 2.75rem; min-height: 2.75rem;
padding: 0.45rem 0.3rem; padding: 0.45rem 0.3rem;
@@ -276,6 +277,13 @@
.dragging .n { .dragging .n {
color: inherit; color: inherit;
} }
.what {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem 0.6rem;
min-width: 0;
}
.name { .name {
font-family: var(--font-text); font-family: var(--font-text);
font-weight: 600; font-weight: 600;
+6 -1
View File
@@ -9,6 +9,7 @@
import { i18n, t } from '$lib/i18n/index.svelte'; import { i18n, t } from '$lib/i18n/index.svelte';
import { needOf } from '$lib/labels'; import { needOf } from '$lib/labels';
import type { Proposal } from '$lib/types'; import type { Proposal } from '$lib/types';
import Crans from './Crans.svelte';
import Prose from './Prose.svelte'; import Prose from './Prose.svelte';
import WaterLevel from './WaterLevel.svelte'; import WaterLevel from './WaterLevel.svelte';
@@ -69,7 +70,8 @@
<span class="status faint"> <span class="status faint">
{#if seed} {#if seed}
{@const need = needOf(seed)} {@const need = needOf(seed)}
{t('tally.short', { for: seed.tally.for, against: seed.tally.against })}{#if need} · <span title={need.title}>{need.short}</span>{/if} {t('tally.short', { for: seed.tally.for, against: seed.tally.against })}{#if need}{' · '}<span title={need.title}>{need.short}</span>{/if}
<span class="crans"><Crans level={seed.votedAtInertia} short /></span>
{:else if seedMinimal}<span title={t('doc.seed.long')}>{i18n.plural('doc.seed.votes', votes)}</span> {:else if seedMinimal}<span title={t('doc.seed.long')}>{i18n.plural('doc.seed.votes', votes)}</span>
{:else if enteredAt}{t('doc.adopted', { date: i18n.date(enteredAt) })}{/if} {:else if enteredAt}{t('doc.adopted', { date: i18n.date(enteredAt) })}{/if}
{#if rivals > 0} · {i18n.plural('doc.others', rivals)}{/if} {#if rivals > 0} · {i18n.plural('doc.others', rivals)}{/if}
@@ -124,6 +126,9 @@
.replaced { .replaced {
background: var(--moss-wash); background: var(--moss-wash);
} }
.crans {
margin-left: 0.6rem;
}
.actions { .actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
+2 -2
View File
@@ -162,8 +162,8 @@ export const en: Messages = {
'deep.version': 'version {n}', 'deep.version': 'version {n}',
'deep.lead': 'leading: {label}', 'deep.lead': 'leading: {label}',
'deep.section.none': 'No section', 'deep.section.none': 'No section',
'deep.section.count.one': '{n} article proposed', 'deep.section.count.one': '{n} article',
'deep.section.count.other': '{n} articles proposed', 'deep.section.count.other': '{n} articles',
'deep.unfold': 'Unfold all', 'deep.unfold': 'Unfold all',
'deep.fold': 'Fold all', 'deep.fold': 'Fold all',
'section.introduction': 'Introduction', 'section.introduction': 'Introduction',
+2 -2
View File
@@ -162,8 +162,8 @@ export const es: Messages = {
'deep.version': 'versión {n}', 'deep.version': 'versión {n}',
'deep.lead': 'en cabeza: {label}', 'deep.lead': 'en cabeza: {label}',
'deep.section.none': 'Sin sección', 'deep.section.none': 'Sin sección',
'deep.section.count.one': '{n} artículo propuesto', 'deep.section.count.one': '{n} artículo',
'deep.section.count.other': '{n} artículos propuestos', 'deep.section.count.other': '{n} artículos',
'deep.unfold': 'Desplegar todo', 'deep.unfold': 'Desplegar todo',
'deep.fold': 'Plegar todo', 'deep.fold': 'Plegar todo',
'section.introduction': 'Introducción', 'section.introduction': 'Introducción',
+2 -2
View File
@@ -166,8 +166,8 @@ export const fr = {
'deep.version': 'version {n}', 'deep.version': 'version {n}',
'deep.lead': 'en tête : {label}', 'deep.lead': 'en tête : {label}',
'deep.section.none': 'Sans section', 'deep.section.none': 'Sans section',
'deep.section.count.one': '{n} article proposé', 'deep.section.count.one': '{n} article',
'deep.section.count.other': '{n} articles proposés', 'deep.section.count.other': '{n} articles',
'deep.unfold': 'Tout déplier', 'deep.unfold': 'Tout déplier',
'deep.fold': 'Tout replier', 'deep.fold': 'Tout replier',
'section.introduction': 'Introduction', 'section.introduction': 'Introduction',
@@ -228,26 +228,25 @@
· <button class="linkish" type="button" onclick={() => openPanel({ unitId: null, proposalId: null, tab: 'settings' })}>{t('settings.title')}</button> · <button class="linkish" type="button" onclick={() => openPanel({ unitId: null, proposalId: null, tab: 'settings' })}>{t('settings.title')}</button>
</p> </p>
{#if canVote} <!-- One line of tools — the basket, the language of the text —, then the readings. -->
<div class="basket-row"> <div class="tools">
{#if canVote}
<button class="btn small basket-btn" type="button" onclick={() => openPanel({ unitId: null, proposalId: null, tab: 'basket' })}> <button class="btn small basket-btn" type="button" onclick={() => openPanel({ unitId: null, proposalId: null, tab: 'basket' })}>
{basket.size ? t('doc.basket.count', { n: basket.size }) : t('doc.basket')} {basket.size ? t('doc.basket.count', { n: basket.size }) : t('doc.basket')}
</button> </button>
{#if view.vote.simulated}<span class="tag" title={t('vote.simulated.note')}>{t('vote.simulated.mark')}</span>{/if} {#if view.vote.simulated}<span class="tag" title={t('vote.simulated.note')}>{t('vote.simulated.mark')}</span>{/if}
</div> {/if}
{/if} <div class="seg small langs" role="radiogroup" aria-label={t('doc.lang.label')}>
<div class="controls">
<div class="seg views" role="radiogroup" aria-label={t('doc.view.label')}>
{#each VIEWS as v (v)}
<button type="button" role="radio" aria-checked={doc.mode === v} class:on={doc.mode === v} onclick={() => doc.setMode(v)}>{t(`doc.view.${v}`)}</button>
{/each}
</div>
<div class="seg small" role="radiogroup" aria-label={t('doc.lang.label')}>
{#each LANGS as l (l)} {#each LANGS as l (l)}
<button type="button" role="radio" aria-checked={lang === l} class:on={lang === l} onclick={() => { doc.original = l === source && i18n.lang !== source; if (l !== source) i18n.set(l); }}>{l.toUpperCase()}</button> <button type="button" role="radio" aria-checked={lang === l} class:on={lang === l} onclick={() => { doc.original = l === source && i18n.lang !== source; if (l !== source) i18n.set(l); }}>{l.toUpperCase()}</button>
{/each} {/each}
</div> </div>
</div> </div>
<div class="seg views" role="radiogroup" aria-label={t('doc.view.label')}>
{#each VIEWS as v (v)}
<button type="button" role="radio" aria-checked={doc.mode === v} class:on={doc.mode === v} onclick={() => doc.setMode(v)}>{t(`doc.view.${v}`)}</button>
{/each}
</div>
{#if lang !== source} {#if lang !== source}
<p class="note faint">{t('doc.lang.translation')} <button class="linkish" type="button" onclick={() => (doc.original = true)}>{t('doc.lang.original')}</button></p> <p class="note faint">{t('doc.lang.translation')} <button class="linkish" type="button" onclick={() => (doc.original = true)}>{t('doc.lang.original')}</button></p>
{/if} {/if}
@@ -402,22 +401,18 @@
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 700; font-weight: 700;
} }
.controls { .tools {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
justify-content: space-between;
margin-top: 1.4rem;
}
.basket-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
gap: 0.6rem; gap: 0.6rem;
margin-top: 1.2rem; margin-top: 1.2rem;
} }
.basket-row + .controls { .langs {
margin-top: 0.8rem; margin-left: auto;
}
.views {
margin-top: 0.7rem;
} }
.basket-btn { .basket-btn {
background: var(--water); background: var(--water);
@@ -471,7 +466,8 @@
margin-top: 2.2rem; margin-top: 2.2rem;
} }
.lead { .lead {
margin-bottom: 1.4rem; margin-bottom: 1.2rem;
font-size: 0.92rem;
font-weight: 650; font-weight: 650;
color: var(--ink-soft); color: var(--ink-soft);
} }