The proposal table had a dozen measure columns with no grouping. Five group headers now gather neighbouring measures without changing any column text; numbers align right in tabular figures, and the kept placement row wears the Retenu tint and a « retenue » pill. Generation settings and progress, badges and the paper choice become cards, each paper a distinct choice; the preview toolbar groups its commands. The printed paper is unchanged. Checked: node 172, browser 162. --- FR --- [IMP] propositions, génération, badges, aperçu : cartes, intitulés Le tableau des propositions alignait une douzaine de mesures sans groupe. Cinq intitulés de groupe réunissent les mesures voisines sans changer le texte d'aucune colonne ; les nombres s'alignent à droite en chiffres tabulaires, la ligne du retenu porte la teinte Retenu et une pastille « retenue ». Réglages et avancement, badges et papier deviennent des cartes, chaque papier un choix distinct ; la barre de l'aperçu groupe ses commandes. Le papier imprimé ne change pas. Vérifié : node 172, navigateur 162. Assisted-by: Claude Opus 5.5
122 lines
5.4 KiB
JavaScript
122 lines
5.4 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||
|
||
// Le panneau « À propos » dans un vrai moteur de rendu (§ 18.6) : monté
|
||
// seul, sur un presse-papiers d'épreuve qui relève ce qu'on lui remet ou
|
||
// refuse. Le bloc copié est celui qu'on lui passe ; l'ouverture depuis le
|
||
// bandeau et la composition du bloc s'éprouvent dans App.navigateur.test.js.
|
||
//
|
||
// Le projet navigateur emploie expect, et test et describe de Vitest
|
||
// (§ 14.8).
|
||
import './jetons.css';
|
||
import { flushSync, mount, unmount } from 'svelte';
|
||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||
import { creerHorlogePilotee } from '../application/horloge.js';
|
||
import { TABLES_APPLICATION } from '../application/libelles.js';
|
||
import { creerTraduction } from '../application/traduction.js';
|
||
import { couleurDeJeton } from '../../test/navigateur/captures.js';
|
||
import { composantes, contraste, couleurTexte, fondPeintSous, sousTheme } from '../../test/navigateur/rendu.js';
|
||
import APropos from './APropos.svelte';
|
||
import { TABLES_INTERFACE } from './libelles.js';
|
||
|
||
const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]);
|
||
|
||
// Un bloc inventé, de la forme d'une ligne d'« À propos ».
|
||
const BLOC = 'Logiciel témoin \u{00B7} forme d\u{2019}épreuve \u{00B7} plateforme simulée';
|
||
|
||
const montes = [];
|
||
afterEach(() => {
|
||
for (const { composant, conteneur } of montes.splice(0)) {
|
||
unmount(composant);
|
||
conteneur.remove();
|
||
}
|
||
});
|
||
|
||
// Un presse-papiers d'épreuve : relève chaque copie, ou lève refus.
|
||
function pressePapiersDEpreuve({ refus = null } = {}) {
|
||
const copies = [];
|
||
return {
|
||
copies,
|
||
async copier(texte) {
|
||
if (refus !== null) throw refus;
|
||
copies.push(texte);
|
||
},
|
||
};
|
||
}
|
||
|
||
function monter(props) {
|
||
const conteneur = document.createElement('div');
|
||
document.body.prepend(conteneur);
|
||
const contexte = new Map([
|
||
['t', t],
|
||
['horloge', creerHorlogePilotee()],
|
||
]);
|
||
const composant = mount(APropos, { target: conteneur, props, context: contexte });
|
||
flushSync();
|
||
montes.push({ composant, conteneur });
|
||
return conteneur;
|
||
}
|
||
|
||
// La carte qui porte un élément : le plus proche ancêtre peint du fond de la
|
||
// carte, au rayon de 12 px ; null sans elle.
|
||
function carteDe(element) {
|
||
const fond = couleurDeJeton('--couleur-carte-fond').join();
|
||
for (let n = element.parentElement; n !== null; n = n.parentElement) {
|
||
const style = getComputedStyle(n);
|
||
if (composantes(style.backgroundColor).join() === fond && style.borderTopLeftRadius === '12px') return n;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
describe('« À propos » : l’habillage (§ 18.6)', () => {
|
||
test('le titre au-dessus d’une carte qui porte le bloc, sur le fond sourd, et les commandes ; « Copier » est l’action principale, dans les deux thèmes', async () => {
|
||
const conteneur = monter({ bloc: BLOC, pressePapiers: pressePapiersDEpreuve(), fermer: () => {} });
|
||
const ecran = conteneur.querySelector('[data-ecran="apropos"]');
|
||
const titre = ecran.querySelector('h1');
|
||
expect(titre.textContent).toBe(t('coquille.apropos.titre'));
|
||
const bloc = ecran.querySelector('[data-bloc]');
|
||
expect(bloc.textContent).toBe(BLOC);
|
||
const carte = carteDe(bloc);
|
||
expect(carte, 'carte du bloc').not.toBeNull();
|
||
expect(ecran.contains(carte)).toBe(true);
|
||
expect(carte.contains(titre)).toBe(false);
|
||
expect(carte.contains(ecran.querySelector('[data-commande="copier"]'))).toBe(true);
|
||
expect(carte.contains(ecran.querySelector('[data-commande="fermerApropos"]'))).toBe(true);
|
||
for (const theme of ['clair', 'sombre']) {
|
||
await sousTheme(theme, async () => {
|
||
const [fond] = fondPeintSous(bloc);
|
||
expect(fond.join(), theme).toBe(couleurDeJeton('--couleur-carte-fond-sourd').join());
|
||
expect(contraste(couleurTexte(bloc), fond), theme).toBeGreaterThanOrEqual(4.5);
|
||
const copier = getComputedStyle(ecran.querySelector('[data-commande="copier"]'));
|
||
expect(composantes(copier.backgroundColor).join(), theme).toBe(couleurDeJeton('--couleur-action-fond').join());
|
||
});
|
||
}
|
||
});
|
||
|
||
test('« Copier » remet exactement le bloc affiché et le dit ; refusée, la copie le dit dans l’encre du refus', async () => {
|
||
const presse = pressePapiersDEpreuve();
|
||
const conteneur = monter({ bloc: BLOC, pressePapiers: presse, fermer: () => {} });
|
||
conteneur.querySelector('[data-commande="copier"]').click();
|
||
await vi.waitFor(() => expect(presse.copies).toEqual([BLOC]));
|
||
await vi.waitFor(() => {
|
||
flushSync();
|
||
expect(conteneur.querySelector('[role="status"]')?.textContent).toBe(t('coquille.apropos.copie'));
|
||
});
|
||
|
||
const refuse = monter({ bloc: BLOC, pressePapiers: pressePapiersDEpreuve({ refus: new Error('refusé') }), fermer: () => {} });
|
||
refuse.querySelector('[data-commande="copier"]').click();
|
||
await vi.waitFor(() => {
|
||
flushSync();
|
||
expect(refuse.querySelector('[data-refus="copier"]')?.textContent).toBe(t('coquille.apropos.copieRefusee'));
|
||
});
|
||
const message = refuse.querySelector('[data-refus="copier"]');
|
||
expect(couleurTexte(message).join()).toBe(couleurDeJeton('--couleur-refus').join());
|
||
});
|
||
|
||
test('« Fermer » appelle fermer une fois', () => {
|
||
const fermer = vi.fn();
|
||
const conteneur = monter({ bloc: BLOC, pressePapiers: pressePapiersDEpreuve(), fermer });
|
||
conteneur.querySelector('[data-commande="fermerApropos"]').click();
|
||
expect(fermer).toHaveBeenCalledTimes(1);
|
||
});
|
||
});
|