744 lines
35 KiB
JavaScript
744 lines
35 KiB
JavaScript
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|||
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|||
|
|
|
|||
|
|
// Épreuves des composants de graphique de la page de qualité dans un vrai
|
|||
|
|
// moteur de rendu (§ 12.3, § 12.4, § 12.5, § 12.7, § 14.5, D30, D34, D44,
|
|||
|
|
// D51) : escalier, points de relevé, barres, grille, jumeau et pourcentage,
|
|||
|
|
// montés chacun par mount sur des données forgées, avec les contextes que
|
|||
|
|
// pose App — 't' et 'format'. Deux graphiques empilés partagent leur rang
|
|||
|
|
// pointé comme une vue le ferait par bind:pointe : un magasin lu par
|
|||
|
|
// fromStore, que chaque graphique lit et écrit. Les gestes passent par des
|
|||
|
|
// entrées que Chromium traite comme réelles (test/navigateur/entrees.js).
|
|||
|
|
// Les noms d'épreuve sont inventés.
|
|||
|
|
//
|
|||
|
|
// Le projet navigateur emploie expect, et test et describe de Vitest
|
|||
|
|
// (§ 14.8).
|
|||
|
|
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest';
|
|||
|
|
import { flushSync, mount, tick, unmount } from 'svelte';
|
|||
|
|
import { fromStore, writable } from 'svelte/store';
|
|||
|
|
import '../../jetons.css';
|
|||
|
|
import { TABLES_APPLICATION } from '../../../application/libelles.js';
|
|||
|
|
import { creerFormat } from '../../../application/format.js';
|
|||
|
|
import { creerTraduction } from '../../../application/traduction.js';
|
|||
|
|
import { releverMarques } from '../../../../test/navigateur/captures.js';
|
|||
|
|
import { fixerCadre, pointer, touche } from '../../../../test/navigateur/entrees.js';
|
|||
|
|
import { attenuations, sousTheme } from '../../../../test/navigateur/rendu.js';
|
|||
|
|
import { TABLES_INTERFACE } from '../../libelles.js';
|
|||
|
|
import { LARGEUR_NOM_PX, margesCommunes } from './pointage.js';
|
|||
|
|
import Barres from './Barres.svelte';
|
|||
|
|
import Escalier from './Escalier.svelte';
|
|||
|
|
import Grille from './Grille.svelte';
|
|||
|
|
import Jumeau from './Jumeau.svelte';
|
|||
|
|
import Pourcentage from './Pourcentage.svelte';
|
|||
|
|
|
|||
|
|
const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]);
|
|||
|
|
const CONTEXTE = new Map([
|
|||
|
|
['t', t],
|
|||
|
|
['format', creerFormat(t)],
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
const NOM_LONG = 'Maëlle-Hortense Quenouille-Vasque de Belle-Rive';
|
|||
|
|
const PRENOMS = ['Iris Ombrelle', 'Théo Pervenche', 'Ondine Lacasse', 'Basile Tourteau', NOM_LONG];
|
|||
|
|
const nomDe = (i) => PRENOMS[i % PRENOMS.length];
|
|||
|
|
|
|||
|
|
const montes = [];
|
|||
|
|
|
|||
|
|
// Le cadre des écrans de l'application (§ 19.4) : un graphique de 1 000 px
|
|||
|
|
// y tient en entier.
|
|||
|
|
beforeAll(async () => {
|
|||
|
|
await fixerCadre(1366, 700);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
afterAll(async () => {
|
|||
|
|
await fixerCadre(1366, 700);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
afterEach(() => {
|
|||
|
|
for (const { composant, cible } of montes.splice(0)) {
|
|||
|
|
unmount(composant);
|
|||
|
|
cible.remove();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Monte un composant dans un conteneur neuf de la largeur donnée, au début
|
|||
|
|
// du corps, puis laisse le graphique mesurer sa largeur et son texte.
|
|||
|
|
async function monter(Composant, props, largeur = 800) {
|
|||
|
|
const cible = document.createElement('div');
|
|||
|
|
cible.style.width = `${largeur}px`;
|
|||
|
|
document.body.prepend(cible);
|
|||
|
|
const composant = mount(Composant, { target: cible, props, context: CONTEXTE });
|
|||
|
|
flushSync();
|
|||
|
|
montes.push({ composant, cible });
|
|||
|
|
await tick();
|
|||
|
|
flushSync();
|
|||
|
|
await tick();
|
|||
|
|
return cible;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Un rang pointé partagé, comme bind:pointe le partage entre deux vues :
|
|||
|
|
// avec(props) rend les propriétés reçues augmentées de pointe, un accesseur
|
|||
|
|
// sur le magasin commun — une décomposition par « ... » copierait sa valeur
|
|||
|
|
// et perdrait la liaison.
|
|||
|
|
function rangPartage() {
|
|||
|
|
const rang = fromStore(writable(null));
|
|||
|
|
const accesseur = {
|
|||
|
|
get() {
|
|||
|
|
return rang.current;
|
|||
|
|
},
|
|||
|
|
set(valeur) {
|
|||
|
|
rang.current = valeur;
|
|||
|
|
},
|
|||
|
|
enumerable: true,
|
|||
|
|
};
|
|||
|
|
return { rang, avec: (props) => Object.defineProperty({ ...props }, 'pointe', accesseur) };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Des marges partagées par un groupe de graphiques, comme une vue les lie :
|
|||
|
|
// chaque graphique écrit les siennes (marges) et reçoit les marges communes
|
|||
|
|
// de tous (alignerSur, margesCommunes). Les accesseurs se posent sur l'objet
|
|||
|
|
// reçu même, qui garde ainsi celui de pointe.
|
|||
|
|
function margesPartagees(n) {
|
|||
|
|
const marges = Array.from({ length: n }, () => fromStore(writable(null)));
|
|||
|
|
return (props, i) =>
|
|||
|
|
Object.defineProperties(
|
|||
|
|
props,
|
|||
|
|
{
|
|||
|
|
marges: { get: () => marges[i].current, set: (v) => (marges[i].current = v), enumerable: true },
|
|||
|
|
alignerSur: { get: () => margesCommunes(marges.map((m) => m.current)), enumerable: true },
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// La couleur calculée d'un jeton, lue sur un élément témoin.
|
|||
|
|
function couleurDuJeton(jeton) {
|
|||
|
|
const temoin = document.createElement('span');
|
|||
|
|
temoin.style.color = `var(${jeton})`;
|
|||
|
|
document.body.append(temoin);
|
|||
|
|
const couleur = getComputedStyle(temoin).color;
|
|||
|
|
temoin.remove();
|
|||
|
|
return couleur;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Le motif calculé d'un trait, ses longueurs en px ; [] pour un trait plein.
|
|||
|
|
const motifDe = (element) => (getComputedStyle(element).strokeDasharray.match(/[\d.]+/g) ?? []).map(Number);
|
|||
|
|
|
|||
|
|
// Le point (x, y) de l'élément SVG d'un graphique dans la fenêtre, pour un
|
|||
|
|
// point local au SVG : sans viewBox, le pixel du dessin est celui de l'écran.
|
|||
|
|
function versFenetre(svg, x, y) {
|
|||
|
|
const cadre = svg.getBoundingClientRect();
|
|||
|
|
return { x: cadre.left + x, y: cadre.top + y };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// La surface de pointage d'un graphique : le rect sans remplissage qui
|
|||
|
|
// capte le pointeur.
|
|||
|
|
const surfaceDe = (svg) => svg.querySelector(':scope > rect[pointer-events="all"]');
|
|||
|
|
|
|||
|
|
const infobulleDe = (cible) => cible.querySelector('[role="tooltip"]');
|
|||
|
|
|
|||
|
|
// Le profil forgé de n personnes : rencontres croissantes, appartenances
|
|||
|
|
// rencontrées dessous, plafond réalisé en escalier, borne de diversité ;
|
|||
|
|
// marques aux rangs 2 et 4.
|
|||
|
|
function profil(n) {
|
|||
|
|
const rencontres = Array.from({ length: n }, (_, i) => 10 + Math.floor((17 * i) / Math.max(1, n - 1)));
|
|||
|
|
return {
|
|||
|
|
rencontres,
|
|||
|
|
appartenances: rencontres.map((r) => Math.floor(r / 3)),
|
|||
|
|
plafond: rencontres.map((_, i) => (i < n / 2 ? 27 : 26)),
|
|||
|
|
borne: rencontres.map(() => 6),
|
|||
|
|
marques: rencontres.map((_, i) => (i === 1 ? 'ancre' : i === 3 ? 'reserve' : null)),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const COLONNES_PROFIL = [
|
|||
|
|
{ cle: 'rang', libelle: 'Rang', genre: 'nombre' },
|
|||
|
|
{ cle: 'nom', libelle: 'Personne', genre: 'texte' },
|
|||
|
|
{ cle: 'rencontres', libelle: 'Rencontres', genre: 'nombre' },
|
|||
|
|
{ cle: 'part', libelle: 'Part', genre: 'pourcentage' },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
function infobulleProfil(p) {
|
|||
|
|
return {
|
|||
|
|
colonnes: COLONNES_PROFIL,
|
|||
|
|
lignes: p.rencontres.map((r, i) => ({
|
|||
|
|
rang: i + 1,
|
|||
|
|
nom: nomDe(i),
|
|||
|
|
rencontres: r,
|
|||
|
|
part: { texte: `${Math.floor((100 * r) / p.plafond[i])} %`, compte: `${r} sur ${p.plafond[i]}` },
|
|||
|
|
})),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function propsProfil(n, extra = {}) {
|
|||
|
|
const p = profil(n);
|
|||
|
|
return {
|
|||
|
|
graphique: 'profil',
|
|||
|
|
nom: 'Qui rencontre le moins de monde ?',
|
|||
|
|
synthese: 'Minimum 10 rencontres, une personne.',
|
|||
|
|
series: [
|
|||
|
|
{ libelle: 'rencontres', valeurs: p.rencontres },
|
|||
|
|
{ libelle: 'appartenances rencontrées', valeurs: p.appartenances },
|
|||
|
|
],
|
|||
|
|
reperes: [
|
|||
|
|
{ genre: 'plafond', libelle: 'plafond réalisé', valeurs: p.plafond },
|
|||
|
|
{ genre: 'borne', libelle: 'borne de diversité', valeurs: p.borne },
|
|||
|
|
],
|
|||
|
|
marques: p.marques,
|
|||
|
|
infobulle: infobulleProfil(p),
|
|||
|
|
...extra,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
describe('graphiques : l’escalier du profil (§ 12.3, D30)', () => {
|
|||
|
|
test('une image nommée et décrite, sans viewBox ; chaque trait peint son jeton, sans opacité ni filtre dans son ascendance', async () => {
|
|||
|
|
const cible = await monter(Escalier, propsProfil(12));
|
|||
|
|
const svg = cible.querySelector('svg[data-graphique="profil"]');
|
|||
|
|
expect(svg.getAttribute('role')).toBe('img');
|
|||
|
|
expect(svg.hasAttribute('viewBox')).toBe(false);
|
|||
|
|
expect(document.getElementById(svg.getAttribute('aria-labelledby')).textContent).toBe('Qui rencontre le moins de monde ?');
|
|||
|
|
expect(document.getElementById(svg.getAttribute('aria-describedby')).textContent).toBe('Minimum 10 rencontres, une personne.');
|
|||
|
|
const attendus = [
|
|||
|
|
['[data-serie="serie1"]', '--couleur-graphique-serie-1'],
|
|||
|
|
['[data-serie="serie2"]', '--couleur-graphique-serie-2'],
|
|||
|
|
['[data-repere="plafond"]', '--couleur-graphique-repere'],
|
|||
|
|
['[data-repere="borne"]', '--couleur-graphique-borne'],
|
|||
|
|
];
|
|||
|
|
for (const [selecteur, jeton] of attendus) {
|
|||
|
|
const trace = svg.querySelector(selecteur);
|
|||
|
|
expect(trace, selecteur).not.toBeNull();
|
|||
|
|
expect(getComputedStyle(trace).stroke, selecteur).toBe(couleurDuJeton(jeton));
|
|||
|
|
expect(attenuations(trace), selecteur).toEqual([]);
|
|||
|
|
}
|
|||
|
|
for (const marque of svg.querySelectorAll('[data-marque]')) expect(attenuations(marque)).toEqual([]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rouge : les quatre traits — plein, tirets, mixte, pointillé — ont des motifs calculés deux à deux distincts', async () => {
|
|||
|
|
const cible = await monter(Escalier, propsProfil(12));
|
|||
|
|
const motifs = ['[data-serie="serie1"]', '[data-serie="serie2"]', '[data-repere="plafond"]', '[data-repere="borne"]'].map(
|
|||
|
|
(s) => getComputedStyle(cible.querySelector(s)).strokeDasharray,
|
|||
|
|
);
|
|||
|
|
expect(new Set(motifs).size, motifs.join(' | ')).toBe(4);
|
|||
|
|
expect(motifs[0]).toBe('none');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rouge : le plafond en tiret-point, la borne en pointillés plus courts que les tirets de la seconde série', async () => {
|
|||
|
|
const cible = await monter(Escalier, propsProfil(12));
|
|||
|
|
const [long, vide1, court, vide2, ...reste] = motifDe(cible.querySelector('path[data-repere="plafond"]'));
|
|||
|
|
expect([long, vide1, court, vide2].every(Number.isFinite) && reste.length === 0, 'plafond : tiret, vide, point, vide').toBe(true);
|
|||
|
|
expect(long).toBeGreaterThan(court);
|
|||
|
|
expect(court).toBeLessThanOrEqual(Math.min(vide1, vide2));
|
|||
|
|
const borne = motifDe(cible.querySelector('path[data-repere="borne"]'));
|
|||
|
|
expect(borne, 'borne : point, vide').toHaveLength(2);
|
|||
|
|
expect(borne[0]).toBeLessThanOrEqual(borne[1]);
|
|||
|
|
expect(borne[0]).toBeLessThan(motifDe(cible.querySelector('path[data-serie="serie2"]'))[0]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rouge : plus étroit que ses marges, l’escalier se rend sans lever, zone nulle', async () => {
|
|||
|
|
const cible = await monter(Escalier, propsProfil(12), 150);
|
|||
|
|
const svg = cible.querySelector('svg[data-graphique="profil"]');
|
|||
|
|
expect(svg).not.toBeNull();
|
|||
|
|
expect(svg.querySelector('[data-serie="serie1"]')).not.toBeNull();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un seul axe vertical ; chaque trace porte son libellé sur le tracé, au bout de sa ligne', async () => {
|
|||
|
|
const cible = await monter(Escalier, propsProfil(12));
|
|||
|
|
const svg = cible.querySelector('svg');
|
|||
|
|
const axes = [...svg.querySelectorAll('line[data-marque="graphiqueAxe"]')];
|
|||
|
|
expect(axes.filter((a) => a.getAttribute('x1') === a.getAttribute('x2'))).toHaveLength(1);
|
|||
|
|
const surface = surfaceDe(svg);
|
|||
|
|
const fin = Number(surface.getAttribute('x')) + Number(surface.getAttribute('width'));
|
|||
|
|
const textes = [...svg.querySelectorAll('text')];
|
|||
|
|
for (const libelle of ['rencontres', 'appartenances rencontrées', 'plafond réalisé', 'borne de diversité']) {
|
|||
|
|
const etiquette = textes.find((x) => x.textContent === libelle);
|
|||
|
|
expect(etiquette, libelle).toBeDefined();
|
|||
|
|
expect(Number(etiquette.getAttribute('x'))).toBeGreaterThan(fin);
|
|||
|
|
const boite = etiquette.getBoundingClientRect();
|
|||
|
|
expect(boite.right).toBeLessThanOrEqual(svg.getBoundingClientRect().right + 0.5);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un signe de forme aux seuls rangs marqués, de la marque du registre', async () => {
|
|||
|
|
const cible = await monter(Escalier, propsProfil(12));
|
|||
|
|
expect(cible.querySelectorAll('svg [data-marque="graphiqueMarque"]')).toHaveLength(2);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rouge : la surface de pointage est un rect sans remplissage, sœur des marques, jamais leur ancêtre', async () => {
|
|||
|
|
const cible = await monter(Escalier, propsProfil(12));
|
|||
|
|
const svg = cible.querySelector('svg');
|
|||
|
|
const surface = surfaceDe(svg);
|
|||
|
|
expect(surface).not.toBeNull();
|
|||
|
|
expect(surface.getAttribute('fill')).toBe('none');
|
|||
|
|
expect(surface.children).toHaveLength(0);
|
|||
|
|
for (const marque of svg.querySelectorAll('[data-marque]')) {
|
|||
|
|
expect(surface.contains(marque)).toBe(false);
|
|||
|
|
// Aucun ancêtre d'une marque, dans le dessin, ne capte le pointeur à
|
|||
|
|
// la place de la surface.
|
|||
|
|
for (let n = marque.parentElement; n !== svg; n = n.parentElement) {
|
|||
|
|
expect(getComputedStyle(n).pointerEvents, n.localName).not.toBe('all');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// La surface est la dernière : rien de peint ne la couvre.
|
|||
|
|
expect(svg.lastElementChild).toBe(surface);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rouge : à 260 rangs dans 1 000 px, le pointage désigne le rang le plus proche, et l’infobulle porte sa ligne', async () => {
|
|||
|
|
const cible = await monter(Escalier, propsProfil(260), 1000);
|
|||
|
|
const svg = cible.querySelector('svg');
|
|||
|
|
const surface = surfaceDe(svg);
|
|||
|
|
const x0 = Number(surface.getAttribute('x'));
|
|||
|
|
const pas = Number(surface.getAttribute('width')) / 260;
|
|||
|
|
const y = Number(surface.getAttribute('y')) + 5;
|
|||
|
|
for (let rang = 1; rang <= 260; rang += 1) {
|
|||
|
|
const p = versFenetre(svg, x0 + (rang - 0.5) * pas, y);
|
|||
|
|
await pointer('bouger', p.x, p.y);
|
|||
|
|
flushSync();
|
|||
|
|
expect(svg.querySelector('[data-rang]')?.getAttribute('data-rang'), `rang ${rang}`).toBe(String(rang));
|
|||
|
|
}
|
|||
|
|
const bulle = infobulleDe(cible);
|
|||
|
|
expect(bulle.hidden).toBe(false);
|
|||
|
|
expect(bulle.textContent).toContain(nomDe(259));
|
|||
|
|
expect(bulle.querySelector('[data-pourcentage] + [data-compte-pire]').textContent).not.toBe('');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('Échap ferme l’infobulle ; quitter le tracé aussi', async () => {
|
|||
|
|
const cible = await monter(Escalier, propsProfil(5));
|
|||
|
|
const svg = cible.querySelector('svg');
|
|||
|
|
const surface = surfaceDe(svg);
|
|||
|
|
const boite = surface.getBoundingClientRect();
|
|||
|
|
// Le document a le focus, comme quand l'opérateur survole puis tape :
|
|||
|
|
// une touche n'atteint que le document focalisé.
|
|||
|
|
const focalisable = document.createElement('button');
|
|||
|
|
cible.append(focalisable);
|
|||
|
|
focalisable.focus();
|
|||
|
|
await pointer('bouger', boite.left + 3, boite.top + 3);
|
|||
|
|
flushSync();
|
|||
|
|
expect(infobulleDe(cible).hidden).toBe(false);
|
|||
|
|
await touche('Escape');
|
|||
|
|
flushSync();
|
|||
|
|
expect(infobulleDe(cible).hidden).toBe(true);
|
|||
|
|
await pointer('bouger', boite.left + 4, boite.top + 3);
|
|||
|
|
flushSync();
|
|||
|
|
expect(infobulleDe(cible).hidden).toBe(false);
|
|||
|
|
await pointer('bouger', boite.right + 200, boite.bottom + 200);
|
|||
|
|
flushSync();
|
|||
|
|
expect(infobulleDe(cible).hidden).toBe(true);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rouge : deux graphiques empilés — comptes, puis pourcentages — désignent le même rang pointé, l’infobulle dans le seul graphique survolé', async () => {
|
|||
|
|
const p = profil(20);
|
|||
|
|
const partage = rangPartage();
|
|||
|
|
const aligner = margesPartagees(2);
|
|||
|
|
const pourcents = p.rencontres.map((r, i) => (i === 6 ? null : Math.floor((100 * r) / p.plafond[i])));
|
|||
|
|
const haut = await monter(Escalier, aligner(partage.avec(propsProfil(20)), 0), 900);
|
|||
|
|
const bas = await monter(
|
|||
|
|
Escalier,
|
|||
|
|
aligner(partage.avec({
|
|||
|
|
graphique: 'profilPourcent',
|
|||
|
|
nom: 'Quelle part de son plafond chacun rencontre-t-il ?',
|
|||
|
|
synthese: 'minimum 10 sur 27',
|
|||
|
|
series: [{ libelle: 'part du plafond réalisé', valeurs: pourcents }],
|
|||
|
|
reperes: [{ genre: 'cent', libelle: '100 %', valeurs: p.rencontres.map(() => 100) }],
|
|||
|
|
graduer: (v) => `${v} %`,
|
|||
|
|
infobulle: infobulleProfil(p),
|
|||
|
|
}), 1),
|
|||
|
|
900,
|
|||
|
|
);
|
|||
|
|
await tick();
|
|||
|
|
flushSync();
|
|||
|
|
const svgHaut = haut.querySelector('svg');
|
|||
|
|
const surface = surfaceDe(svgHaut);
|
|||
|
|
const pas = Number(surface.getAttribute('width')) / 20;
|
|||
|
|
const point = versFenetre(svgHaut, Number(surface.getAttribute('x')) + 7.5 * pas, Number(surface.getAttribute('y')) + 4);
|
|||
|
|
await pointer('bouger', point.x, point.y);
|
|||
|
|
flushSync();
|
|||
|
|
expect(haut.querySelector('[data-rang]').getAttribute('data-rang')).toBe('8');
|
|||
|
|
expect(bas.querySelector('[data-rang]')?.getAttribute('data-rang')).toBe('8');
|
|||
|
|
expect(infobulleDe(haut).hidden).toBe(false);
|
|||
|
|
expect(infobulleDe(bas).hidden).toBe(true);
|
|||
|
|
// La règle à 100 % a le motif du plafond, la série en pourcentage le plein.
|
|||
|
|
const regle = bas.querySelector('path[data-repere="cent"]');
|
|||
|
|
expect(bas.querySelector('text[data-repere="cent"]').textContent).toBe('100 %');
|
|||
|
|
expect(getComputedStyle(regle).strokeDasharray).toBe(getComputedStyle(haut.querySelector('[data-repere="plafond"]')).strokeDasharray);
|
|||
|
|
expect(getComputedStyle(regle).strokeDasharray).not.toBe(getComputedStyle(bas.querySelector('[data-serie="serie1"]')).strokeDasharray);
|
|||
|
|
// Les deux abscisses s'alignent : même zone, rang contre rang ; le rang
|
|||
|
|
// 7, sans valeur, interrompt le tracé du second.
|
|||
|
|
const zones = [haut, bas].map((c) => ['x', 'width'].map((a) => surfaceDe(c.querySelector('svg')).getAttribute(a)));
|
|||
|
|
expect(zones[1]).toEqual(zones[0]);
|
|||
|
|
expect(bas.querySelector('[data-serie="serie1"]').getAttribute('d').match(/M/g)).toHaveLength(2);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rouge : un chemin ne relie jamais deux rangs par-dessus un rang sans valeur', async () => {
|
|||
|
|
const cible = await monter(Escalier, {
|
|||
|
|
graphique: 'trou',
|
|||
|
|
nom: 'Trou',
|
|||
|
|
synthese: 'Un rang sans valeur.',
|
|||
|
|
series: [{ libelle: 'part', valeurs: [40, null, 40, 60] }],
|
|||
|
|
});
|
|||
|
|
const d = cible.querySelector('[data-serie="serie1"]').getAttribute('d');
|
|||
|
|
expect(d.match(/M/g)).toHaveLength(2);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('graphiques : les points de relevé (§ 12.14, D49)', () => {
|
|||
|
|
const ABSCISSES = [1, 10, 100, 1000, 10000, 20000];
|
|||
|
|
const multiple = (graphique, valeurs) => ({
|
|||
|
|
graphique,
|
|||
|
|
nom: `Le ${graphique} au fil de la recherche`,
|
|||
|
|
synthese: t('graphiques.synthese.pointsDeReleve'),
|
|||
|
|
forme: 'points',
|
|||
|
|
abscisses: ABSCISSES,
|
|||
|
|
series: [{ libelle: graphique, valeurs }],
|
|||
|
|
infobulle: {
|
|||
|
|
colonnes: [
|
|||
|
|
{ cle: 'mouvements', libelle: 'Mouvements', genre: 'nombre' },
|
|||
|
|
{ cle: 'valeur', libelle: 'Valeur', genre: 'nombre' },
|
|||
|
|
],
|
|||
|
|
lignes: ABSCISSES.map((mouvements, i) => ({ mouvements, valeur: valeurs[i] })),
|
|||
|
|
},
|
|||
|
|
hauteur: 140,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rouge : un point aux seuls points de relevé ; le point pointé est désigné dans tous les petits multiples à la fois', async () => {
|
|||
|
|
const partage = rangPartage();
|
|||
|
|
const aligner = margesPartagees(2);
|
|||
|
|
const a = await monter(Escalier, aligner(partage.avec(multiple('collisions', [9, 7, 4, 2, 1, 1])), 0), 700);
|
|||
|
|
const b = await monter(Escalier, aligner(partage.avec(multiple('appartenances rencontrées', [3, 5, 8, 9, 10, 10])), 1), 700);
|
|||
|
|
await tick();
|
|||
|
|
flushSync();
|
|||
|
|
for (const cible of [a, b]) expect(cible.querySelectorAll('[data-point-releve]')).toHaveLength(6);
|
|||
|
|
// L'abscisse est commune : chaque point de relevé à la même abscisse.
|
|||
|
|
const abscisses = [a, b].map((c) => [...c.querySelectorAll('[data-point-releve]')].map((p) => p.getAttribute('cx')));
|
|||
|
|
expect(abscisses[1]).toEqual(abscisses[0]);
|
|||
|
|
const point = a.querySelector('[data-point-releve="100"]');
|
|||
|
|
const boite = point.getBoundingClientRect();
|
|||
|
|
await pointer('bouger', boite.left + boite.width / 2 + 2, boite.top + boite.height / 2);
|
|||
|
|
flushSync();
|
|||
|
|
for (const cible of [a, b]) {
|
|||
|
|
const designe = cible.querySelector('[data-rang]');
|
|||
|
|
expect(designe?.getAttribute('data-rang')).toBe('3');
|
|||
|
|
expect(designe.getAttribute('cx')).toBe(cible.querySelector('[data-point-releve="100"]').getAttribute('cx'));
|
|||
|
|
}
|
|||
|
|
expect(infobulleDe(a).hidden).toBe(false);
|
|||
|
|
expect(infobulleDe(b).hidden).toBe(true);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rouge : l’infobulle écrit les nombres comme les axes, par format.nombre', async () => {
|
|||
|
|
const cible = await monter(Escalier, multiple('collisions', [9, 7, 4, 2, 1, 1]), 700);
|
|||
|
|
const boite = cible.querySelector('[data-point-releve="20000"]').getBoundingClientRect();
|
|||
|
|
await pointer('bouger', boite.left + boite.width / 2, boite.top + boite.height / 2);
|
|||
|
|
flushSync();
|
|||
|
|
const bulle = infobulleDe(cible);
|
|||
|
|
expect(bulle.hidden).toBe(false);
|
|||
|
|
expect(bulle.textContent).toContain('20\u{202F}000');
|
|||
|
|
expect(bulle.textContent).not.toContain('20000');
|
|||
|
|
const graduations = [...cible.querySelectorAll('svg text')].map((x) => x.textContent);
|
|||
|
|
expect(graduations).toContain('20\u{202F}000');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('graphiques : les barres (§ 12.4, § 12.13)', () => {
|
|||
|
|
const LIGNES = [
|
|||
|
|
{ id: 'aubier', nom: 'Coopérative Aubier', appartenance: 1, valeurs: [0, 3] },
|
|||
|
|
{ id: 'long', nom: NOM_LONG, appartenance: null, valeurs: [2, 0] },
|
|||
|
|
{ id: 'ressac', nom: 'Atelier Ressac', appartenance: 2, valeurs: [1, 1] },
|
|||
|
|
];
|
|||
|
|
const INFOBULLE = {
|
|||
|
|
colonnes: [
|
|||
|
|
{ cle: 'nom', libelle: 'Appartenance', genre: 'texte' },
|
|||
|
|
{ cle: 'cumulees', libelle: 'Collisions cumulées', genre: 'nombre' },
|
|||
|
|
],
|
|||
|
|
lignes: LIGNES.map((l) => ({ nom: l.nom, cumulees: l.valeurs[0] + l.valeurs[1] })),
|
|||
|
|
};
|
|||
|
|
const props = (extra) => ({
|
|||
|
|
graphique: 'collisions',
|
|||
|
|
nom: 'Où les appartenances se retrouvent-elles ?',
|
|||
|
|
synthese: 'Trois appartenances.',
|
|||
|
|
lignes: LIGNES,
|
|||
|
|
segments: [
|
|||
|
|
{ genre: 'prouve', libelle: 'prouvé inévitable' },
|
|||
|
|
{ genre: 'reste', libelle: 'reste' },
|
|||
|
|
],
|
|||
|
|
infobulle: INFOBULLE,
|
|||
|
|
...extra,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rouge : un nom long se tronque sur le tracé, et reste entier dans l’infobulle et le jumeau', async () => {
|
|||
|
|
const cible = await monter(Barres, props());
|
|||
|
|
const ligne = cible.querySelector('[data-ligne-graphique="long"]');
|
|||
|
|
const intitule = [...ligne.querySelectorAll('text')].find((x) => x.textContent.startsWith('Maëlle'));
|
|||
|
|
expect(intitule.textContent).not.toBe(NOM_LONG);
|
|||
|
|
expect(intitule.textContent.endsWith(t('graphiques.troncature'))).toBe(true);
|
|||
|
|
expect(intitule.getComputedTextLength()).toBeLessThanOrEqual(LARGEUR_NOM_PX);
|
|||
|
|
const boite = ligne.querySelector('[data-segment]').getBoundingClientRect();
|
|||
|
|
await pointer('bouger', boite.left + 2, boite.top + boite.height / 2);
|
|||
|
|
flushSync();
|
|||
|
|
expect(infobulleDe(cible).hidden).toBe(false);
|
|||
|
|
expect(infobulleDe(cible).textContent).toContain(NOM_LONG);
|
|||
|
|
expect(cible.querySelector('[data-rang]').getAttribute('data-rang')).toBe('2');
|
|||
|
|
|
|||
|
|
const jumeau = await monter(Jumeau, { tableaux: [{ nom: 'Collisions', ...INFOBULLE }] });
|
|||
|
|
jumeau.querySelector('button').click();
|
|||
|
|
flushSync();
|
|||
|
|
expect(jumeau.querySelector('[data-jumeau]').textContent).toContain(NOM_LONG);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rouge : le libellé de chaque segment se pose sur la première barre où il est non nul, et dans la légende', async () => {
|
|||
|
|
const cible = await monter(Barres, props());
|
|||
|
|
const svg = cible.querySelector('svg');
|
|||
|
|
const textes = [...svg.querySelectorAll('text')];
|
|||
|
|
for (const [libelle, id] of [['prouvé inévitable', 'long'], ['reste', 'aubier']]) {
|
|||
|
|
const etiquette = textes.find((x) => x.textContent === libelle);
|
|||
|
|
const barre = svg.querySelector(`[data-ligne-graphique="${id}"] [data-segment]`).getBoundingClientRect();
|
|||
|
|
const boite = etiquette.getBoundingClientRect();
|
|||
|
|
expect(boite.top, libelle).toBeGreaterThanOrEqual(barre.top - 1);
|
|||
|
|
expect(boite.bottom, libelle).toBeLessThanOrEqual(barre.bottom + 1);
|
|||
|
|
expect(cible.querySelector('ul').textContent).toContain(libelle);
|
|||
|
|
}
|
|||
|
|
for (const segment of svg.querySelectorAll('[data-segment]')) {
|
|||
|
|
expect(Number(segment.getAttribute('width'))).toBeGreaterThan(0);
|
|||
|
|
expect(attenuations(segment)).toEqual([]);
|
|||
|
|
}
|
|||
|
|
expect(svg.querySelectorAll('[data-segment="prouve"]')).toHaveLength(2);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rouge : chaque ligne porte ses autres chiffres en encre secondaire, après sa barre ; une ligne sans barre écrit « — »', async () => {
|
|||
|
|
const vide = t('format.vide');
|
|||
|
|
const cible = await monter(
|
|||
|
|
Barres,
|
|||
|
|
props({
|
|||
|
|
lignes: [
|
|||
|
|
{ ...LIGNES[0], secondaire: '7 personnes, 3 paires, excédent 2' },
|
|||
|
|
{ ...LIGNES[1], secondaire: '4 personnes, 2 paires, excédent 0' },
|
|||
|
|
{ id: 'sans', nom: 'Ondine Lacasse', valeurs: [0, 0], secondaire: vide },
|
|||
|
|
],
|
|||
|
|
infobulle: null,
|
|||
|
|
}),
|
|||
|
|
640,
|
|||
|
|
);
|
|||
|
|
const svg = cible.querySelector('svg');
|
|||
|
|
const droite = svg.getBoundingClientRect().right;
|
|||
|
|
const encre = couleurDuJeton('--couleur-page-texte-secondaire');
|
|||
|
|
for (const [id, texte] of [['aubier', '7 personnes, 3 paires, excédent 2'], ['long', '4 personnes, 2 paires, excédent 0'], ['sans', vide]]) {
|
|||
|
|
const ligne = svg.querySelector(`[data-ligne-graphique="${id}"]`);
|
|||
|
|
const ecrit = [...ligne.querySelectorAll('text')].find((x) => x.textContent === texte);
|
|||
|
|
expect(ecrit, id).toBeDefined();
|
|||
|
|
expect(getComputedStyle(ecrit).fill, id).toBe(encre);
|
|||
|
|
expect(attenuations(ecrit), id).toEqual([]);
|
|||
|
|
const boite = ecrit.getBoundingClientRect();
|
|||
|
|
expect(boite.right, id).toBeLessThanOrEqual(droite + 0.5);
|
|||
|
|
for (const segment of ligne.querySelectorAll('[data-segment]')) {
|
|||
|
|
expect(boite.left, id).toBeGreaterThanOrEqual(segment.getBoundingClientRect().right);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Le libellé du segment « reste », posé sur la ligne d'Aubier, précède
|
|||
|
|
// ses chiffres.
|
|||
|
|
const reste = [...svg.querySelectorAll('text')].find((x) => x.textContent === 'reste').getBoundingClientRect();
|
|||
|
|
const chiffres = [...svg.querySelectorAll('[data-ligne-graphique="aubier"] text')].find((x) => x.textContent.startsWith('7 personnes'));
|
|||
|
|
expect(chiffres.getBoundingClientRect().left).toBeGreaterThanOrEqual(reste.right);
|
|||
|
|
expect(svg.querySelectorAll('[data-ligne-graphique="sans"] [data-segment]')).toHaveLength(0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('deux colonnes juxtaposées, chacune son axe, les lignes dans le même ordre', async () => {
|
|||
|
|
const cible = await monter(Barres, {
|
|||
|
|
graphique: 'diversite',
|
|||
|
|
nom: 'Quel animateur rencontre le moins de diversité ?',
|
|||
|
|
synthese: 'Deux animateurs.',
|
|||
|
|
lignes: [
|
|||
|
|
{ id: 'iris', nom: 'Iris Ombrelle', valeurs: [2, 9] },
|
|||
|
|
{ id: 'theo', nom: 'Théo Pervenche', valeurs: [4, 1] },
|
|||
|
|
],
|
|||
|
|
colonnes: [
|
|||
|
|
{ genre: 'serie1', libelle: 'appartenances' },
|
|||
|
|
{ genre: 'serie2', libelle: 'étiquettes' },
|
|||
|
|
],
|
|||
|
|
});
|
|||
|
|
const svg = cible.querySelector('svg');
|
|||
|
|
const lignes = [...svg.querySelectorAll('[data-ligne-graphique]')].map((g) => g.getAttribute('data-ligne-graphique'));
|
|||
|
|
expect(lignes).toEqual(['iris', 'theo']);
|
|||
|
|
const [premiere, seconde] = ['serie1', 'serie2'].map((g) => svg.querySelector(`[data-ligne-graphique="theo"] [data-segment="${g}"]`));
|
|||
|
|
expect(premiere.getBoundingClientRect().right).toBeLessThan(seconde.getBoundingClientRect().left);
|
|||
|
|
expect(svg.querySelectorAll('line[data-marque="graphiqueAxe"]')).toHaveLength(2);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('graphiques : la grille (§ 12.5, § 12.11, D33)', () => {
|
|||
|
|
const PERSONNES = [{ libelle: 'Iris Ombrelle' }, { libelle: 'Théo Pervenche' }, { libelle: 'Basile Tourteau' }];
|
|||
|
|
const VALEURS = [null, 1, 3, 1, null, 0, 3, 0, null];
|
|||
|
|
const props = (extra) => ({
|
|||
|
|
graphique: 'matrice',
|
|||
|
|
nom: 'Qui a rencontré qui ?',
|
|||
|
|
synthese: 'Une paire vue trois fois.',
|
|||
|
|
lignes: PERSONNES,
|
|||
|
|
colonnes: PERSONNES,
|
|||
|
|
valeurs: VALEURS,
|
|||
|
|
diagonale: true,
|
|||
|
|
infobulle: {
|
|||
|
|
colonnes: [{ cle: 'paire', libelle: 'Paire', genre: 'texte' }, { cle: 'fois', libelle: 'Fois', genre: 'nombre' }],
|
|||
|
|
lignes: VALEURS.map((fois, i) => ({ paire: `${PERSONNES[Math.floor(i / 3)].libelle} et ${PERSONNES[i % 3].libelle}`, fois })),
|
|||
|
|
},
|
|||
|
|
...extra,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('cases bordées, de leur niveau, le compte écrit de l’encre du niveau ; diagonale vide ; aucune case n’est une cible', async () => {
|
|||
|
|
const cible = await monter(Grille, props());
|
|||
|
|
const cases = cible.querySelectorAll('[data-case]');
|
|||
|
|
expect(cases).toHaveLength(9);
|
|||
|
|
const trois = cible.querySelector('[data-case="0:2"]');
|
|||
|
|
expect(trois.querySelector('[data-marque="graphiqueNiveau3"]')).not.toBeNull();
|
|||
|
|
expect(trois.querySelector('[data-marque="graphiqueBordureCase"]')).not.toBeNull();
|
|||
|
|
expect(trois.querySelector('text').textContent).toBe('3');
|
|||
|
|
expect(getComputedStyle(trois.querySelector('text')).fill).toBe(couleurDuJeton('--couleur-graphique-encre-claire'));
|
|||
|
|
const diagonale = cible.querySelector('[data-case="1:1"]');
|
|||
|
|
expect(diagonale.querySelector('text')?.textContent ?? '').toBe('');
|
|||
|
|
expect(diagonale.querySelector('[data-marque^="graphiqueNiveau"]')).toBeNull();
|
|||
|
|
for (const c of cases) {
|
|||
|
|
expect(c.querySelector('[tabindex], [role="button"], a, button')).toBeNull();
|
|||
|
|
expect(c.hasAttribute('tabindex')).toBe(false);
|
|||
|
|
}
|
|||
|
|
const zero = cible.querySelector('[data-case="1:2"]');
|
|||
|
|
expect(zero.querySelector('[data-marque^="graphiqueNiveau"]')).toBeNull();
|
|||
|
|
expect(zero.querySelector('text').textContent).toBe('0');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('le pointage désigne la case ; la diagonale n’ouvre rien', async () => {
|
|||
|
|
const cible = await monter(Grille, props());
|
|||
|
|
const boite = cible.querySelector('[data-case="1:2"] [data-marque="graphiqueBordureCase"]').getBoundingClientRect();
|
|||
|
|
await pointer('bouger', boite.left + boite.width / 2, boite.top + boite.height / 2);
|
|||
|
|
flushSync();
|
|||
|
|
expect(infobulleDe(cible).hidden).toBe(false);
|
|||
|
|
expect(infobulleDe(cible).textContent).toContain('Théo Pervenche et Basile Tourteau');
|
|||
|
|
const diag = cible.querySelector('[data-case="1:1"] [data-marque="graphiqueBordureCase"]').getBoundingClientRect();
|
|||
|
|
await pointer('bouger', diag.left + diag.width / 2, diag.top + diag.height / 2);
|
|||
|
|
flushSync();
|
|||
|
|
expect(infobulleDe(cible).hidden).toBe(true);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('une case sans compte écrit « — », sans couleur ; une grille trop étroite cède la place à sa phrase', async () => {
|
|||
|
|
const cible = await monter(Grille, props({ diagonale: false }));
|
|||
|
|
const vide = cible.querySelector('[data-case="0:0"]');
|
|||
|
|
expect(vide.querySelector('text').textContent).toBe(t('format.vide'));
|
|||
|
|
expect(vide.querySelector('[data-marque^="graphiqueNiveau"]')).toBeNull();
|
|||
|
|
const etroite = await monter(Grille, props({ phrase: 'La grille ne tient pas dans la carte.' }), 60);
|
|||
|
|
expect(etroite.querySelector('svg')).toBeNull();
|
|||
|
|
expect(etroite.textContent).toContain('La grille ne tient pas dans la carte.');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('graphiques : le jumeau (§ 12.7, D44)', () => {
|
|||
|
|
const TABLEAU = {
|
|||
|
|
nom: 'Profil des rencontres',
|
|||
|
|
colonnes: COLONNES_PROFIL,
|
|||
|
|
lignes: [
|
|||
|
|
{ rang: 1, nom: 'Iris Ombrelle', rencontres: 24, part: { texte: '88 %', compte: '24 sur 27' } },
|
|||
|
|
{ rang: 2, nom: 'Théo Pervenche', rencontres: 0, part: { texte: null, compte: '0 sur 0' } },
|
|||
|
|
],
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
test('rouge : ouvert au clavier, refermé au clavier, le focus revient à la commande, jamais sur body', async () => {
|
|||
|
|
const cible = await monter(Jumeau, { tableaux: [TABLEAU] });
|
|||
|
|
const avant = document.createElement('button');
|
|||
|
|
avant.textContent = 'avant';
|
|||
|
|
cible.prepend(avant);
|
|||
|
|
avant.focus();
|
|||
|
|
await touche('Tab');
|
|||
|
|
const commande = document.activeElement;
|
|||
|
|
expect(commande.getAttribute('data-commande')).toBe('ouvrirJumeau');
|
|||
|
|
expect(commande.getAttribute('aria-expanded')).toBe('false');
|
|||
|
|
expect(cible.querySelector('[data-jumeau]')).toBeNull();
|
|||
|
|
await touche('Enter');
|
|||
|
|
flushSync();
|
|||
|
|
await tick();
|
|||
|
|
expect(commande.getAttribute('aria-expanded')).toBe('true');
|
|||
|
|
expect(commande.getAttribute('data-commande')).toBe('fermerJumeau');
|
|||
|
|
expect(cible.querySelector('table[data-jumeau]')).not.toBeNull();
|
|||
|
|
await touche('Enter');
|
|||
|
|
flushSync();
|
|||
|
|
await tick();
|
|||
|
|
expect(cible.querySelector('[data-jumeau]')).toBeNull();
|
|||
|
|
expect(document.activeElement).toBe(commande);
|
|||
|
|
expect(document.activeElement).not.toBe(document.body);
|
|||
|
|
expect(commande.getAttribute('aria-expanded')).toBe('false');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rouge : chaque cellule de pourcentage est suivie du compte de sa ligne ; « — » sans valeur', async () => {
|
|||
|
|
const cible = await monter(Jumeau, { tableaux: [TABLEAU], ouvert: true });
|
|||
|
|
const cellules = cible.querySelectorAll('td[data-colonne="part"]');
|
|||
|
|
expect(cellules).toHaveLength(2);
|
|||
|
|
const lus = [...cellules].map((c) => [c.querySelector('[data-pourcentage]').textContent, c.querySelector('[data-pourcentage] + [data-compte-pire]').textContent]);
|
|||
|
|
expect(lus).toEqual([
|
|||
|
|
['88 %', '24 sur 27'],
|
|||
|
|
[t('format.vide'), '0 sur 0'],
|
|||
|
|
]);
|
|||
|
|
expect(cible.querySelector('th[scope="row"]').textContent).toBe('1');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rouge : une cellule de nombre s’écrit par format.nombre, aux décimales de sa colonne', async () => {
|
|||
|
|
const tableau = {
|
|||
|
|
nom: 'Course',
|
|||
|
|
colonnes: [
|
|||
|
|
{ cle: 'nom', libelle: 'Personne', genre: 'texte' },
|
|||
|
|
{ cle: 'mouvements', libelle: 'Mouvements', genre: 'nombre' },
|
|||
|
|
{ cle: 'taux', libelle: 'Taux de diversité', genre: 'nombre', decimales: 2 },
|
|||
|
|
],
|
|||
|
|
lignes: [{ nom: 'Basile Tourteau', mouvements: 20000, taux: 0.75 }],
|
|||
|
|
};
|
|||
|
|
const cible = await monter(Jumeau, { tableaux: [tableau], ouvert: true });
|
|||
|
|
expect(cible.querySelector('td[data-colonne="mouvements"]').textContent.trim()).toBe('20\u{202F}000');
|
|||
|
|
expect(cible.querySelector('td[data-colonne="taux"]').textContent.trim()).toBe('0,75');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('graphiques : le pourcentage et son compte (D34, contrainte 44)', () => {
|
|||
|
|
test('rouge : le compte se rend juste sous le pourcentage ; « — » quand le pourcentage n’a pas de valeur', async () => {
|
|||
|
|
const cible = await monter(Pourcentage, { texte: '88 %', compte: 'minimum 24 sur 27' });
|
|||
|
|
const valeur = cible.querySelector('[data-pourcentage]');
|
|||
|
|
const compte = cible.querySelector('[data-pourcentage] + [data-compte-pire]');
|
|||
|
|
expect(valeur.textContent).toBe('88 %');
|
|||
|
|
expect(compte.textContent).toBe('minimum 24 sur 27');
|
|||
|
|
expect(compte.getBoundingClientRect().top).toBeGreaterThanOrEqual(valeur.getBoundingClientRect().bottom - 0.5);
|
|||
|
|
const vide = await monter(Pourcentage, { texte: null, compte: '0 sur 0' });
|
|||
|
|
expect(vide.querySelector('[data-pourcentage]').textContent).toBe(t('format.vide'));
|
|||
|
|
expect(vide.querySelector('[data-compte-pire]').textContent).toBe('0 sur 0');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('graphiques : les marques mesurées dans les deux thèmes (§ 14.5)', () => {
|
|||
|
|
test('chaque marque de graphique rendue est au registre, peinte de son jeton, à 3:1 de ce qui l’entoure', async () => {
|
|||
|
|
const racine = document.createElement('div');
|
|||
|
|
racine.style.background = 'var(--couleur-carte-fond)';
|
|||
|
|
const montages = [
|
|||
|
|
await monter(Escalier, propsProfil(12)),
|
|||
|
|
await monter(Barres, {
|
|||
|
|
graphique: 'collisions',
|
|||
|
|
nom: 'Collisions',
|
|||
|
|
synthese: 'Deux.',
|
|||
|
|
lignes: [{ id: 'a', nom: 'Atelier Ressac', appartenance: 1, valeurs: [2, 1] }],
|
|||
|
|
segments: [
|
|||
|
|
{ genre: 'prouve', libelle: 'prouvé inévitable' },
|
|||
|
|
{ genre: 'reste', libelle: 'reste' },
|
|||
|
|
],
|
|||
|
|
}),
|
|||
|
|
await monter(Grille, {
|
|||
|
|
graphique: 'grille',
|
|||
|
|
nom: 'Grille',
|
|||
|
|
synthese: 'Quatre niveaux.',
|
|||
|
|
lignes: [{ libelle: 'Table 1' }, { libelle: 'Table 2' }],
|
|||
|
|
colonnes: [{ libelle: 'Tour 1' }, { libelle: 'Tour 2' }],
|
|||
|
|
valeurs: [0, 1, 2, 3],
|
|||
|
|
}),
|
|||
|
|
];
|
|||
|
|
for (const m of montages) racine.append(m);
|
|||
|
|
document.body.prepend(racine);
|
|||
|
|
try {
|
|||
|
|
for (const theme of ['clair', 'sombre']) {
|
|||
|
|
const releve = await sousTheme(theme, () => releverMarques(racine));
|
|||
|
|
console.log(`contrastes ${theme} : ${releve.ratios.map((r) => `${r.marque} ${r.ratio.toFixed(2)}`).join(', ')}`);
|
|||
|
|
expect(releve.fautes, `${theme} :\n${releve.fautes.join('\n')}`).toEqual([]);
|
|||
|
|
expect(releve.attenuations, theme).toEqual([]);
|
|||
|
|
expect(releve.mesures, theme).toBeGreaterThan(0);
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
racine.remove();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|