gestion_table_tournante_libre/test/navigateur/rendu.navigateur.test.js
Mathieu Benoit f76c5dede5 [ADD] tests: browser tooling — reachable entries, real colours, contrast
The interface tests must judge what an operator sees, not what the markup
claims: a class can sit on an element that paints nothing (§ 14.2).
test/navigateur/ gives them shared helpers: entering the app as a user
does, reading the colour actually painted under a text — the topmost
opaque background below it, an outer <svg> background included — and
deciding visibility by geometry, style and occlusion rather than by
hit-testing, so overlays in pointer-events: none stay visible. The header
names what the helpers do not detect.
Checked: 1776 node and 127 browser tests.

--- FR ---

[ADD] épreuves : outillage du navigateur — entrées, couleurs, contraste

Les épreuves de l'interface doivent juger ce que voit l'opérateur, non ce
que prétend le balisage : une classe peut se poser sur un élément qui ne
peint rien (§ 14.2). test/navigateur/ leur donne des aides communes :
entrer dans l'application comme un utilisateur, lire la couleur réellement
peinte sous un texte — le premier fond opaque sous lui, celui d'un <svg>
extérieur compris — et juger la visibilité par la géométrie, le style et
l'occultation plutôt que par l'atteinte du pointeur. L'en-tête nomme ce
que les aides ne détectent pas.
Vérifié : 1776 node et 127 navigateur.

Assisted-by: Claude Opus 5.5
2026-10-07 04:13:34 -04:00

