gestion_table_tournante_libre/test/navigateur/rendu.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

593 lines
28 KiB
JavaScript
Raw Permalink 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)
// Aides de rendu des épreuves du navigateur (§ 14.2, § 14.5). Elles lisent ce
// que le moteur de rendu a résolu — style calculé, boîte, élément qu'atteint
// un point — et jamais la présence d'une classe. Elles ne lisent aucun pixel :
// le contraste se calcule sur les couleurs calculées, qui ne sont les couleurs
// peintes que sans opacité partielle, filtre, masque ni mode de fusion, ce
// qu'attenuations relève sur toute l'ascendance d'un texte.
//
// Une couleur est un tableau [r, g, b, a] : composantes sRGB de 0 à 255,
// alpha de 0 à 1, la forme de palette.js.
//
// Un porteur de texte est { element, pseudo } : un élément qui porte lui-même
// un nœud de texte non vide (pseudo null), ou l'un de ses pseudo-éléments
// ::before et ::after dont content produit un texte. Les aides qui reçoivent
// un porteur acceptent aussi un élément nu, lu comme { element, pseudo: null }.
//
// Ce que les aides ne décèlent pas. Le centre de la boîte tient lieu de
// l'élément : un élément dont ce centre tombe hors de sa peinture — un anneau,
// un trait coudé — est dit hors d'atteinte (« n'atteint pas »). La lecture
// s'ancre à la première entrée d'elementsFromPoint que la cible contient : un
// descendant hissé au-dessus d'un recouvrement l'y ancre, et ce recouvrement
// échappe à visible et se lit comme fond peint dessous. elementsFromPoint ne
// rend jamais un pseudo-élément : sa peinture se lit sur son hôte. Un
// pseudo-élément à texte fait couvrir toute la boîte de l'hôte, un
// pseudo-élément sans texte ne couvre rien, et aucun fond de pseudo-élément
// ne se lit sous le texte d'un autre élément. Une boîte HTML se juge
// entière : son fond, son texte ou son image couvrent tout point de la boîte,
// coin arrondi ou zone sans glyphe compris. isPointInFill et isPointInStroke
// ignorent clip-path : une forme rognée au point y compte comme peinte.
/** Le rapport de contraste des WCAG, seule implémentation du dépôt. */
export { contraste } from '../../src/interface/palette.js';
const PSEUDOS = ['::before', '::after'];
// Éléments dont le texte ne se peint jamais : code, feuille, gabarit, et les
// textes de description d'un SVG, lus par l'accessibilité seule.
const TEXTES_NON_PEINTS = new Set(['script', 'style', 'template', 'noscript', 'title', 'desc', 'metadata']);
// Conteneurs SVG dont les formes ne se peignent qu'à travers une référence :
// une forme qu'ils portent n'est jamais « dessous » un élément du dessin.
const CONTENEURS_DE_REFERENCE = 'defs, pattern, clipPath, mask, marker, symbol, linearGradient, radialGradient';
// Valeurs calculées qui laissent la couleur peinte égale à la couleur résolue.
const NEUTRES = [
['opacity', '1'],
['fill-opacity', '1'],
['stroke-opacity', '1'],
['filter', 'none'],
['backdrop-filter', 'none'],
['mask-image', 'none'],
['mix-blend-mode', 'normal'],
];
/** Composantes [r, g, b, a] d'une couleur calculée. Chromium rend une couleur
* sRGB sous la forme « rgb(r, g, b) » ou « rgba(r, g, b, a) » ; toute autre
* forme lève, plutôt que de laisser passer une épreuve sans mesure. */
export function composantes(couleur) {
const m = /^rgba?\(([\d.]+), ([\d.]+), ([\d.]+)(?:, ([\d.]+))?\)$/.exec(couleur);
if (m === null) {
throw new Error(`couleur calculée illisible : « ${couleur} »`);
}
return [Number(m[1]), Number(m[2]), Number(m[3]), m[4] === undefined ? 1 : Number(m[4])];
}
/** L'élément, puis chacun de ses ancêtres jusqu'à la racine du document ;
* parentElement relie aussi un élément SVG à son parent. */
export function ascendance(element) {
const chaine = [];
for (let n = element; n !== null; n = n.parentElement) {
chaine.push(n);
}
return chaine;
}
// { element, pseudo } d'un porteur ou d'un élément nu.
function porteur(cible) {
return cible instanceof Element ? { element: cible, pseudo: null } : cible;
}
/** Nom lisible d'un élément ou d'un porteur dans un message d'échec : la
* balise, chaque attribut data-… dans l'ordre de l'élément, chaque classe,
* puis le pseudo-élément. */
export function designation(cible) {
const { element, pseudo } = porteur(cible);
const donnees = [...element.attributes]
.filter((a) => a.name.startsWith('data-'))
.map((a) => (a.value === '' ? `[${a.name}]` : `[${a.name}="${a.value}"]`))
.join('');
const classes = [...element.classList].map((c) => `.${c}`).join('');
return `${element.localName}${donnees}${classes}${pseudo ?? ''}`;
}
// Le content calculé d'un pseudo-élément produit-il un texte ? « none » et
// « normal » n'en créent pas ; une chaîne littérale vide ou faite de blancs
// crée une boîte sans texte à lire. Toute autre valeur — compteur, attr(),
// image — compte comme texte.
function contenuTextuel(element, pseudo) {
const contenu = getComputedStyle(element, pseudo).content;
if (contenu === 'none' || contenu === 'normal') return false;
const litteral = /^"((?:[^"\\]|\\.)*)"$/.exec(contenu) ?? /^'((?:[^'\\]|\\.)*)'$/.exec(contenu);
return litteral === null || litteral[1].trim() !== '';
}
// L'élément porte-t-il lui-même un nœud de texte non vide ?
function porteTexte(element) {
return [...element.childNodes].some((n) => n.nodeType === Node.TEXT_NODE && n.textContent.trim() !== '');
}
/** Porteurs de texte du sous-arbre de racine, racine comprise, dans l'ordre
* du document : pour chaque élément HTML, son ::before à content, l'élément
* s'il porte un nœud de texte non vide, puis son ::after à content. Un
* élément SVG ne porte pas de pseudo-élément ; les éléments dont le texte ne
* se peint jamais (script, style, title…) sont écartés. */
export function porteursDeTexte(racine) {
const porteurs = [];
for (const element of [racine, ...racine.querySelectorAll('*')]) {
if (TEXTES_NON_PEINTS.has(element.localName)) continue;
const html = element instanceof HTMLElement;
if (html && contenuTextuel(element, '::before')) porteurs.push({ element, pseudo: '::before' });
if (porteTexte(element)) porteurs.push({ element, pseudo: null });
if (html && contenuTextuel(element, '::after')) porteurs.push({ element, pseudo: '::after' });
}
return porteurs;
}
/** Couleur du texte d'un porteur : color d'un élément HTML ou d'un
* pseudo-élément, fill d'un texte SVG. Un fill qui n'est pas une couleur —
* none, un motif — lève par composantes. */
export function couleurTexte(cible) {
const { element, pseudo } = porteur(cible);
const style = getComputedStyle(element, pseudo);
return composantes(element instanceof SVGElement ? style.fill : style.color);
}
// Une couleur peint-elle quelque chose ? Un alpha nul ne peint rien.
const peint = (couleur) => couleur[3] !== 0;
// Ajoute les couleurs à la liste, sans doublon, dans leur ordre.
function ajouter(liste, couleurs) {
for (const c of couleurs) {
if (!liste.some((d) => d.join() === c.join())) liste.push(c);
}
return liste;
}
const DEGRADES = new Set(['linearGradient', 'radialGradient']);
// Un serveur de peinture porte-t-il lui-même ce qu'il peint : des <stop> pour
// un dégradé, des éléments enfants pour un motif ?
function porteSonContenu(serveur) {
return DEGRADES.has(serveur.localName)
? serveur.querySelector(':scope > stop') !== null
: serveur.firstElementChild !== null;
}
// Élément que désigne une peinture url(#id), lue dans le document de element,
// ou null pour une couleur. Un dégradé sans <stop> ou un motif sans enfant
// hérite ceux du serveur que désigne son href (ou xlink:href) : la chaîne se
// suit jusqu'au premier serveur qui porte son contenu, ou jusqu'au dernier,
// qui ne peint alors rien. Une référence vers un élément absent, ou une
// chaîne qui boucle, lève.
function serveurDePeinture(valeur, element) {
const reference = /^url\("?#([^")]+)"?\)/.exec(valeur);
if (reference === null) return null;
const doc = element.ownerDocument;
let serveur = doc.getElementById(reference[1]);
if (serveur === null) {
throw new Error(`peinture ${valeur} : aucun élément #${reference[1]}`);
}
const vus = new Set([serveur]);
while (!porteSonContenu(serveur)) {
const lien = serveur.getAttribute('href') ?? serveur.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
if (lien === null || !lien.startsWith('#')) break;
const suivant = doc.getElementById(lien.slice(1));
if (suivant === null) throw new Error(`peinture ${valeur} : #${serveur.id} hérite de ${lien}, absent`);
if (vus.has(suivant)) throw new Error(`peinture ${valeur} : la chaîne href de #${serveur.id} boucle`);
vus.add(suivant);
serveur = suivant;
}
return serveur;
}
// Éléments d'un motif qui peignent : formes et textes, hors display none. Un
// groupe <g> porte un fill calculé, noir par défaut, qu'il ne peint pas ; il
// est écarté, ses formes sont lues.
function peintresDuMotif(motif) {
return [...motif.querySelectorAll('*')].filter(
(n) =>
(n instanceof SVGGeometryElement || n instanceof SVGTextContentElement) &&
n.checkVisibility({ visibilityProperty: true }),
);
}
// Couleurs que peint une valeur calculée de fill ou de stroke : none ne peint
// rien ; url(#id) désigne un motif, dont les formes et les textes peignent par
// leurs propres fill et stroke, ou un dégradé, qui peint ses stop-color ;
// sinon, une couleur. Une référence vers un élément d'un autre genre lève.
function couleursDeLaPeinture(valeur, element) {
if (valeur === 'none') return [];
const serveur = serveurDePeinture(valeur, element);
if (serveur === null) {
const couleur = composantes(valeur);
return peint(couleur) ? [couleur] : [];
}
if (serveur.localName === 'pattern') {
const couleurs = [];
for (const peintre of peintresDuMotif(serveur)) {
const { fill, stroke } = couleursPeintes(peintre);
ajouter(couleurs, [...fill, ...stroke]);
}
return couleurs;
}
if (DEGRADES.has(serveur.localName)) {
return ajouter(
[],
[...serveur.querySelectorAll('stop')]
.map((arret) => composantes(getComputedStyle(arret).stopColor))
.filter(peint),
);
}
throw new Error(`peinture ${valeur} : #${serveur.id} est un <${serveur.localName}>`);
}
/** fill et stroke calculés d'une forme SVG, chacun en liste de couleurs, sans
* doublon : vide pour none ou une couleur transparente ; un motif url(#…)
* rend les couleurs que peignent ses enfants, un dégradé ses arrêts. */
export function couleursPeintes(element) {
const style = getComputedStyle(element);
return {
fill: couleursDeLaPeinture(style.fill, element),
stroke: couleursDeLaPeinture(style.stroke, element),
};
}
// Fond propre d'un élément ou d'un pseudo-élément, s'il est opaque, sinon
// null. Une image de fond se peint par-dessus la couleur de fond, et ses
// couleurs ne se lisent pas dans le style calculé : elle lève en nommant son
// porteur, plutôt que de laisser mesurer une couleur que l'écran ne montre pas.
function fondOpaque(element, pseudo = null) {
const style = getComputedStyle(element, pseudo);
if (style.backgroundImage !== 'none') {
throw new Error(`fond ${designation({ element, pseudo })} : background-image ${style.backgroundImage} ne se mesure pas`);
}
const fond = composantes(style.backgroundColor);
return fond[3] === 1 ? fond : null;
}
// Propriétés qui composent un élément avec ce qui est dessous : non neutres
// sur un fond ou sur l'un de ses ancêtres, sa couleur calculée n'est plus
// celle de l'écran.
const COMPOSITION = ['opacity', 'filter', 'mask-image', 'mix-blend-mode'];
const NEUTRE = new Map(NEUTRES);
// Fautes { element, pseudo, propriete, valeur } des propriétés nommées dont la
// valeur calculée sur element n'est pas neutre.
function fautesSur(element, proprietes) {
const style = getComputedStyle(element);
return proprietes
.filter((propriete) => style.getPropertyValue(propriete) !== NEUTRE.get(propriete))
.map((propriete) => ({ element, pseudo: null, propriete, valeur: style.getPropertyValue(propriete) }));
}
// Fautes de composition d'un fond : sur lui et sur chacun de ses ancêtres qui
// ne contiennent pas le texte mesuré. Un ancêtre commun compose le texte et
// son fond ensemble ; attenuations(texte) le relève déjà.
function voiles(fond, texte) {
return ascendance(fond)
.filter((n) => !n.contains(texte))
.flatMap((n) => fautesSur(n, COMPOSITION));
}
// Fautes qui rendent translucide la peinture fill ou stroke d'un élément SVG :
// les voiles de l'élément, son fill-opacity ou stroke-opacity — hérités, ils se
// lisent sur l'élément même —, une couleur d'alpha inférieur à 1 ; pour un
// motif, les mêmes fautes sur chaque forme qui y peint ; pour un dégradé, un
// arrêt translucide.
function translucidite(element, quoi, texte) {
const style = getComputedStyle(element);
const valeur = style.getPropertyValue(quoi);
const fautes = [...voiles(element, texte), ...fautesSur(element, [`${quoi}-opacity`])];
const serveur = serveurDePeinture(valeur, element);
if (serveur === null) {
if (composantes(valeur)[3] !== 1) fautes.push({ element, pseudo: null, propriete: quoi, valeur });
} else if (serveur.localName === 'pattern') {
for (const peintre of peintresDuMotif(serveur)) {
const { fill, stroke } = couleursPeintes(peintre);
if (fill.length > 0) fautes.push(...translucidite(peintre, 'fill', texte));
if (stroke.length > 0) fautes.push(...translucidite(peintre, 'stroke', texte));
}
} else if (DEGRADES.has(serveur.localName)) {
for (const arret of serveur.querySelectorAll('stop')) {
const { stopColor, stopOpacity } = getComputedStyle(arret);
if (composantes(stopColor)[3] !== 1) fautes.push({ element: arret, pseudo: null, propriete: 'stop-color', valeur: stopColor });
if (stopOpacity !== '1') fautes.push({ element: arret, pseudo: null, propriete: 'stop-opacity', valeur: stopOpacity });
}
}
return fautes;
}
// Lève quand le fond retenu n'est pas opaque à l'écran, en nommant le fond
// et chaque faute : une couleur composée avec ce qui est dessous ne se mesure
// pas sur sa valeur calculée (§ 14.5).
function exigerOpaque(fond, fautes, texte) {
if (fautes.length === 0) return;
const liste = fautes.map((f) => `${designation(f)} ${f.propriete}: ${f.valeur}`).join(' ; ');
throw new Error(`fond ${designation(fond)} translucide sous ${designation(texte)} : ${liste}`);
}
// Peinture, 'stroke' ou 'fill', qu'une forme SVG pose au point donné en
// coordonnées de la fenêtre, ou null quand elle n'y peint rien : forme en
// display none, cachée ou d'opacité nulle, elle ou un ancêtre
// (checkVisibility) ; peinture absente, transparente ou d'opacité nulle ;
// point hors du trait et du remplissage. Le point passe en coordonnées
// propres de la forme par l'inverse de getScreenCTM, que le moteur de rendu
// tient pour exact (§ 7.2) ; isPointInStroke et isPointInFill ignorent
// pointer-events. Le trait se peint sur le remplissage et se teste d'abord.
function peintureAuPoint(forme, centre) {
if (!forme.checkVisibility({ opacityProperty: true, visibilityProperty: true })) return null;
const point = centre.matrixTransform(forme.getScreenCTM().inverse());
const couleurs = couleursPeintes(forme);
const style = getComputedStyle(forme);
for (const quoi of ['stroke', 'fill']) {
if (couleurs[quoi].length === 0 || Number(style.getPropertyValue(`${quoi}-opacity`)) === 0) continue;
if (quoi === 'stroke' ? forme.isPointInStroke(point) : forme.isPointInFill(point)) return quoi;
}
return null;
}
// Couleurs qu'une forme SVG peint au point, ou null quand elle n'y peint
// rien ; lève quand la peinture retenue est translucide à l'écran.
function fondDeForme(forme, centre, texte) {
const quoi = peintureAuPoint(forme, centre);
if (quoi === null) return null;
exigerOpaque(forme, translucidite(forme, quoi, texte), texte);
return couleursPeintes(forme)[quoi];
}
// Un <use> peint une copie de l'élément qu'il désigne, que les aides ne
// lisent pas : celui qui peint au point lève en se nommant, plutôt que de
// laisser rendre ce qui est sous lui.
function refuserUse(use, texte) {
return new Error(`fond sous ${designation(texte)} : ${designation(use)} peint une copie par référence, que l'aide ne lit pas`);
}
// Le <use> peint-il au point ? Sa boîte, dans son propre repère, contient le
// point, et il n'est ni caché ni d'opacité nulle.
function useAuPoint(use, centre) {
if (!use.checkVisibility({ opacityProperty: true, visibilityProperty: true })) return false;
const point = centre.matrixTransform(use.getScreenCTM().inverse());
const b = use.getBBox();
return point.x >= b.x && point.x <= b.x + b.width && point.y >= b.y && point.y <= b.y + b.height;
}
// Centre de la boîte d'un élément, en coordonnées de la fenêtre.
function centreDe(element) {
const boite = element.getBoundingClientRect();
return new DOMPoint(boite.left + boite.width / 2, boite.top + boite.height / 2);
}
// Feuille qui rend chaque élément atteignable le temps d'une lecture :
// elementsFromPoint suit l'atteinte du pointeur, et un élément en
// pointer-events: none n'en est pas moins peint.
const ATTEIGNABLE = '*, *::before, *::after { pointer-events: auto !important; }';
// Éléments présents au point centre, en coordonnées de la fenêtre, du plus
// haut au plus bas dans l'ordre de peinture — cascade, positionnement et
// dessins SVG compris —, pointer-events ignoré. Un point hors de la fenêtre
// n'atteint rien et lève, au nom de texte.
function elementsAuPoint(doc, centre, texte) {
const fenetre = doc.defaultView;
if (centre.x < 0 || centre.y < 0 || centre.x >= fenetre.innerWidth || centre.y >= fenetre.innerHeight) {
throw new Error(`fond sous ${designation(texte)} : centre (${centre.x}, ${centre.y}) hors de la fenêtre`);
}
const feuille = doc.createElement('style');
feuille.textContent = ATTEIGNABLE;
doc.head.append(feuille);
try {
return doc.elementsFromPoint(centre.x, centre.y);
} finally {
feuille.remove();
}
}
// Premier fond opaque peint au point centre sous racine. Parmi les éléments
// présents au point, seuls comptent la première entrée que racine contient et
// celles qui la suivent : ce qui la précède se peint au-dessus d'elle. Les
// descendants de racine sont écartés, racine elle-même non — le fond CSS d'un
// <svg> extérieur se lit. Un élément d'opacité nulle, lui ou un ancêtre, ne
// peint rien et se saute. Une forme SVG rend sa peinture au point, un <use>
// lève, les autres éléments internes d'un SVG ne peignent pas de fond ; un
// élément HTML ou un <svg> extérieur rend son fond opaque. Le fond retenu doit
// être opaque à l'écran, sinon lève ; une racine que le point n'atteint pas
// lève aussi. Rend [] quand rien d'opaque n'est dessous.
function fondAuPoint(centre, racine, texte) {
const atteints = elementsAuPoint(racine.ownerDocument, centre, texte);
const debut = atteints.findIndex((n) => racine.contains(n));
if (debut === -1) {
throw new Error(`fond sous ${designation(texte)} : le centre (${centre.x}, ${centre.y}) n'atteint pas ${designation(racine)}`);
}
for (const n of atteints.slice(debut)) {
if (n !== racine && racine.contains(n)) continue;
if (!n.checkVisibility({ opacityProperty: true })) continue;
if (n instanceof SVGGeometryElement) {
const couleurs = fondDeForme(n, centre, texte);
if (couleurs !== null) return couleurs;
continue;
}
if (n instanceof SVGUseElement) throw refuserUse(n, texte);
if (n instanceof SVGElement && n.ownerSVGElement !== null) continue;
const fond = fondOpaque(n);
if (fond !== null) {
exigerOpaque(n, voiles(n, texte), texte);
return [fond];
}
}
return [];
}
// Le <svg> le plus extérieur qui contient element.
function svgExterieur(element) {
let svg = element.ownerSVGElement ?? element;
while (svg.ownerSVGElement !== null) svg = svg.ownerSVGElement;
return svg;
}
// Couleurs peintes sous un élément SVG, au centre de sa boîte : les formes
// et les <use> qui le précèdent dans l'ordre du document, hors de lui et de
// ses descendants et hors des conteneurs de référence, se peignent avant lui ;
// de la dernière à la première, la première forme qui peint au point rend ses
// couleurs, un <use> qui peint au point lève. Sans rien dessous dans le
// dessin, ce qui est peint au même point sous le <svg> extérieur, son propre
// fond CSS compris.
function fondSvg(element) {
const centre = centreDe(element);
const svg = svgExterieur(element);
const formes = [...svg.querySelectorAll('*')].filter(
(n) =>
(n instanceof SVGGeometryElement || n instanceof SVGUseElement) &&
n !== element &&
!element.contains(n) &&
n.closest(CONTENEURS_DE_REFERENCE) === null &&
(n.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0,
);
for (const forme of formes.reverse()) {
if (forme instanceof SVGUseElement) {
if (useAuPoint(forme, centre)) throw refuserUse(forme, element);
continue;
}
const couleurs = fondDeForme(forme, centre, element);
if (couleurs !== null) return couleurs;
}
return fondAuPoint(centre, svg, element);
}
/** Couleurs peintes sous le contenu d'un porteur ou d'un élément, au centre
* de sa boîte. Un élément HTML : son propre fond s'il est opaque — son texte
* se peint dessus —, sinon le premier fond opaque peint dessous à ce point,
* ses descendants exceptés : un élément HTML frère ou cousin, ou le trait ou
* le remplissage d'une forme SVG ; ce qui se peint au-dessus de lui ne compte
* pas, en pointer-events: none ou non. Un pseudo-élément : son fond opaque,
* sinon celui de son hôte. Un élément SVG, lui et ses descendants exceptés :
* le trait ou le remplissage de la forme qui le précède dessous — les
* couleurs d'un motif, ou d'un dégradé, href suivi —, ou ce qui est peint
* sous le dessin, fond CSS du <svg> extérieur compris. Une forme ou un fond
* qui ne peint rien — caché, opacité nulle, peinture d'opacité nulle — est
* sauté. Rend [] quand rien d'opaque n'est dessous. Lève quand le fond retenu
* est translucide à l'écran (opacity, fill-opacity, stroke-opacity, alpha,
* filter, masque, fusion, sur lui ou ses ancêtres propres), quand une image
* de fond ou un <use> se peint entre le texte et le fond retenu, quand le
* centre est hors de la fenêtre, ou quand il n'atteint pas l'élément. */
export function fondPeintSous(cible) {
const { element, pseudo } = porteur(cible);
if (pseudo !== null) {
const fond = fondOpaque(element, pseudo);
return fond !== null ? [fond] : fondPeintSous(element);
}
if (element instanceof SVGElement && element.ownerSVGElement !== null) return fondSvg(element);
const fond = fondOpaque(element);
if (fond !== null) return [fond];
return fondAuPoint(centreDe(element), element, element);
}
// Fautes de neutralité d'un style : chaque propriété de NEUTRES dont la
// valeur calculée n'est pas la valeur neutre.
function fautesDe(element, pseudo) {
const style = getComputedStyle(element, pseudo);
return NEUTRES.filter(([propriete, neutre]) => style.getPropertyValue(propriete) !== neutre).map(
([propriete]) => ({ element, pseudo, propriete, valeur: style.getPropertyValue(propriete) }),
);
}
/** Propriétés qui font différer le pixel peint de la couleur calculée :
* opacity, fill-opacity et stroke-opacity différents de 1 ; filter,
* backdrop-filter et mask-image différents de none ; mix-blend-mode différent
* de normal. Relevées sur le pseudo-élément, puis sur l'élément et chacun de
* ses ancêtres : opacity et filter ne s'héritent pas, et changent le pixel
* sans changer le style calculé du texte. fill-opacity s'hérite : posé sur un
* groupe, il se relève sur le groupe et sur chaque niveau qui en hérite.
* → [{ element, pseudo, propriete, valeur }], vide quand tout est neutre. */
export function attenuations(cible) {
const { element, pseudo } = porteur(cible);
return [
...(pseudo !== null ? fautesDe(element, pseudo) : []),
...ascendance(element).flatMap((n) => fautesDe(n, null)),
];
}
// Éléments dont le contenu se peint sans qu'un style le dise : image, média,
// dessin, cadre, champ.
const REMPLACES = new Set(['img', 'video', 'canvas', 'iframe', 'embed', 'object', 'input', 'textarea', 'select']);
// Un élément présent au point y peint-il quelque chose qui couvre ce qui est
// dessous ? Non s'il est caché ou d'opacité nulle, lui ou un ancêtre. Une
// forme SVG : une peinture au point (peintureAuPoint). Un autre élément
// interne d'un SVG — texte, <use>, image — : oui, son contenu est au point.
// Un élément HTML ou un <svg> extérieur : un fond d'alpha non nul, une image
// de fond, un nœud de texte propre, un pseudo-élément à texte, ou un élément
// remplacé ; une boîte sans rien de cela, qui ne sert qu'à la mise en page,
// ne couvre rien. Bordure, ombre et contour ne se lisent pas.
function peintAuPoint(n, centre) {
if (!n.checkVisibility({ opacityProperty: true, visibilityProperty: true })) return false;
if (n instanceof SVGGeometryElement) return peintureAuPoint(n, centre) !== null;
if (n instanceof SVGElement && n.ownerSVGElement !== null) return true;
const style = getComputedStyle(n);
return (
composantes(style.backgroundColor)[3] !== 0 ||
style.backgroundImage !== 'none' ||
porteTexte(n) ||
(n instanceof HTMLElement && PSEUDOS.some((pseudo) => contenuTextuel(n, pseudo))) ||
REMPLACES.has(n.localName)
);
}
/** true quand la cible se voit dans la fenêtre, sinon la raison, en texte.
* Un élément : checkVisibility (display, visibility, opacité nulle,
* content-visibility), une boîte non vide qui tient dans la fenêtre, puis
* l'occlusion au centre de cette boîte. Les éléments présents au point se
* lisent pointer-events ignoré — un élément en pointer-events: none se voit,
* et un recouvrement en pointer-events: none cache ce qu'il couvre. Le centre
* doit atteindre l'élément ou l'un de ses descendants — un élément rogné par
* un ancêtre le laisse à un autre —, et aucun élément peint au-dessus ne doit
* y peindre (peintAuPoint) : une boîte de mise en page vide, une zone
* d'atteinte d'opacité nulle ne cachent rien. Un pseudo-élément : son hôte,
* puis son display, sa visibility et son content. */
export function visible(cible) {
const { element, pseudo } = porteur(cible);
if (pseudo !== null) {
const hote = visible(element);
if (hote !== true) return `hôte : ${hote}`;
const style = getComputedStyle(element, pseudo);
if (style.display === 'none') return `${pseudo} : display none`;
if (style.visibility !== 'visible') return `${pseudo} : visibility ${style.visibility}`;
if (!contenuTextuel(element, pseudo)) return `${pseudo} : content ${style.content}`;
return true;
}
if (!element.checkVisibility({ opacityProperty: true, visibilityProperty: true })) {
return 'checkVisibility faux';
}
const b = element.getBoundingClientRect();
if (!(b.width > 0 && b.height > 0)) return `boîte vide : ${b.width} × ${b.height}`;
if (b.left < 0 || b.top < 0 || b.right > innerWidth || b.bottom > innerHeight) {
return `hors de la fenêtre ${innerWidth} × ${innerHeight} : boîte (${b.left}, ${b.top}) – (${b.right}, ${b.bottom})`;
}
const centre = centreDe(element);
const atteints = elementsAuPoint(element.ownerDocument, centre, element);
const rang = atteints.findIndex((n) => element.contains(n));
if (rang === -1) return `le centre de sa boîte n'atteint pas ${designation(element)} : rogné, ou hors de son dessin`;
const couvrant = atteints.slice(0, rang).find((n) => peintAuPoint(n, centre));
if (couvrant !== undefined) return `le centre de sa boîte est couvert par ${designation(couvrant)}`;
return true;
}
/** Pose data-theme ('clair'|'sombre') sur <html>, attend mesure(), puis rend
* à l'attribut sa valeur d'avant, ou son absence, même quand mesure lève :
* l'épreuve suivante part du même document. Rend la valeur de mesure. */
export async function sousTheme(theme, mesure) {
const racine = document.documentElement;
const avant = racine.getAttribute('data-theme');
racine.setAttribute('data-theme', theme);
try {
return await mesure();
} finally {
if (avant === null) {
racine.removeAttribute('data-theme');
} else {
racine.setAttribute('data-theme', avant);
}
}
}