// © 2026 TechnoLibre (http://www.technolibre.ca) // License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) // Épreuves des entrées réelles (§ 7.3) et du cadre d'affichage (§ 19.4). // Les entrées passent par CDP : Chromium les traite comme celles d'un // périphérique, chaque événement relevé porte isTrusted vrai, et la capture // du pointeur se pose — ce qu'un événement construit par la page n'obtient // pas. Les coordonnées sont celles du cadre de l'épreuve. import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'vitest'; import { page } from 'vitest/browser'; import { fixerCadre, glisser, molette, pointer, touche, toucher } from './entrees.js'; const SVG = 'http://www.w3.org/2000/svg'; let scene; let cadreInitial; beforeAll(() => { cadreInitial = { largeur: innerWidth, hauteur: innerHeight }; }); afterAll(async () => { await page.viewport(cadreInitial.largeur, cadreInitial.hauteur); }); beforeEach(() => { scrollTo(0, 0); scene = document.createElement('div'); document.body.append(scene); }); afterEach(() => { scene.remove(); scrollTo(0, 0); }); // Un plan d'épreuve : un en touch-action: none, posé à 30 px du bord, // qui porte deux rectangles côte à côte. function monterPlan() { scene.innerHTML = `' + '' + ''; return scene.querySelector('[data-plan]'); } function centre(element) { const boite = element.getBoundingClientRect(); return { x: boite.left + boite.width / 2, y: boite.top + boite.height / 2 }; } // Relève les événements des types donnés sur la cible, dans leur ordre. function relever(cible, types) { const releves = []; for (const type of types) { cible.addEventListener(type, (e) => releves.push(e)); } return releves; } describe('pointeur', () => { test('un presser réel permet setPointerCapture sur la racine ', async () => { const plan = monterPlan(); let capture = null; plan.addEventListener('pointerdown', (e) => { plan.setPointerCapture(e.pointerId); capture = { isTrusted: e.isTrusted, tenue: plan.hasPointerCapture(e.pointerId) }; }); const { x, y } = centre(plan.querySelector('[data-a]')); await pointer('presser', x, y); await pointer('relacher', x, y); expect(capture).toEqual({ isTrusted: true, tenue: true }); }); test('un pointerdown construit par la page ne permet pas la capture', () => { const plan = monterPlan(); let capture = null; plan.addEventListener('pointerdown', (e) => { try { plan.setPointerCapture(e.pointerId); } catch { // Un identifiant qu'aucun pointeur actif ne porte lève NotFoundError. } capture = { isTrusted: e.isTrusted, tenue: plan.hasPointerCapture(e.pointerId) }; }); const { x, y } = centre(plan.querySelector('[data-a]')); plan.querySelector('[data-a]').dispatchEvent( new PointerEvent('pointerdown', { bubbles: true, pointerId: 1, clientX: x, clientY: y, buttons: 1 }), ); expect(capture).toEqual({ isTrusted: false, tenue: false }); }); for (const defile of [0, 400]) { test(`le centre d'un élément, pointé, l'atteint comme cible — page défilée de ${defile} px`, async () => { const plan = monterPlan(); // Un espace sous le plan rend la page défilable, puis le plan descend // sous la marge défilée. plan.style.marginTop = `${30 + defile}px`; plan.style.marginBottom = '2000px'; scrollTo(0, defile); expect(scrollY).toBe(defile); const releves = relever(plan, ['pointerdown', 'pointerup']); for (const cible of [plan.querySelector('[data-a]'), plan.querySelector('[data-b]')]) { const { x, y } = centre(cible); await pointer('presser', x, y); await pointer('relacher', x, y); } expect(releves.map((e) => [e.type, e.target.getAttribute('data-a') !== null ? 'a' : 'b', e.isTrusted])).toEqual([ ['pointerdown', 'a', true], ['pointerup', 'a', true], ['pointerdown', 'b', true], ['pointerup', 'b', true], ]); const { x, y } = centre(plan.querySelector('[data-b]')); expect(releves[2].clientX).toBeCloseTo(x, 0); expect(releves[2].clientY).toBeCloseTo(y, 0); }); } test("le cadre de l'épreuve décalé et réduit dans sa page : le centre visé reste atteint", async () => { // Le cadre de l'épreuve est une