v3 : interface Svelte — accueil, groupes, lecteur du texte en vigueur, panneau contextuel, éditeur à assistant, trustinWallet
- Accueil sobre, menu complet à gauche (Décisions, Docs de référence, Mandats, Protocoles), intro demandée, quatre sections, connexion Ğ1. - Lecteur : texte en vigueur en Literata, actions contextuelles par article (le vote, propositions, proposer), propositions en cours sous la « surface » avec leur niveau d'eau, aperçus « si les bien cotées passent » / « si tout passe », langue du texte fr/en/es (traductions éditoriales). - Panneau : vote pour/contre, seuil et formule (MathML), inertie et son changement (voté en inertie +), coquille sans vote, traduction, signalement de contradiction, historique des corrections. - trustinWallet (/acces) : groupes et membres, invitations à usage unique, administrateurs, retours d'usage. - Contrôles : vitest (markdown, catalogues, anti-lexique), Playwright C1–C10 + M1. - Sections du seed portées jusqu'à l'écran. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
// The words of the screen: every catalogue has the same keys and the same
|
||||
// placeholders, and no visible text uses a forbidden term.
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { en } from '../../src/lib/i18n/en';
|
||||
import { es } from '../../src/lib/i18n/es';
|
||||
import { fr } from '../../src/lib/i18n/fr';
|
||||
|
||||
/** Never shown to a person (case- and accent-insensitive). They may live on as code identifiers. */
|
||||
export const FORBIDDEN_UI_TERMS = [
|
||||
'déléguer', 'délégation', 'délégué', 'dette', 'retard', 'en souffrance', 'corpus', 'snapshot',
|
||||
'verdict', 'triage', 'oligarchie', 'peuple adulte',
|
||||
];
|
||||
|
||||
const fold = (s: string) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
||||
const marks = (s: string) => (s.match(/\{\w+\}/g) ?? []).sort().join(',');
|
||||
|
||||
function svelteFiles(dir: string): string[] {
|
||||
return readdirSync(dir).flatMap((name) => {
|
||||
const p = join(dir, name);
|
||||
return statSync(p).isDirectory() ? svelteFiles(p) : p.endsWith('.svelte') ? [p] : [];
|
||||
});
|
||||
}
|
||||
|
||||
describe('catalogues', () => {
|
||||
it('en and es have exactly the keys of fr, in order', () => {
|
||||
expect(Object.keys(en)).toEqual(Object.keys(fr));
|
||||
expect(Object.keys(es)).toEqual(Object.keys(fr));
|
||||
});
|
||||
it('placeholders are kept', () => {
|
||||
for (const k of Object.keys(fr) as (keyof typeof fr)[]) {
|
||||
expect(marks(en[k]), `en ${k}`).toBe(marks(fr[k]));
|
||||
expect(marks(es[k]), `es ${k}`).toBe(marks(fr[k]));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('anti-lexicon', () => {
|
||||
it('no forbidden term in the French catalogue', () => {
|
||||
for (const [k, v] of Object.entries(fr)) {
|
||||
for (const term of FORBIDDEN_UI_TERMS) expect(fold(v).includes(fold(term)), `${k}: ${term}`).toBe(false);
|
||||
}
|
||||
});
|
||||
it('no forbidden term in template text of components', () => {
|
||||
for (const file of svelteFiles(join(__dirname, '../../src'))) {
|
||||
const markup = readFileSync(file, 'utf8')
|
||||
.replace(/<script[\s\S]*?<\/script>/g, '')
|
||||
.replace(/<style[\s\S]*?<\/style>/g, '')
|
||||
.replace(/\{[^}]*\}/g, ' ');
|
||||
const text = fold(markup.replace(/<[^>]+>/g, ' '));
|
||||
for (const term of FORBIDDEN_UI_TERMS) expect(text.includes(fold(term)), `${file}: ${term}`).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { plain, render } from '../../src/lib/markdown';
|
||||
|
||||
describe('markdown', () => {
|
||||
it('escapes html before anything', () => {
|
||||
expect(render('<script>alert(1)</script>')).toBe('<p><script>alert(1)</script></p>');
|
||||
expect(render('**<b>x</b>**')).toBe('<p><strong><b>x</b></strong></p>');
|
||||
});
|
||||
it('paragraphs, breaks, emphasis, code', () => {
|
||||
expect(render('un **être humain** vivant\nligne\n\n*note*')).toBe(
|
||||
'<p>un <strong>être humain</strong> vivant<br>ligne</p><p><em>note</em></p>',
|
||||
);
|
||||
expect(render('`DU(n+1) = DU(n)`')).toBe('<p><code>DU(n+1) = DU(n)</code></p>');
|
||||
});
|
||||
it('lists', () => {
|
||||
expect(render('- Stock de **100**\n- 5 jours')).toBe('<ul><li>Stock de <strong>100</strong></li><li>5 jours</li></ul>');
|
||||
expect(render('1. un\n2. deux')).toBe('<ol><li>un</li><li>deux</li></ol>');
|
||||
});
|
||||
it('numbered paragraphs that are not lists stay paragraphs', () => {
|
||||
expect(render('**1.** Derrière une clé')).toBe('<p><strong>1.</strong> Derrière une clé</p>');
|
||||
});
|
||||
it('plain excerpts', () => {
|
||||
expect(plain('**1.** Derrière\n- une clé')).toBe('1. Derrière une clé');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user