648 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Épreuves des aides de rendu (§ 14.2, § 14.5) dans un vrai moteur de rendu.
// Chaque épreuve monte un petit fragment HTML ou SVG aux couleurs inventées,
// lit ce que l'aide en rend et le confronte à ce que le fragment déclare. Le
// projet navigateur emploie expect, test et describe de Vitest.
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import {
ascendance,
attenuations,
composantes,
contraste,
couleursPeintes,
couleurTexte,
designation,
fondPeintSous,
porteursDeTexte,
sousTheme,
visible,
} from './rendu.js';
import { contraste as contrastePalette } from '../../src/interface/palette.js';
const SVG = 'http://www.w3.org/2000/svg';
let scene;
let feuille;
beforeEach(() => {
scene = document.createElement('div');
scene.setAttribute('data-scene', '');
document.body.append(scene);
});
afterEach(() => {
scene.remove();
feuille?.remove();
feuille = undefined;
});
// Monte le fragment dans la scène et rend la scène.
function monter(html) {
scene.innerHTML = html;
return scene;
}
// Pose une feuille de style dans <head> : un pseudo-élément ne se déclare
// que par une règle.
function regles(texte) {
feuille = document.createElement('style');
feuille.textContent = texte;
document.head.append(feuille);
}
const $ = (selecteur) => scene.querySelector(selecteur);
describe('composantes', () => {
test('lit rgb(…) et rgba(…)', () => {
expect(composantes('rgb(12, 34, 56)')).toEqual([12, 34, 56, 1]);
expect(composantes('rgba(12, 34, 56, 0.5)')).toEqual([12, 34, 56, 0.5]);
expect(composantes('rgba(0, 0, 0, 0)')).toEqual([0, 0, 0, 0]);
});
test('lève sur une forme inconnue', () => {
for (const forme of ['hsl(0 0% 0%)', 'none', 'url("#motif")', 'rouge', '']) {
expect(() => composantes(forme), forme).toThrow(/couleur/);
}
});
test('contraste est celui de palette.js', () => {
expect(contraste).toBe(contrastePalette);
});
});
describe('ascendance et designation', () => {
test("l'ascendance d'un élément SVG remonte jusqu'à <html>", () => {
monter(`<svg xmlns="${SVG}"><g><text x="5" y="15">Iris</text></g></svg>`);
const chaine = ascendance($('text'));
expect(chaine.map((n) => n.localName).slice(0, 4)).toEqual(['text', 'g', 'svg', 'div']);
expect(chaine.at(-1)).toBe(document.documentElement);
});
test('designation nomme la balise, les attributs data-… et les classes', () => {
monter('<span class="nom gras" data-participant="7" data-actif>Théo</span><b>x</b>');
expect(designation($('span'))).toBe('span[data-participant="7"][data-actif].nom.gras');
expect(designation($('b'))).toBe('b');
expect(designation({ element: $('span'), pseudo: '::before' })).toBe(
'span[data-participant="7"][data-actif].nom.gras::before',
);
});
});
describe('porteursDeTexte', () => {
test('rend les éléments à texte non vide, HTML et SVG, dans leur ordre', () => {
monter(
`<p>Ondine <em>Lacasse</em></p><div> <span> </span></div>` +
`<svg xmlns="${SVG}"><title>bulle</title><text x="0" y="10">Grisaille</text></svg>`,
);
const porteurs = porteursDeTexte(scene);
expect(porteurs.length).toBe(3);
expect(porteurs.map((p) => p.element.localName)).toEqual(['p', 'em', 'text']);
expect(porteurs.every((p) => p.pseudo === null)).toBe(true);
});
test('rend un ::before à content comme porteur, pas un ::after à content vide', () => {
regles(
'[data-scene] .epreuve-marque::before { content: "→ "; }' +
'[data-scene] .epreuve-marque::after { content: ""; }',
);
monter('<span class="epreuve-marque"></span>');
expect(porteursDeTexte(scene)).toEqual([{ element: $('span'), pseudo: '::before' }]);
});
test("un pseudo-élément dont content n'est qu'un blanc n'est pas un porteur", () => {
regles(
'[data-scene] .epreuve-blanc::before { content: " "; }' +
'[data-scene] .epreuve-blanc::after { content: "\\a0"; }',
);
monter('<span class="epreuve-blanc">a</span>');
expect(porteursDeTexte(scene)).toEqual([{ element: $('span'), pseudo: null }]);
});
});
describe('couleurTexte', () => {
test("lit color d'un élément HTML et fill d'un <text> SVG", () => {
monter(
`<span style="color: rgb(40, 50, 60)">Iris</span>` +
`<svg xmlns="${SVG}"><text x="0" y="10" fill="rgb(70, 80, 90)" style="color: rgb(1, 1, 1)">Ombrelle</text></svg>`,
);
expect(couleurTexte({ element: $('span'), pseudo: null })).toEqual([40, 50, 60, 1]);
expect(couleurTexte({ element: $('text'), pseudo: null })).toEqual([70, 80, 90, 1]);
expect(couleurTexte($('span'))).toEqual([40, 50, 60, 1]);
});
});
describe('couleursPeintes', () => {
test('un motif de hachure rend les deux couleurs que peignent ses enfants', () => {
monter(
`<svg xmlns="${SVG}" width="80" height="80"><defs>` +
`<pattern id="epreuve-hachure" width="8" height="8" patternUnits="userSpaceOnUse">` +
`<rect width="8" height="8" fill="rgb(250, 240, 200)"/>` +
`<path d="M0 8 L8 0" fill="none" stroke="rgb(20, 30, 90)"/>` +
`</pattern></defs>` +
`<rect data-forme width="80" height="80" fill="url(#epreuve-hachure)" stroke="rgb(1, 2, 3)"/></svg>`,
);
expect(couleursPeintes($('[data-forme]'))).toEqual({
fill: [
[250, 240, 200, 1],
[20, 30, 90, 1],
],
stroke: [[1, 2, 3, 1]],
});
});
test('un remplissage plein rend sa couleur, none et transparent ne rendent rien', () => {
monter(
`<svg xmlns="${SVG}"><circle data-a r="5" cx="5" cy="5" fill="rgb(9, 8, 7)"/>` +
`<circle data-b r="5" cx="5" cy="5" fill="none" stroke="transparent"/></svg>`,
);
expect(couleursPeintes($('[data-a]'))).toEqual({ fill: [[9, 8, 7, 1]], stroke: [] });
expect(couleursPeintes($('[data-b]'))).toEqual({ fill: [], stroke: [] });
});
test('une hachure groupée dans un <g> : les couleurs de ses formes, pas le noir du groupe', () => {
monter(
`<svg xmlns="${SVG}" width="80" height="80"><defs>` +
`<pattern id="epreuve-hachure-groupee" width="8" height="8" patternUnits="userSpaceOnUse"><g>` +
`<rect width="8" height="8" fill="rgb(250, 240, 200)"/>` +
`<path d="M0 8 L8 0" fill="none" stroke="rgb(20, 30, 90)"/>` +
`<rect width="2" height="2" fill="rgb(7, 7, 7)" style="display: none"/>` +
`</g></pattern></defs>` +
`<rect data-forme width="80" height="80" fill="url(#epreuve-hachure-groupee)"/></svg>`,
);
expect(couleursPeintes($('[data-forme]')).fill).toEqual([
[250, 240, 200, 1],
[20, 30, 90, 1],
]);
});
});
describe('fondPeintSous', () => {
test("un cercle posé sur un rectangle rempli : le remplissage du rectangle", () => {
monter(
`<div style="background: rgb(255, 255, 255)"><svg xmlns="${SVG}" width="200" height="100">` +
`<rect width="200" height="100" fill="rgb(200, 220, 240)"/>` +
`<circle data-pastille cx="100" cy="50" r="20" fill="rgb(10, 10, 10)"/></svg></div>`,
);
expect(fondPeintSous($('[data-pastille]'))).toEqual([[200, 220, 240, 1]]);
});
test('un texte SVG sur un rectangle hachuré : chaque couleur du motif', () => {
monter(
`<svg xmlns="${SVG}" width="200" height="100"><defs>` +
`<pattern id="epreuve-hachure-fond" width="8" height="8" patternUnits="userSpaceOnUse">` +
`<rect width="8" height="8" fill="rgb(250, 240, 200)"/>` +
`<path d="M0 8 L8 0" fill="none" stroke="rgb(20, 30, 90)"/></pattern></defs>` +
`<rect x="10" y="10" width="180" height="80" fill="url(#epreuve-hachure-fond)"/>` +
`<text x="40" y="55" fill="rgb(0, 0, 0)">Pervenche</text></svg>`,
);
expect(fondPeintSous({ element: $('text'), pseudo: null })).toEqual([
[250, 240, 200, 1],
[20, 30, 90, 1],
]);
});
test('seules comptent les formes qui précèdent la cible : un rectangle peint après elle ne la porte pas', () => {
monter(
`<svg xmlns="${SVG}" width="200" height="100">` +
`<rect data-a width="200" height="100" fill="rgb(200, 220, 240)"/>` +
`<circle data-pastille cx="100" cy="50" r="20" fill="rgb(10, 10, 10)"/>` +
`<rect data-b width="200" height="100" fill="rgb(120, 40, 60)"/></svg>`,
);
expect(fondPeintSous($('[data-pastille]'))).toEqual([[200, 220, 240, 1]]);
});
// Chaque cas rend translucide la forme posée sur le rectangle noir ; la
// couleur peinte n'est plus la couleur calculée, et l'aide lève en nommant
// la forme plutôt que de rendre un blanc que l'écran ne montre pas.
const TRANSLUCIDES = [
['fill-opacity', 'fill="rgb(255, 255, 255)" fill-opacity="0.2"', '', /fill-opacity/],
['opacity', 'fill="rgb(255, 255, 255)" opacity="0.2"', '', /opacity/],
['un alpha de couleur', 'fill="rgba(255, 255, 255, 0.2)"', '', /fill/],
["l'opacity d'un groupe", 'fill="rgb(255, 255, 255)"', 'opacity="0.2"', /opacity/],
];
for (const [nom, attributs, groupe, attendu] of TRANSLUCIDES) {
test(`une forme translucide (${nom}) sur une forme opaque : lève en nommant la forme`, () => {
monter(
`<svg xmlns="${SVG}" width="200" height="100">` +
`<rect width="200" height="100" fill="rgb(0, 0, 0)"/>` +
`<g ${groupe}><rect data-voile width="200" height="100" ${attributs}/></g>` +
`<text x="60" y="55" fill="rgb(0, 0, 0)">Iris</text></svg>`,
);
expect(() => fondPeintSous($('text'))).toThrow(attendu);
expect(() => fondPeintSous($('text'))).toThrow(/rect\[data-voile\]/);
});
}
test("un motif dont une forme est translucide, un dégradé à arrêt translucide : lève en les nommant", () => {
monter(
`<svg xmlns="${SVG}" width="200" height="200"><defs>` +
`<pattern id="epreuve-motif-voile" width="8" height="8" patternUnits="userSpaceOnUse">` +
`<rect data-maille width="8" height="8" fill="rgb(250, 240, 200)" fill-opacity="0.5"/></pattern>` +
`<linearGradient id="epreuve-degrade-voile"><stop offset="0" stop-color="rgb(250, 240, 200)"/>` +
`<stop offset="1" stop-color="rgb(20, 30, 90)" stop-opacity="0.5"/></linearGradient></defs>` +
`<rect width="200" height="100" fill="url(#epreuve-motif-voile)"/>` +
`<rect y="100" width="200" height="100" fill="url(#epreuve-degrade-voile)"/>` +
`<text data-haut x="60" y="55" fill="rgb(0, 0, 0)">Iris</text>` +
`<text data-bas x="60" y="155" fill="rgb(0, 0, 0)">Théo</text></svg>`,
);
expect(() => fondPeintSous($('[data-haut]'))).toThrow(/rect\[data-maille\] fill-opacity: 0\.5/);
expect(() => fondPeintSous($('[data-bas]'))).toThrow(/stop stop-opacity: 0\.5/);
});
test('un élément SVG sans forme dessous : le premier fond HTML opaque', () => {
monter(
`<div style="background: rgb(230, 240, 250)"><div><svg xmlns="${SVG}" width="200" height="100">` +
`<rect width="40" height="40" fill="rgb(1, 1, 1)"/>` +
`<text x="100" y="60">Grisaille</text></svg></div></div>`,
);
expect(fondPeintSous($('text'))).toEqual([[230, 240, 250, 1]]);
});
test("un texte HTML sans fond : le premier fond opaque de son ascendance", () => {
monter(
'<div style="background: rgb(250, 250, 240)"><p style="background: rgba(0, 0, 0, 0.5)">' +
'<span>Club des Merles</span></p></div>',
);
expect(fondPeintSous($('span'))).toEqual([[250, 250, 240, 1]]);
});
test('un texte HTML posé sur un rect SVG frère : le remplissage du rect, pas le fond de la surface', () => {
monter(
'<div style="background: rgb(255, 255, 255); position: relative; height: 100px">' +
`<svg xmlns="${SVG}" width="200" height="100" style="display: block"><rect width="200" height="100" fill="rgb(0, 0, 0)"/></svg>` +
'<span style="position: absolute; left: 10px; top: 10px">Iris</span></div>',
);
expect(fondPeintSous($('span'))).toEqual([[0, 0, 0, 1]]);
});
test('un texte HTML sur un div frère absolu : le fond du frère, même en pointer-events: none', () => {
monter(
'<div style="background: rgb(255, 255, 255); position: relative; height: 100px">' +
'<div data-plateau style="position: absolute; inset: 0; background: rgb(0, 0, 0); pointer-events: none"></div>' +
'<span style="position: absolute; left: 10px; top: 10px; pointer-events: none">Iris</span></div>',
);
expect(fondPeintSous($('span'))).toEqual([[0, 0, 0, 1]]);
});
test('un texte HTML sur un frère opaque mais à opacity 0,2 : lève en nommant le frère', () => {
monter(
'<div style="background: rgb(255, 255, 255); position: relative; height: 100px">' +
'<div data-plateau style="position: absolute; inset: 0; background: rgb(0, 0, 0); opacity: 0.2"></div>' +
'<span style="position: absolute; left: 10px; top: 10px">Iris</span></div>',
);
expect(() => fondPeintSous($('span'))).toThrow(/div\[data-plateau\].*opacity/);
});
test('un texte HTML hors de la fenêtre : lève, rien ne se lit sous un point hors champ', () => {
monter('<span style="position: fixed; left: -500px; top: 0">Iris</span>');
expect(() => fondPeintSous($('span'))).toThrow(/fenêtre/);
});
test("le fond CSS du <svg> extérieur, sous un texte SVG sans forme dessous", () => {
monter(
`<div style="background: rgb(255, 255, 255)"><svg xmlns="${SVG}" width="200" height="100" ` +
`style="display: block; background: rgb(0, 0, 0)"><text x="60" y="55" fill="rgb(0, 0, 0)">Iris</text></svg></div>`,
);
expect(fondPeintSous($('text'))).toEqual([[0, 0, 0, 1]]);
});
test("un recouvrement peint après un texte HTML, en pointer-events: none, n'est pas dessous", () => {
monter(
'<div style="background: rgb(255, 255, 255); position: relative; height: 100px">' +
'<span style="position: absolute; left: 10px; top: 10px">Iris</span>' +
'<div data-voile style="position: absolute; inset: 0; background: rgb(0, 0, 0); pointer-events: none"></div></div>',
);
expect(fondPeintSous($('span'))).toEqual([[255, 255, 255, 1]]);
});
test("un recouvrement HTML peint après le dessin n'est pas sous un texte SVG", () => {
monter(
'<div style="background: rgb(255, 255, 255); position: relative; height: 100px">' +
`<svg xmlns="${SVG}" width="200" height="100" style="display: block"><text x="60" y="55">Iris</text></svg>` +
'<div data-voile style="position: absolute; inset: 0; background: rgb(0, 0, 0); pointer-events: none"></div></div>',
);
expect(fondPeintSous($('text'))).toEqual([[255, 255, 255, 1]]);
});
test("un descendant à fond opaque qui couvre le centre d'un texte HTML n'est pas dessous", () => {
monter(
'<div style="background: rgb(255, 255, 255)"><p style="position: relative">Iris' +
'<b style="position: absolute; inset: 0; background: rgb(0, 0, 0)"></b></p></div>',
);
expect(fondPeintSous($('p'))).toEqual([[255, 255, 255, 1]]);
});
test("une forme peinte après un texte SVG sans forme dessous n'est pas dessous", () => {
monter(
`<div style="background: rgb(255, 255, 255)"><svg xmlns="${SVG}" width="200" height="100" style="display: block">` +
`<text x="60" y="55">Iris</text><rect width="200" height="100" fill="rgb(0, 0, 0)"/></svg></div>`,
);
expect(fondPeintSous($('text'))).toEqual([[255, 255, 255, 1]]);
});
test("un texte que son centre n'atteint pas : lève en le nommant", () => {
monter('<div style="background: rgb(255, 255, 255)"><span data-cache style="visibility: hidden">Iris</span></div>');
expect(() => fondPeintSous($('span'))).toThrow(/span\[data-cache\].*n'atteint pas/);
});
// Chaque cas pose, entre le rectangle clair et le texte, une forme qui ne
// peint rien au centre : l'aide la saute et rend le rectangle clair.
const SANS_PEINTURE = [
['opacity 0', '<rect width="200" height="100" fill="rgb(0, 0, 0)" opacity="0"/>'],
['fill-opacity 0', '<rect width="200" height="100" fill="rgb(0, 0, 0)" fill-opacity="0"/>'],
['visibility hidden', '<rect width="200" height="100" fill="rgb(0, 0, 0)" visibility="hidden"/>'],
['maille d’un <pattern>', '<pattern id="epreuve-maille" width="200" height="100" patternUnits="userSpaceOnUse"><rect width="200" height="100" fill="rgb(0, 0, 0)"/></pattern>'],
];
for (const [nom, forme] of SANS_PEINTURE) {
test(`une forme qui ne peint rien au centre (${nom}) n'est pas dessous`, () => {
monter(
`<svg xmlns="${SVG}" width="200" height="100"><rect width="200" height="100" fill="rgb(200, 220, 240)"/>` +
`${forme}<text x="60" y="55" fill="rgb(0, 0, 0)">Iris</text></svg>`,
);
expect(fondPeintSous($('text'))).toEqual([[200, 220, 240, 1]]);
});
}
test("un frère HTML à opacity 0 sous un texte HTML ne peint rien", () => {
monter(
'<div style="background: rgb(255, 255, 255); position: relative; height: 100px">' +
'<div style="position: absolute; inset: 0; background: rgb(0, 0, 0); opacity: 0"></div>' +
'<span style="position: absolute; left: 10px; top: 10px">Iris</span></div>',
);
expect(fondPeintSous($('span'))).toEqual([[255, 255, 255, 1]]);
});
test('une forme peinte par <use> sous un texte SVG : lève en nommant le <use>', () => {
monter(
`<div style="background: rgb(255, 255, 255)"><svg xmlns="${SVG}" width="200" height="100">` +
`<defs><rect id="epreuve-plateau" width="200" height="100" fill="rgb(0, 0, 0)"/></defs>` +
`<use data-plateau href="#epreuve-plateau"/><text x="60" y="55" fill="rgb(255, 255, 255)">Iris</text></svg></div>`,
);
expect(() => fondPeintSous($('text'))).toThrow(/use\[data-plateau\]/);
});
test('un texte HTML posé sur un <use> : lève en nommant le <use>', () => {
monter(
'<div style="background: rgb(255, 255, 255); position: relative; height: 100px">' +
`<svg xmlns="${SVG}" width="200" height="100" style="display: block">` +
`<defs><rect id="epreuve-plateau-html" width="200" height="100" fill="rgb(0, 0, 0)"/></defs>` +
`<use data-plateau href="#epreuve-plateau-html"/></svg>` +
'<span style="position: absolute; left: 10px; top: 10px">Iris</span></div>',
);
expect(() => fondPeintSous($('span'))).toThrow(/use\[data-plateau\]/);
});
test('un dégradé et un motif qui héritent leurs arrêts ou leurs formes par href', () => {
monter(
`<svg xmlns="${SVG}" width="200" height="200"><defs>` +
`<linearGradient id="epreuve-g0"><stop stop-color="rgb(0, 0, 0)"/><stop offset="1" stop-color="rgb(10, 10, 10)"/></linearGradient>` +
`<linearGradient id="epreuve-g1" href="#epreuve-g0"/>` +
`<pattern id="epreuve-p0" width="8" height="8" patternUnits="userSpaceOnUse"><rect width="8" height="8" fill="rgb(20, 20, 20)"/></pattern>` +
`<pattern id="epreuve-p1" href="#epreuve-p0"/></defs>` +
`<rect width="200" height="200" fill="rgb(255, 255, 255)"/>` +
`<rect width="200" height="100" fill="url(#epreuve-g1)"/>` +
`<rect y="100" width="200" height="100" fill="url(#epreuve-p1)"/>` +
`<text data-haut x="60" y="55">Iris</text><text data-bas x="60" y="155">Théo</text></svg>`,
);
expect(fondPeintSous($('[data-haut]'))).toEqual([
[0, 0, 0, 1],
[10, 10, 10, 1],
]);
expect(fondPeintSous($('[data-bas]'))).toEqual([[20, 20, 20, 1]]);
});
test('une image de fond sur le chemin du fond retenu, ou sur la cible : lève en nommant son porteur', () => {
monter(
'<div style="background: rgb(255, 255, 255)"><p data-degrade style="background-image: linear-gradient(rgb(0, 0, 0), rgb(0, 0, 0))">' +
'<span>Iris</span></p>' +
'<button style="background: rgb(30, 60, 90) linear-gradient(rgb(0, 0, 0), rgb(0, 0, 0))">Retenir</button></div>',
);
expect(() => fondPeintSous($('span'))).toThrow(/p\[data-degrade\].*background-image/);
expect(() => fondPeintSous($('button'))).toThrow(/button.*background-image/);
});
test('un bouton à fond opaque : son propre fond', () => {
monter(
'<div style="background: rgb(250, 250, 240)">' +
'<button style="background: rgb(30, 60, 90); color: rgb(255, 255, 255)">Retenir</button></div>',
);
expect(fondPeintSous($('button'))).toEqual([[30, 60, 90, 1]]);
});
test("sans aucun fond opaque, la liste est vide", async () => {
// L'épreuve retire le fond de <html> et de <body> le temps de la mesure.
const avant = [document.documentElement.style.background, document.body.style.background];
document.documentElement.style.background = 'transparent';
document.body.style.background = 'transparent';
try {
monter('<span>Iris</span>');
expect(fondPeintSous($('span'))).toEqual([]);
} finally {
[document.documentElement.style.background, document.body.style.background] = avant;
}
});
});
describe('pseudo-éléments', () => {
test('couleurTexte, fondPeintSous et attenuations lisent le ::before, pas son hôte', () => {
regles(
'[data-scene] .epreuve-avant::before { content: "★"; color: rgb(10, 20, 30); opacity: 0.6; }' +
'[data-scene] .epreuve-propre::before { content: "☆"; color: rgb(10, 20, 30); background: rgb(90, 80, 70); }',
);
monter(
'<span class="epreuve-avant" style="background: rgb(240, 230, 220); color: rgb(5, 5, 5)"></span>' +
'<span class="epreuve-propre" style="background: rgb(240, 230, 220)"></span>',
);
const [avant, propre] = porteursDeTexte(scene);
expect(avant).toEqual({ element: $('.epreuve-avant'), pseudo: '::before' });
expect(couleurTexte(avant)).toEqual([10, 20, 30, 1]);
expect(fondPeintSous(avant)).toEqual([[240, 230, 220, 1]]);
expect(fondPeintSous(propre)).toEqual([[90, 80, 70, 1]]);
expect(attenuations(avant)).toEqual([
{ element: $('.epreuve-avant'), pseudo: '::before', propriete: 'opacity', valeur: '0.6' },
]);
expect(attenuations(propre)).toEqual([]);
});
});
describe('attenuations', () => {
test('un texte sans atténuation : aucune faute', () => {
monter('<p><span>Ondine</span></p>');
expect(attenuations($('span'))).toEqual([]);
});
// Chaque cas pose une propriété sur un ancêtre ou sur le texte, et attend
// la faute sur l'élément qui la porte.
const CAS = [
['opacity', 'opacity: 0.6', '0.6'],
['filter', 'filter: grayscale(1)', 'grayscale(1)'],
['backdrop-filter', 'backdrop-filter: blur(2px)', 'blur(2px)'],
['mask-image', 'mask-image: linear-gradient(black, black)', 'linear-gradient(rgb(0, 0, 0), rgb(0, 0, 0))'],
['mix-blend-mode', 'mix-blend-mode: multiply', 'multiply'],
];
for (const [propriete, declaration, valeur] of CAS) {
test(`relève ${propriete} posé sur un ancêtre`, () => {
monter(`<section style="${declaration}"><p><span>Iris</span></p></section>`);
expect(attenuations($('span'))).toEqual([
{ element: $('section'), pseudo: null, propriete, valeur },
]);
});
}
test("relève un fill-opacity de 0,5 sur le groupe d'un texte SVG, et sur ce qui en hérite", () => {
monter(`<svg xmlns="${SVG}"><g fill-opacity="0.5"><text x="0" y="10">Théo</text></g></svg>`);
const fautes = attenuations($('text'));
expect(fautes).toContainEqual({ element: $('g'), pseudo: null, propriete: 'fill-opacity', valeur: '0.5' });
expect(fautes.every((f) => f.propriete === 'fill-opacity')).toBe(true);
});
test('relève un stroke-opacity', () => {
monter(`<svg xmlns="${SVG}"><circle cx="5" cy="5" r="4" stroke="rgb(0, 0, 0)" stroke-opacity="0.4"/></svg>`);
expect(attenuations($('circle'))).toContainEqual({
element: $('circle'),
pseudo: null,
propriete: 'stroke-opacity',
valeur: '0.4',
});
});
});
describe('visible', () => {
test('un élément visible, puis caché, recouvert, hors de la fenêtre', () => {
monter(
'<div style="position: relative; width: 200px; height: 40px">' +
'<span data-a>Iris</span>' +
'<span data-b style="display: none">Théo</span>' +
'<span data-c style="position: absolute; left: 120px; top: 0">Ondine</span>' +
'<span data-d style="position: absolute; left: 0; top: 0; width: 200px; height: 40px; background: rgb(1, 1, 1)"></span>' +
'<span data-e style="position: fixed; left: -500px; top: 0">Grisaille</span>' +
'</div>',
);
expect(visible($('[data-a]'))).toMatch(/centre/);
expect(visible($('[data-b]'))).toMatch(/checkVisibility/);
expect(visible($('[data-c]'))).toMatch(/centre/);
expect(visible($('[data-e]'))).toMatch(/fenêtre/);
$('[data-d]').remove();
expect(visible($('[data-a]'))).toBe(true);
expect(visible($('[data-c]'))).toBe(true);
});
test('un élément en pointer-events: none se voit, comme la pastille du plan', () => {
monter(
`<svg xmlns="${SVG}" width="200" height="100"><rect width="200" height="100" fill="rgb(200, 220, 240)"/>` +
`<text data-pastille x="60" y="55" style="pointer-events: none">3</text></svg>`,
);
expect(visible($('text'))).toBe(true);
});
test("un recouvrement opaque en pointer-events: none cache le texte dessous, et se nomme", () => {
monter(
'<div style="position: relative; width: 200px; height: 40px">' +
'<span>Iris</span>' +
'<div data-fantome style="position: absolute; inset: 0; background: rgb(1, 1, 1); pointer-events: none"></div></div>',
);
expect(visible($('span'))).toMatch(/couvert par div\[data-fantome\]/);
});
// Recouvrements qui ne peignent rien au centre : ils ne cachent pas le texte.
const TRANSPARENTS = [
['une boîte HTML sans fond ni texte', '<div style="position: absolute; inset: 0"></div>'],
['une boîte HTML à opacity 0', '<div style="position: absolute; inset: 0; background: rgb(1, 1, 1); opacity: 0"></div>'],
[
'une zone d’atteinte SVG à fill-opacity 0',
`<svg xmlns="${SVG}" width="200" height="40" style="position: absolute; left: 0; top: 0"><rect width="200" height="40" fill="rgb(1, 1, 1)" fill-opacity="0"/></svg>`,
],
];
for (const [nom, recouvrement] of TRANSPARENTS) {
test(`${nom} posée sur le texte ne le cache pas`, () => {
monter(`<div style="position: relative; width: 200px; height: 40px"><span>Iris</span>${recouvrement}</div>`);
expect(visible($('span'))).toBe(true);
});
}
// Recouvrements qui peignent au centre : chacun cache le texte.
const OPAQUES = [
['une boîte HTML qui porte un texte', '<div style="position: absolute; inset: 0">Théo</div>'],
[
'une boîte HTML à image de fond',
'<div style="position: absolute; inset: 0; background-image: linear-gradient(rgb(1, 1, 1), rgb(1, 1, 1))"></div>',
],
['une image sans fond', '<img alt="" style="position: absolute; left: 0; top: 0; width: 200px; height: 40px">'],
[
'une forme SVG remplie',
`<svg xmlns="${SVG}" width="200" height="40" style="position: absolute; left: 0; top: 0"><rect width="200" height="40" fill="rgb(1, 1, 1)"/></svg>`,
],
];
for (const [nom, recouvrement] of OPAQUES) {
test(`${nom} posée sur le texte le cache`, () => {
monter(`<div style="position: relative; width: 200px; height: 40px"><span>Iris</span>${recouvrement}</div>`);
expect(visible($('span'))).toMatch(/couvert/);
});
}
test('une boîte sans fond dont un pseudo-élément porte un texte, posée sur le texte, le cache', () => {
regles('[data-scene] .epreuve-etiquette::before { content: "Théo"; }');
monter(
'<div style="position: relative; width: 200px; height: 40px"><span>Iris</span>' +
'<div class="epreuve-etiquette" style="position: absolute; inset: 0"></div></div>',
);
expect(visible($('span'))).toMatch(/couvert par div\.epreuve-etiquette/);
});
test("un élément rogné par son ancêtre au centre de sa boîte : le centre ne l'atteint pas", () => {
monter(
'<div style="position: relative; width: 100px; height: 40px; overflow: hidden">' +
'<span style="position: absolute; left: 60px; top: 0; width: 100px">Iris</span></div>',
);
expect(visible($('span'))).toMatch(/n'atteint pas/);
});
test("un pseudo-élément : son hôte, son display et son content", () => {
regles(
'[data-scene] .epreuve-vu::before { content: "★"; }' +
'[data-scene] .epreuve-cache::before { content: "★"; display: none; }',
);
monter('<span class="epreuve-vu">Iris</span> <span class="epreuve-cache">Théo</span>');
expect(visible({ element: $('.epreuve-vu'), pseudo: '::before' })).toBe(true);
expect(visible({ element: $('.epreuve-cache'), pseudo: '::before' })).toMatch(/display/);
expect(visible({ element: $('.epreuve-vu'), pseudo: '::after' })).toMatch(/content/);
});
});
describe('sousTheme', () => {
const racine = document.documentElement;
test("pose le thème pendant la mesure, puis rend 'clair' qu'il portait", async () => {
racine.setAttribute('data-theme', 'clair');
try {
const lu = await sousTheme('sombre', () => racine.getAttribute('data-theme'));
expect(lu).toBe('sombre');
expect(racine.getAttribute('data-theme')).toBe('clair');
} finally {
racine.removeAttribute('data-theme');
}
});
test("laisse l'attribut absent quand il l'était", async () => {
racine.removeAttribute('data-theme');
await sousTheme('sombre', () => {
expect(racine.getAttribute('data-theme')).toBe('sombre');
});
expect(racine.hasAttribute('data-theme')).toBe(false);
});
test('rend la valeur d’avant, ou son absence, même quand la mesure lève', async () => {
racine.removeAttribute('data-theme');
await expect(sousTheme('sombre', () => { throw new Error('mesure manquée'); })).rejects.toThrow('mesure manquée');
expect(racine.hasAttribute('data-theme')).toBe(false);
racine.setAttribute('data-theme', 'clair');
try {
await expect(sousTheme('sombre', async () => { throw new Error('mesure manquée'); })).rejects.toThrow('mesure manquée');
expect(racine.getAttribute('data-theme')).toBe('clair');
} finally {
racine.removeAttribute('data-theme');
}
});
});