[ADD] interface: cross-screen rendering checks, frame marks, badge plate
A screen walker reaches every screen of the iteration by gestures; on each, in both themes, text holds 4.5:1 and marks 3:1 against what is painted under them, every text passes through the translation tables, the conflict hue paints only conflict marks, and no state differs by colour alone. Mode frames now carry their mark, the count badge sits on a plate over the conflict hatch, the name list clears the focus square, the participant form is novalidate. Demonstrations space columns so a name list never runs over a neighbour's chairs. Checked: 2676 node, 489 browser, 60 long tests. --- FR --- [ADD] interface : rendu transversal, marques des cadres, socle, pastille Un parcours atteint par gestes chaque écran de l'itération ; sur chacun, dans les deux thèmes, le texte tient 4,5:1 et les marques 3:1 sur ce qui est peint dessous, tout texte passe par les tables de libellés, la teinte de conflit ne peint que les marques de conflit, et aucun état ne diffère par la seule couleur. Les cadres de mode portent leur marque, la pastille pose sur un socle au-dessus de la trame, la liste des noms s'écarte du focus, le formulaire est novalidate. Les démonstrations espacent leurs colonnes : une liste ne court plus sur les chaises voisines. Vérifié : 2676, 489, 60. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
819a3853e5
commit
71b9e75363
20 changed files with 2041 additions and 84 deletions
|
|
@ -28,9 +28,11 @@
|
|||
// plus grande à égalité ; une table d'une autre capacité porte la sienne
|
||||
// en surcharge (§ 6.1) ;
|
||||
// - les tables rondes, rangées en grille dans l'ordre de la configuration,
|
||||
// rangée par rangée, leurs centres espacés de 300 cm ; la grille a la plus
|
||||
// petite largeur dont le carré contient toutes les tables, et seule sa
|
||||
// dernière rangée est incomplète ;
|
||||
// rangée par rangée, leurs rangées espacées de 300 cm ; la grille a la
|
||||
// plus petite largeur dont le carré contient toutes les tables, et seule
|
||||
// sa dernière rangée est incomplète. Ses colonnes s'écartent assez pour
|
||||
// que le bloc de noms d'une table, de toute sa largeur, tienne entre deux
|
||||
// colonnes : aucune liste ne passe sur une autre table (§ 7.4) ;
|
||||
// - les contraintes de la configuration, les sièges non attribués, la
|
||||
// génération par défaut de la recherche ;
|
||||
// - chaque réservation, celle d'un animateur, au siège 1 de sa table pour
|
||||
|
|
@ -47,6 +49,8 @@ import { decoder } from '../src/csv/encodage.js';
|
|||
import { SEPARATEUR, ecrireCsv, exporterParticipants } from '../src/csv/export.js';
|
||||
import { decouper } from '../src/csv/lecture.js';
|
||||
import { CATALOGUE } from '../src/demo/catalogue.js';
|
||||
import { constante } from '../src/geometrie/constantes.js';
|
||||
import { ECART_LISTE, geometrieTable } from '../src/geometrie/tables.js';
|
||||
import { serialiser, serialiserCharge } from '../src/stockage/canonique.js';
|
||||
import { analyser, creerCharge } from '../src/stockage/document.js';
|
||||
import { VERSION } from '../src/version.genere.js';
|
||||
|
|
@ -66,8 +70,8 @@ const DEMONSTRATIONS_EN_CSV = ['petite', 'grande'];
|
|||
// Séparateur de la variante windows-1252 (§ 10.3).
|
||||
const VIRGULE = ',';
|
||||
|
||||
// Écart entre les centres de deux tables voisines de la grille, en cm.
|
||||
const ECART_CM = 300;
|
||||
// Écart entre les centres de deux rangées de la grille, en cm.
|
||||
const ECART_RANGEES_CM = 300;
|
||||
|
||||
// Le siège de l'animateur, et le titre qu'il porte.
|
||||
const SIEGE_ANIMATEUR = 1;
|
||||
|
|
@ -96,6 +100,21 @@ function colonnesDe(nombre) {
|
|||
return colonnes;
|
||||
}
|
||||
|
||||
// Écart entre les centres de deux colonnes de la grille, en cm entiers : le
|
||||
// bloc de noms d'une table commence à ECART_LISTE de son emprise et s'étend
|
||||
// sur LARGEUR_BLOC_NOMS_CM, où toute ligne s'abrège ; il s'achève à
|
||||
// ECART_LISTE de l'emprise de la colonne suivante, pour la plus grande
|
||||
// emprise des capacités données. Aucune emprise, d'aucune rangée, ne
|
||||
// rencontre ainsi la bande du bloc.
|
||||
function ecartColonnes(capacites) {
|
||||
const emprise = Math.max(
|
||||
...capacites.map(
|
||||
(places) => geometrieTable({ id: 0, numero: 0, forme: 'ronde', position: { x: 0, y: 0 } }, places).rayonEmprise,
|
||||
),
|
||||
);
|
||||
return Math.ceil(2 * emprise + 2 * ECART_LISTE + constante('LARGEUR_BLOC_NOMS_CM'));
|
||||
}
|
||||
|
||||
// Plus grand identifiant d'une liste d'enregistrements ; 0 pour une liste
|
||||
// vide.
|
||||
const plusGrandId = (enregistrements) => enregistrements.reduce((plus, { id }) => Math.max(plus, id), 0);
|
||||
|
|
@ -134,6 +153,7 @@ export function chargeDeDemonstration({ cle, nom, construire }) {
|
|||
const animateurs = animateursDe(reservations, cle);
|
||||
const reserves = new Set(animateurs.values());
|
||||
const colonnes = colonnesDe(tables.length);
|
||||
const ecart = ecartColonnes(tables.map(({ capacite }) => capacite));
|
||||
const { separerAppartenances, nouveauxVoisins, nouvelleTable, varierAppartenances } = contraintes;
|
||||
return {
|
||||
...neuve,
|
||||
|
|
@ -154,7 +174,7 @@ export function chargeDeDemonstration({ cle, nom, construire }) {
|
|||
numero,
|
||||
sieges: capacite === siegesParDefaut ? null : capacite,
|
||||
forme: 'ronde',
|
||||
position: { x: (rang % colonnes) * ECART_CM, y: Math.floor(rang / colonnes) * ECART_CM },
|
||||
position: { x: (rang % colonnes) * ecart, y: Math.floor(rang / colonnes) * ECART_RANGEES_CM },
|
||||
})),
|
||||
reservations: [...animateurs].map(([table, participant]) => ({
|
||||
participant,
|
||||
|
|
|
|||
|
|
@ -209,18 +209,22 @@ describe("engendrer_livrables : la charge d'une démonstration (§ 15)", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('les tables : rondes, en grille rangée par rangée, centres à 300 cm, la dernière rangée seule incomplète', () => {
|
||||
test('les tables : rondes, en grille rangée par rangée, la dernière rangée seule incomplète ; rangées à 300 cm, colonnes assez écartées pour le bloc de noms', () => {
|
||||
// Écart des colonnes : 2 × l'emprise de la plus grande table, deux écarts
|
||||
// de liste (36 cm) et la largeur du bloc de noms (500 cm), arrondi au cm
|
||||
// supérieur — petite, 3 places, emprise 79 : 158 + 72 + 500 = 730 ;
|
||||
// grande, 8 places, emprise 120,39 : ⌈240,79 + 72 + 500⌉ = 813.
|
||||
assert.deepEqual(chargeDeDemonstration(entreeDe('petite')).tables, [
|
||||
{ id: 1, numero: 1, sieges: null, forme: 'ronde', position: { x: 0, y: 0 } },
|
||||
{ id: 2, numero: 2, sieges: null, forme: 'ronde', position: { x: 300, y: 0 } },
|
||||
{ id: 2, numero: 2, sieges: null, forme: 'ronde', position: { x: 730, y: 0 } },
|
||||
{ id: 3, numero: 3, sieges: null, forme: 'ronde', position: { x: 0, y: 300 } },
|
||||
{ id: 4, numero: 4, sieges: null, forme: 'ronde', position: { x: 300, y: 300 } },
|
||||
{ id: 4, numero: 4, sieges: null, forme: 'ronde', position: { x: 730, y: 300 } },
|
||||
]);
|
||||
// 33 tables : six colonnes, cinq rangées pleines, puis trois tables.
|
||||
const { tables } = chargeDeDemonstration(entreeDe('grande'));
|
||||
const positions = tables.map(({ position: { x, y } }) => [x, y]);
|
||||
assert.deepEqual(positions.slice(0, 7), [[0, 0], [300, 0], [600, 0], [900, 0], [1200, 0], [1500, 0], [0, 300]]);
|
||||
assert.deepEqual(positions.slice(-4), [[1500, 1200], [0, 1500], [300, 1500], [600, 1500]]);
|
||||
assert.deepEqual(positions.slice(0, 7), [[0, 0], [813, 0], [1626, 0], [2439, 0], [3252, 0], [4065, 0], [0, 300]]);
|
||||
assert.deepEqual(positions.slice(-4), [[4065, 1200], [0, 1500], [813, 1500], [1626, 1500]]);
|
||||
assert.equal(new Set(positions.map(String)).size, 33);
|
||||
assert.ok(tables.every(({ forme }) => forme === 'ronde'));
|
||||
assert.deepEqual(tables.map(({ id, numero }) => [id, numero]), positions.map((_, rang) => [rang + 1, rang + 1]));
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import { versionVoisine } from '../../test/version_voisine.js';
|
|||
import { apercevoir } from '../csv/apercu.js';
|
||||
import { decoder } from '../csv/encodage.js';
|
||||
import { exporterParticipants } from '../csv/export.js';
|
||||
import { constante } from '../geometrie/constantes.js';
|
||||
import { geometrieTable } from '../geometrie/tables.js';
|
||||
import { serialiser } from '../stockage/canonique.js';
|
||||
import { analyser } from '../stockage/document.js';
|
||||
import { examiner } from '../stockage/placements.js';
|
||||
|
|
@ -155,6 +157,25 @@ describe('démonstrations livrées : les fichiers (§ 15.5, point 5)', () => {
|
|||
assert.deepEqual([charge.propositions, charge.retenu], [[], null], cle);
|
||||
}
|
||||
});
|
||||
|
||||
test("le bloc de noms d'une table, de toute sa largeur, ne rencontre l'emprise d'aucune autre table, d'aucune rangée (§ 7.4)", () => {
|
||||
// Le bloc s'étend de origineListe.x sur LARGEUR_BLOC_NOMS_CM, où toute
|
||||
// ligne s'abrège ; sa hauteur, mesurée sur la police, ne se connaît pas
|
||||
// ici : l'épreuve exige la bande verticale entière libre de toute emprise.
|
||||
const largeur = constante('LARGEUR_BLOC_NOMS_CM');
|
||||
for (const { cle, texte } of LIVREES) {
|
||||
const { charge } = analyser(texte);
|
||||
const geometries = charge.tables.map((table) => geometrieTable(table, table.sieges ?? charge.evenement.siegesParDefaut));
|
||||
const rencontres = geometries.flatMap((g) =>
|
||||
geometries
|
||||
.filter((autre) => autre !== g)
|
||||
.filter((autre) => autre.centre.x - autre.rayonEmprise < g.origineListe.x + largeur && autre.centre.x + autre.rayonEmprise > g.origineListe.x)
|
||||
.map((autre) => `table ${g.numero} sur la table ${autre.numero}`),
|
||||
);
|
||||
assert.ok(geometries.length > 1, cle);
|
||||
assert.deepEqual(rencontres, [], cle);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('démonstrations livrées : aller-retour sur la petite (§ 8.9, § 14.4)', () => {
|
||||
|
|
|
|||
|
|
@ -268,38 +268,38 @@
|
|||
],
|
||||
"tables": [
|
||||
{"id":1,"numero":1,"sieges":null,"forme":"ronde","position":{"x":0,"y":0}},
|
||||
{"id":2,"numero":2,"sieges":null,"forme":"ronde","position":{"x":300,"y":0}},
|
||||
{"id":3,"numero":3,"sieges":null,"forme":"ronde","position":{"x":600,"y":0}},
|
||||
{"id":4,"numero":4,"sieges":null,"forme":"ronde","position":{"x":900,"y":0}},
|
||||
{"id":5,"numero":5,"sieges":null,"forme":"ronde","position":{"x":1200,"y":0}},
|
||||
{"id":6,"numero":6,"sieges":null,"forme":"ronde","position":{"x":1500,"y":0}},
|
||||
{"id":2,"numero":2,"sieges":null,"forme":"ronde","position":{"x":813,"y":0}},
|
||||
{"id":3,"numero":3,"sieges":null,"forme":"ronde","position":{"x":1626,"y":0}},
|
||||
{"id":4,"numero":4,"sieges":null,"forme":"ronde","position":{"x":2439,"y":0}},
|
||||
{"id":5,"numero":5,"sieges":null,"forme":"ronde","position":{"x":3252,"y":0}},
|
||||
{"id":6,"numero":6,"sieges":null,"forme":"ronde","position":{"x":4065,"y":0}},
|
||||
{"id":7,"numero":7,"sieges":null,"forme":"ronde","position":{"x":0,"y":300}},
|
||||
{"id":8,"numero":8,"sieges":null,"forme":"ronde","position":{"x":300,"y":300}},
|
||||
{"id":9,"numero":9,"sieges":null,"forme":"ronde","position":{"x":600,"y":300}},
|
||||
{"id":10,"numero":10,"sieges":null,"forme":"ronde","position":{"x":900,"y":300}},
|
||||
{"id":11,"numero":11,"sieges":null,"forme":"ronde","position":{"x":1200,"y":300}},
|
||||
{"id":12,"numero":12,"sieges":null,"forme":"ronde","position":{"x":1500,"y":300}},
|
||||
{"id":8,"numero":8,"sieges":null,"forme":"ronde","position":{"x":813,"y":300}},
|
||||
{"id":9,"numero":9,"sieges":null,"forme":"ronde","position":{"x":1626,"y":300}},
|
||||
{"id":10,"numero":10,"sieges":null,"forme":"ronde","position":{"x":2439,"y":300}},
|
||||
{"id":11,"numero":11,"sieges":null,"forme":"ronde","position":{"x":3252,"y":300}},
|
||||
{"id":12,"numero":12,"sieges":null,"forme":"ronde","position":{"x":4065,"y":300}},
|
||||
{"id":13,"numero":13,"sieges":null,"forme":"ronde","position":{"x":0,"y":600}},
|
||||
{"id":14,"numero":14,"sieges":null,"forme":"ronde","position":{"x":300,"y":600}},
|
||||
{"id":15,"numero":15,"sieges":null,"forme":"ronde","position":{"x":600,"y":600}},
|
||||
{"id":16,"numero":16,"sieges":null,"forme":"ronde","position":{"x":900,"y":600}},
|
||||
{"id":17,"numero":17,"sieges":null,"forme":"ronde","position":{"x":1200,"y":600}},
|
||||
{"id":18,"numero":18,"sieges":null,"forme":"ronde","position":{"x":1500,"y":600}},
|
||||
{"id":14,"numero":14,"sieges":null,"forme":"ronde","position":{"x":813,"y":600}},
|
||||
{"id":15,"numero":15,"sieges":null,"forme":"ronde","position":{"x":1626,"y":600}},
|
||||
{"id":16,"numero":16,"sieges":null,"forme":"ronde","position":{"x":2439,"y":600}},
|
||||
{"id":17,"numero":17,"sieges":null,"forme":"ronde","position":{"x":3252,"y":600}},
|
||||
{"id":18,"numero":18,"sieges":null,"forme":"ronde","position":{"x":4065,"y":600}},
|
||||
{"id":19,"numero":19,"sieges":null,"forme":"ronde","position":{"x":0,"y":900}},
|
||||
{"id":20,"numero":20,"sieges":null,"forme":"ronde","position":{"x":300,"y":900}},
|
||||
{"id":21,"numero":21,"sieges":null,"forme":"ronde","position":{"x":600,"y":900}},
|
||||
{"id":22,"numero":22,"sieges":null,"forme":"ronde","position":{"x":900,"y":900}},
|
||||
{"id":23,"numero":23,"sieges":null,"forme":"ronde","position":{"x":1200,"y":900}},
|
||||
{"id":24,"numero":24,"sieges":null,"forme":"ronde","position":{"x":1500,"y":900}},
|
||||
{"id":20,"numero":20,"sieges":null,"forme":"ronde","position":{"x":813,"y":900}},
|
||||
{"id":21,"numero":21,"sieges":null,"forme":"ronde","position":{"x":1626,"y":900}},
|
||||
{"id":22,"numero":22,"sieges":null,"forme":"ronde","position":{"x":2439,"y":900}},
|
||||
{"id":23,"numero":23,"sieges":null,"forme":"ronde","position":{"x":3252,"y":900}},
|
||||
{"id":24,"numero":24,"sieges":null,"forme":"ronde","position":{"x":4065,"y":900}},
|
||||
{"id":25,"numero":25,"sieges":null,"forme":"ronde","position":{"x":0,"y":1200}},
|
||||
{"id":26,"numero":26,"sieges":null,"forme":"ronde","position":{"x":300,"y":1200}},
|
||||
{"id":27,"numero":27,"sieges":null,"forme":"ronde","position":{"x":600,"y":1200}},
|
||||
{"id":28,"numero":28,"sieges":null,"forme":"ronde","position":{"x":900,"y":1200}},
|
||||
{"id":29,"numero":29,"sieges":null,"forme":"ronde","position":{"x":1200,"y":1200}},
|
||||
{"id":30,"numero":30,"sieges":null,"forme":"ronde","position":{"x":1500,"y":1200}},
|
||||
{"id":26,"numero":26,"sieges":null,"forme":"ronde","position":{"x":813,"y":1200}},
|
||||
{"id":27,"numero":27,"sieges":null,"forme":"ronde","position":{"x":1626,"y":1200}},
|
||||
{"id":28,"numero":28,"sieges":null,"forme":"ronde","position":{"x":2439,"y":1200}},
|
||||
{"id":29,"numero":29,"sieges":null,"forme":"ronde","position":{"x":3252,"y":1200}},
|
||||
{"id":30,"numero":30,"sieges":null,"forme":"ronde","position":{"x":4065,"y":1200}},
|
||||
{"id":31,"numero":31,"sieges":null,"forme":"ronde","position":{"x":0,"y":1500}},
|
||||
{"id":32,"numero":32,"sieges":null,"forme":"ronde","position":{"x":300,"y":1500}},
|
||||
{"id":33,"numero":33,"sieges":null,"forme":"ronde","position":{"x":600,"y":1500}}
|
||||
{"id":32,"numero":32,"sieges":null,"forme":"ronde","position":{"x":813,"y":1500}},
|
||||
{"id":33,"numero":33,"sieges":null,"forme":"ronde","position":{"x":1626,"y":1500}}
|
||||
],
|
||||
"reservations": [
|
||||
{"participant":2,"table":17,"siege":1,"portee":"tous","tour":null},
|
||||
|
|
|
|||
|
|
@ -268,38 +268,38 @@
|
|||
],
|
||||
"tables": [
|
||||
{"id":1,"numero":1,"sieges":null,"forme":"ronde","position":{"x":0,"y":0}},
|
||||
{"id":2,"numero":2,"sieges":null,"forme":"ronde","position":{"x":300,"y":0}},
|
||||
{"id":3,"numero":3,"sieges":null,"forme":"ronde","position":{"x":600,"y":0}},
|
||||
{"id":4,"numero":4,"sieges":null,"forme":"ronde","position":{"x":900,"y":0}},
|
||||
{"id":5,"numero":5,"sieges":null,"forme":"ronde","position":{"x":1200,"y":0}},
|
||||
{"id":6,"numero":6,"sieges":null,"forme":"ronde","position":{"x":1500,"y":0}},
|
||||
{"id":2,"numero":2,"sieges":null,"forme":"ronde","position":{"x":813,"y":0}},
|
||||
{"id":3,"numero":3,"sieges":null,"forme":"ronde","position":{"x":1626,"y":0}},
|
||||
{"id":4,"numero":4,"sieges":null,"forme":"ronde","position":{"x":2439,"y":0}},
|
||||
{"id":5,"numero":5,"sieges":null,"forme":"ronde","position":{"x":3252,"y":0}},
|
||||
{"id":6,"numero":6,"sieges":null,"forme":"ronde","position":{"x":4065,"y":0}},
|
||||
{"id":7,"numero":7,"sieges":null,"forme":"ronde","position":{"x":0,"y":300}},
|
||||
{"id":8,"numero":8,"sieges":null,"forme":"ronde","position":{"x":300,"y":300}},
|
||||
{"id":9,"numero":9,"sieges":null,"forme":"ronde","position":{"x":600,"y":300}},
|
||||
{"id":10,"numero":10,"sieges":null,"forme":"ronde","position":{"x":900,"y":300}},
|
||||
{"id":11,"numero":11,"sieges":null,"forme":"ronde","position":{"x":1200,"y":300}},
|
||||
{"id":12,"numero":12,"sieges":null,"forme":"ronde","position":{"x":1500,"y":300}},
|
||||
{"id":8,"numero":8,"sieges":null,"forme":"ronde","position":{"x":813,"y":300}},
|
||||
{"id":9,"numero":9,"sieges":null,"forme":"ronde","position":{"x":1626,"y":300}},
|
||||
{"id":10,"numero":10,"sieges":null,"forme":"ronde","position":{"x":2439,"y":300}},
|
||||
{"id":11,"numero":11,"sieges":null,"forme":"ronde","position":{"x":3252,"y":300}},
|
||||
{"id":12,"numero":12,"sieges":null,"forme":"ronde","position":{"x":4065,"y":300}},
|
||||
{"id":13,"numero":13,"sieges":null,"forme":"ronde","position":{"x":0,"y":600}},
|
||||
{"id":14,"numero":14,"sieges":null,"forme":"ronde","position":{"x":300,"y":600}},
|
||||
{"id":15,"numero":15,"sieges":null,"forme":"ronde","position":{"x":600,"y":600}},
|
||||
{"id":16,"numero":16,"sieges":null,"forme":"ronde","position":{"x":900,"y":600}},
|
||||
{"id":17,"numero":17,"sieges":null,"forme":"ronde","position":{"x":1200,"y":600}},
|
||||
{"id":18,"numero":18,"sieges":null,"forme":"ronde","position":{"x":1500,"y":600}},
|
||||
{"id":14,"numero":14,"sieges":null,"forme":"ronde","position":{"x":813,"y":600}},
|
||||
{"id":15,"numero":15,"sieges":null,"forme":"ronde","position":{"x":1626,"y":600}},
|
||||
{"id":16,"numero":16,"sieges":null,"forme":"ronde","position":{"x":2439,"y":600}},
|
||||
{"id":17,"numero":17,"sieges":null,"forme":"ronde","position":{"x":3252,"y":600}},
|
||||
{"id":18,"numero":18,"sieges":null,"forme":"ronde","position":{"x":4065,"y":600}},
|
||||
{"id":19,"numero":19,"sieges":null,"forme":"ronde","position":{"x":0,"y":900}},
|
||||
{"id":20,"numero":20,"sieges":null,"forme":"ronde","position":{"x":300,"y":900}},
|
||||
{"id":21,"numero":21,"sieges":null,"forme":"ronde","position":{"x":600,"y":900}},
|
||||
{"id":22,"numero":22,"sieges":null,"forme":"ronde","position":{"x":900,"y":900}},
|
||||
{"id":23,"numero":23,"sieges":null,"forme":"ronde","position":{"x":1200,"y":900}},
|
||||
{"id":24,"numero":24,"sieges":null,"forme":"ronde","position":{"x":1500,"y":900}},
|
||||
{"id":20,"numero":20,"sieges":null,"forme":"ronde","position":{"x":813,"y":900}},
|
||||
{"id":21,"numero":21,"sieges":null,"forme":"ronde","position":{"x":1626,"y":900}},
|
||||
{"id":22,"numero":22,"sieges":null,"forme":"ronde","position":{"x":2439,"y":900}},
|
||||
{"id":23,"numero":23,"sieges":null,"forme":"ronde","position":{"x":3252,"y":900}},
|
||||
{"id":24,"numero":24,"sieges":null,"forme":"ronde","position":{"x":4065,"y":900}},
|
||||
{"id":25,"numero":25,"sieges":null,"forme":"ronde","position":{"x":0,"y":1200}},
|
||||
{"id":26,"numero":26,"sieges":null,"forme":"ronde","position":{"x":300,"y":1200}},
|
||||
{"id":27,"numero":27,"sieges":null,"forme":"ronde","position":{"x":600,"y":1200}},
|
||||
{"id":28,"numero":28,"sieges":null,"forme":"ronde","position":{"x":900,"y":1200}},
|
||||
{"id":29,"numero":29,"sieges":null,"forme":"ronde","position":{"x":1200,"y":1200}},
|
||||
{"id":30,"numero":30,"sieges":7,"forme":"ronde","position":{"x":1500,"y":1200}},
|
||||
{"id":26,"numero":26,"sieges":null,"forme":"ronde","position":{"x":813,"y":1200}},
|
||||
{"id":27,"numero":27,"sieges":null,"forme":"ronde","position":{"x":1626,"y":1200}},
|
||||
{"id":28,"numero":28,"sieges":null,"forme":"ronde","position":{"x":2439,"y":1200}},
|
||||
{"id":29,"numero":29,"sieges":null,"forme":"ronde","position":{"x":3252,"y":1200}},
|
||||
{"id":30,"numero":30,"sieges":7,"forme":"ronde","position":{"x":4065,"y":1200}},
|
||||
{"id":31,"numero":31,"sieges":7,"forme":"ronde","position":{"x":0,"y":1500}},
|
||||
{"id":32,"numero":32,"sieges":7,"forme":"ronde","position":{"x":300,"y":1500}},
|
||||
{"id":33,"numero":33,"sieges":7,"forme":"ronde","position":{"x":600,"y":1500}}
|
||||
{"id":32,"numero":32,"sieges":7,"forme":"ronde","position":{"x":813,"y":1500}},
|
||||
{"id":33,"numero":33,"sieges":7,"forme":"ronde","position":{"x":1626,"y":1500}}
|
||||
],
|
||||
"reservations": [
|
||||
{"participant":2,"table":17,"siege":1,"portee":"tous","tour":null},
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@
|
|||
],
|
||||
"tables": [
|
||||
{"id":1,"numero":1,"sieges":null,"forme":"ronde","position":{"x":0,"y":0}},
|
||||
{"id":2,"numero":2,"sieges":null,"forme":"ronde","position":{"x":300,"y":0}},
|
||||
{"id":2,"numero":2,"sieges":null,"forme":"ronde","position":{"x":730,"y":0}},
|
||||
{"id":3,"numero":3,"sieges":null,"forme":"ronde","position":{"x":0,"y":300}},
|
||||
{"id":4,"numero":4,"sieges":null,"forme":"ronde","position":{"x":300,"y":300}}
|
||||
{"id":4,"numero":4,"sieges":null,"forme":"ronde","position":{"x":730,"y":300}}
|
||||
],
|
||||
"reservations": [],
|
||||
"titres": [],
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@
|
|||
],
|
||||
"tables": [
|
||||
{"id":1,"numero":1,"sieges":null,"forme":"ronde","position":{"x":0,"y":0}},
|
||||
{"id":2,"numero":2,"sieges":null,"forme":"ronde","position":{"x":300,"y":0}},
|
||||
{"id":2,"numero":2,"sieges":null,"forme":"ronde","position":{"x":730,"y":0}},
|
||||
{"id":3,"numero":3,"sieges":null,"forme":"ronde","position":{"x":0,"y":300}},
|
||||
{"id":4,"numero":4,"sieges":null,"forme":"ronde","position":{"x":300,"y":300}}
|
||||
{"id":4,"numero":4,"sieges":null,"forme":"ronde","position":{"x":730,"y":300}}
|
||||
],
|
||||
"reservations": [],
|
||||
"titres": [],
|
||||
|
|
|
|||
|
|
@ -35,8 +35,11 @@ export const RAYON_CHAISE = 22;
|
|||
export const RAYON_POIGNEE = 15;
|
||||
/** Distance entre l'emprise et le bord de la poignée, en cm. */
|
||||
export const ECART_POIGNEE = 8;
|
||||
/** Distance entre l'emprise et le bloc de noms, en cm. */
|
||||
export const ECART_LISTE = 10;
|
||||
/** Distance entre l'emprise et le bloc de noms, en cm : le carré du focus
|
||||
* que l'interface pose autour de l'emprise, ou d'une chaise, s'étend à
|
||||
* 26 cm d'elle, plus la moitié de son trait ; le bloc commence au-delà, et
|
||||
* aucune marque ne couvre la liste. */
|
||||
export const ECART_LISTE = 36;
|
||||
/** Distance entre deux cases de la grille des tables créées, en cm. */
|
||||
export const PAS_GRILLE = 300;
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ describe('constantes', () => {
|
|||
assert.equal(RAYON_CHAISE, 22);
|
||||
assert.equal(RAYON_POIGNEE, 15);
|
||||
assert.equal(ECART_POIGNEE, 8);
|
||||
assert.equal(ECART_LISTE, 10);
|
||||
assert.equal(ECART_LISTE, 36);
|
||||
assert.equal(PAS_GRILLE, 300);
|
||||
});
|
||||
});
|
||||
|
|
@ -119,7 +119,7 @@ describe('geometrieTable, ronde', () => {
|
|||
proche(g.rayonEmprise, r + 44, 'rayon d’emprise');
|
||||
const ecart = (r + 44 + 23) / Math.SQRT2;
|
||||
procheDuPoint(g.poignee, { x: 300 + ecart, y: 600 + ecart }, 'poignée');
|
||||
procheDuPoint(g.origineListe, { x: 300 + r + 54, y: 600 - r }, 'origine de la liste');
|
||||
procheDuPoint(g.origineListe, { x: 300 + r + 80, y: 600 - r }, 'origine de la liste');
|
||||
});
|
||||
|
||||
test('ronde de 3 : plancher de 70 cm, pas 70π/3', () => {
|
||||
|
|
@ -163,10 +163,10 @@ describe('geometrieTable, carrée', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('emprise 104, poignée au-delà du coin, liste en (414, 540)', () => {
|
||||
test('emprise 104, poignée au-delà du coin, liste en (440, 540)', () => {
|
||||
assert.equal(g.rayonEmprise, 104);
|
||||
procheDuPoint(g.poignee, { x: 404 + 23 / Math.SQRT2, y: 704 + 23 / Math.SQRT2 }, 'poignée');
|
||||
procheDuPoint(g.origineListe, { x: 414, y: 540 }, 'origine de la liste');
|
||||
procheDuPoint(g.origineListe, { x: 440, y: 540 }, 'origine de la liste');
|
||||
});
|
||||
|
||||
test('carrée de 3 : côtés garnis 1, 1, 1, 0 et pas 70', () => {
|
||||
|
|
|
|||
|
|
@ -38,12 +38,15 @@
|
|||
// forme de la lecture, et n'a pas de libellé.
|
||||
const cadre = $derived(cadreDeMode($seance));
|
||||
const forme = $derived(cadre?.forme ?? 'tirets');
|
||||
// Le trait du cadre est une marque (§ 14.5) : data-marque nomme l'entrée
|
||||
// du registre (marques.js) qui porte le jeton de sa couleur.
|
||||
const MARQUE_DE_LA_FORME = { tirets: 'cadreLecture', plein: 'cadreEcriture', double: 'cadreBloque' };
|
||||
const commande = $derived(commandeFixe($seance));
|
||||
const verrou = $derived($seance.mode === 'lecture' ? $seance.verrouAutre : null);
|
||||
const reprise = $derived(repriseOfferte($seance));
|
||||
</script>
|
||||
|
||||
<div class="cadre" data-mode={forme} class:tirets={forme === 'tirets'} class:double={forme === 'double'}>
|
||||
<div class="cadre" data-mode={forme} data-marque={MARQUE_DE_LA_FORME[forme]} class:tirets={forme === 'tirets'} class:double={forme === 'double'}>
|
||||
<div class="barre" role="group" aria-label={t('evenement.cadre.commandes')}>
|
||||
<div class="ligne">
|
||||
<div class="fixe" data-place-fixe>
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@
|
|||
onDestroy(() => geste.liberer());
|
||||
</script>
|
||||
|
||||
<form aria-labelledby={`${uid}-titre`} onsubmit={ajouter}>
|
||||
<form novalidate aria-labelledby={`${uid}-titre`} onsubmit={ajouter}>
|
||||
<h3 id={`${uid}-titre`}>{t('participants.formulaire.titre')}</h3>
|
||||
<div class="champs">
|
||||
{#each CHAMPS as cle (cle)}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import { rechercher } from '../../moteur/recherche.js';
|
|||
import { analyser, configurationDepuisCharge, creerCharge } from '../../stockage/document.js';
|
||||
import { versFichier } from '../../stockage/placements.js';
|
||||
import { fixerCadre } from '../../../test/navigateur/entrees.js';
|
||||
import { composantes, couleursPeintes, sousTheme, visible } from '../../../test/navigateur/rendu.js';
|
||||
import { composantes, couleursPeintes, fondPeintSous, sousTheme, visible } from '../../../test/navigateur/rendu.js';
|
||||
import TEXTE_TOUTES_EN_CONFLIT from '../../../test/fixtures/plan/toutes_en_conflit.gtt.json?raw';
|
||||
import { TABLES_INTERFACE } from '../libelles.js';
|
||||
import BandeauPlan from './BandeauPlan.svelte';
|
||||
|
|
@ -510,6 +510,11 @@ describe('le signal de conflit (§ 7.5, § 13.3)', () => {
|
|||
expect(pastilles[0].textContent).toBe(String(tour2.tables[0].pastille));
|
||||
expect(visible(pastilles[0].querySelector('text'))).toBe(true);
|
||||
expect(couleursPeintes(pastilles[0].querySelector('[data-marque=pastille]')).fill).toEqual([jeton('--couleur-pastille-fond')]);
|
||||
// La pastille se pose sur un socle du fond du plateau : la trame ne
|
||||
// passe pas sous elle (§ 14.5), et reste peinte autour du socle.
|
||||
expect(fondPeintSous(pastilles[0].querySelector('[data-marque=pastille]'))).toEqual([jeton('--couleur-table-fond')]);
|
||||
const socle = trames[0].closest('[data-table]').querySelector('[data-socle]');
|
||||
expect(Number(socle.getAttribute('r'))).toBeLessThan(Number(enConflit.getAttribute('r')));
|
||||
expect(getComputedStyle(pastilles[0]).pointerEvents).toBe('none');
|
||||
demonterTout();
|
||||
|
||||
|
|
@ -638,7 +643,9 @@ describe('les seuils de rendu (§ 7.3, § 17 point 6, § 19.10)', () => {
|
|||
|
||||
describe('dessin et désignation (§ 7.3, § 13.5)', () => {
|
||||
test('le centre rendu de chaque siège est sa position ; aux points témoins, le DOM et designer désignent la même place ; une liste ne masque aucune chaise', () => {
|
||||
const charge = chargeDeTables([8, 8], { pas: 200 });
|
||||
// La table 2 si près que la liste de la table 1 passe sur ses chaises de
|
||||
// gauche.
|
||||
const charge = chargeDeTables([8, 8], { pas: 280 });
|
||||
const vue = { k: 1.5, tx: 300, ty: 350 };
|
||||
const modele = modeleDe(charge, { k: vue.k });
|
||||
const { svg, cadre, ecran } = monterPlan(modele, vue);
|
||||
|
|
|
|||
|
|
@ -465,7 +465,10 @@ describe('le plan est un seul arrêt de tabulation (§ 7.3, § 13.5)', () => {
|
|||
const tables = charge(m).tables;
|
||||
const premiere = tables[0];
|
||||
expect(actif(m)).toBe(`table-${premiere.id}`);
|
||||
const voisine = tables.find(({ position }) => position.x === premiere.position.x + 300 && position.y === premiere.position.y);
|
||||
// La voisine de droite : la plus proche à droite, sur la même rangée.
|
||||
const voisine = tables
|
||||
.filter(({ position }) => position.y === premiere.position.y && position.x > premiere.position.x)
|
||||
.reduce((plus, table) => (plus === undefined || table.position.x < plus.position.x ? table : plus), undefined);
|
||||
expect(voisine).toBeDefined();
|
||||
await touche('ArrowRight');
|
||||
flushSync();
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@
|
|||
//
|
||||
// Une table en conflit, quand le plan signale, porte un contour épaissi
|
||||
// (contourConflit) et une trame (trameConflit) par-dessus son plateau ; sa
|
||||
// pastille porte le nombre de violations, signalée ou non (§ 7.5).
|
||||
// pastille porte le nombre de violations, signalée ou non (§ 7.5). Sur la
|
||||
// trame, un socle [data-socle] du fond du plateau, centré sous la
|
||||
// pastille, la sépare des traits de la trame : la pastille se mesure
|
||||
// contre le fond du plateau (§ 14.5), et la trame reste peinte autour.
|
||||
// Le socle précède la marque de gel, qui se peint sur lui.
|
||||
//
|
||||
// Le groupe de la table porte id="table-<id>", celui d'une chaise
|
||||
// id="siege-<table>-<siège>" : la racine du plan, seul arrêt de tabulation,
|
||||
|
|
@ -39,6 +43,7 @@
|
|||
ECART_FOCUS,
|
||||
ECART_MISE_EN_EVIDENCE,
|
||||
RAYON_PASTILLE,
|
||||
RAYON_SOCLE_PASTILLE,
|
||||
TAILLE_TEXTE_PASTILLE_CM,
|
||||
TRACE_GEL,
|
||||
formesTable,
|
||||
|
|
@ -87,6 +92,7 @@
|
|||
{:else}
|
||||
<rect class="trame" data-marque="trameConflit" fill={trame} {...formes.plateau} />
|
||||
{/if}
|
||||
<circle class="socle" data-socle cx={formes.pastille.x} cy={formes.pastille.y} r={RAYON_SOCLE_PASTILLE} />
|
||||
{/if}
|
||||
{#if table.gelee}
|
||||
<path class="gel" data-marque="gel" d={TRACE_GEL} transform="translate({formes.gel.x},{formes.gel.y})" />
|
||||
|
|
@ -180,6 +186,12 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.socle {
|
||||
fill: var(--couleur-table-fond);
|
||||
stroke: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.gel {
|
||||
fill: var(--couleur-gel);
|
||||
pointer-events: none;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ export const LARGEUR_ECHELLE_PX = 120;
|
|||
/** Rayon du disque de la pastille, en cm de dessin : sous la marque de gel
|
||||
* d'une table de 2, la plus petite. */
|
||||
export const RAYON_PASTILLE = 15;
|
||||
/** Rayon du socle de la pastille, en cm de dessin : un disque du fond du
|
||||
* plateau, posé sur la trame d'une table signalée, sous la pastille, qui
|
||||
* l'en sépare (§ 14.5). Plus petit que le plateau le plus petit, de rayon
|
||||
* PLANCHER_DIAMETRE / 2 : la trame reste peinte autour. */
|
||||
export const RAYON_SOCLE_PASTILLE = 20;
|
||||
/** Corps du chiffre de la pastille, en cm de dessin. */
|
||||
export const TAILLE_TEXTE_PASTILLE_CM = 18;
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { creerTraduction } from '../../application/traduction.js';
|
|||
import { texteLivre } from '../../demo/livrees.js';
|
||||
import { constante } from '../../geometrie/constantes.js';
|
||||
import { regimeCibles } from '../../geometrie/designation.js';
|
||||
import { geometrieTable } from '../../geometrie/tables.js';
|
||||
import { PLANCHER_DIAMETRE, RAYON_POIGNEE, geometrieTable } from '../../geometrie/tables.js';
|
||||
import { indexerPlan } from '../../moteur/configuration.js';
|
||||
import { conflitsParTable } from '../../moteur/conflits.js';
|
||||
import { diagnostiquer } from '../../moteur/diagnostic.js';
|
||||
|
|
@ -26,7 +26,13 @@ import { analyser, configurationDepuisCharge, creerCharge } from '../../stockage
|
|||
import { planDepuisFichier, versFichier } from '../../stockage/placements.js';
|
||||
import { TABLES_INTERFACE } from '../libelles.js';
|
||||
import {
|
||||
ECART_ANNEAU,
|
||||
ECART_DESIGNATION,
|
||||
ECART_FOCUS,
|
||||
ECART_MISE_EN_EVIDENCE,
|
||||
LARGEUR_ECHELLE_PX,
|
||||
RAYON_PASTILLE,
|
||||
RAYON_SOCLE_PASTILLE,
|
||||
actionsPersonne,
|
||||
actionsPlace,
|
||||
ajusterLigne,
|
||||
|
|
@ -708,6 +714,36 @@ describe('formesTable : les formes d\u{2019}une table, en attributs SVG (§ 7.1,
|
|||
});
|
||||
});
|
||||
|
||||
describe('les marques autour d\u{2019}une table laissent la liste de noms à découvert (§ 7.4, § 14.5)', () => {
|
||||
// Le trait du carré du focus fait 3 px à l'écran (TablePlan) : sa moitié
|
||||
// extérieure couvre au plus 1,5 px, soit 1,5 / k cm, et les listes ne se
|
||||
// rendent qu'à k ≥ K_LISTES.
|
||||
const DEMI_TRAIT_FOCUS_CM = 1.5 / K_LISTES;
|
||||
|
||||
test('le focus, de la table ou de sa chaise la plus à droite, et la poignée finissent avant le bloc de noms', () => {
|
||||
for (const forme of ['ronde', 'carree']) {
|
||||
for (const places of [2, 3, 5, 8, 12, 16]) {
|
||||
const g = geometrieTable({ id: 1, numero: 1, forme, position: { x: 40, y: -70 } }, places);
|
||||
const droite = Math.max(...g.chaises.map((chaise) => chaise.x + chaise.rayon));
|
||||
const bords = {
|
||||
'focus de la table': g.centre.x + g.rayonEmprise + ECART_FOCUS + DEMI_TRAIT_FOCUS_CM,
|
||||
'focus de la chaise': droite + ECART_FOCUS + DEMI_TRAIT_FOCUS_CM,
|
||||
poignée: g.poignee.x + RAYON_POIGNEE,
|
||||
};
|
||||
for (const [quoi, bord] of Object.entries(bords)) {
|
||||
assert.ok(bord < g.origineListe.x, `${forme} de ${places} : ${quoi} jusqu'à ${bord}, liste dès ${g.origineListe.x}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('les autres marques d\u{2019}une chaise restent sous le focus ; le socle de la pastille, entre elle et le plus petit plateau', () => {
|
||||
for (const ecart of [ECART_ANNEAU, ECART_MISE_EN_EVIDENCE, ECART_DESIGNATION]) assert.ok(ecart < ECART_FOCUS, String(ecart));
|
||||
assert.ok(RAYON_PASTILLE < RAYON_SOCLE_PASTILLE);
|
||||
assert.ok(RAYON_SOCLE_PASTILLE < PLANCHER_DIAMETRE / 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('partiesLigne : les tspans d\u{2019}une ligne, dans l\u{2019}ordre', () => {
|
||||
test('chaque partie présente, nommée ; les absentes omises', () => {
|
||||
const ligne = { tete: '1. ', titulaire: null, suite: null, nom: 'Iris Ombrelle', avantTitre: ' \u{2014} ', titre: 'animateur', apresTitre: null };
|
||||
|
|
|
|||
1241
src/interface/transversal.navigateur.test.js
Normal file
1241
src/interface/transversal.navigateur.test.js
Normal file
File diff suppressed because it is too large
Load diff
582
test/navigateur/ecrans.js
Normal file
582
test/navigateur/ecrans.js
Normal file
|
|
@ -0,0 +1,582 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// Les écrans de l'itération, chacun atteint depuis le premier écran de
|
||||
// l'application montée sur le système simulé (§ 14.5, § 14.6, § 19.11). Un
|
||||
// écran est { nom, mode, atteindre(contexte) } : atteindre monte App par le
|
||||
// contexte, pose sur le disque simulé le fichier d'état qu'il lui faut, puis
|
||||
// mène l'application jusqu'à l'écran par les gestes de l'opérateur — le clic
|
||||
// d'un bouton désigné par son attribut data-…, une entrée réelle de
|
||||
// test/navigateur/entrees.js sur le plan, une touche — et rend l'élément qui
|
||||
// porte l'écran entier, bandeau compris : le conteneur du montage. mode dit
|
||||
// ce que le bandeau et le cadre en annoncent : null sans événement ouvert,
|
||||
// 'lecture', 'ecriture' ou 'bloque' sinon.
|
||||
//
|
||||
// Aucun délai fixe (§ 19.3) : chaque étape attend une condition du document,
|
||||
// image après image, et lève en la nommant quand elle ne vient pas. Les
|
||||
// données sont tirées des démonstrations livrées et de commandes de
|
||||
// l'application, sans aléa : deux parcours rendent le même écran.
|
||||
//
|
||||
// Deux états s'atteignent sans geste de l'opérateur : la mise en évidence
|
||||
// d'une personne passe par l'état de navigation du montage
|
||||
// (ui.mettreEnEvidence), qui n'écrit rien ; la question du dossier de
|
||||
// travail, qui se pose au démarrage, par les options du système simulé.
|
||||
import { flushSync } from 'svelte';
|
||||
import { appliquerCommande } from '../../src/application/commandes.js';
|
||||
import { ErreurAnnulee } from '../../src/application/generation.js';
|
||||
import { etatPlaces } from '../../src/application/places.js';
|
||||
import { texteLivre } from '../../src/demo/livrees.js';
|
||||
import { rechercher } from '../../src/moteur/recherche.js';
|
||||
import { serialiser } from '../../src/stockage/canonique.js';
|
||||
import { analyser, configurationDepuisCharge } from '../../src/stockage/document.js';
|
||||
import { SUFFIXES } from '../../src/stockage/noms.js';
|
||||
import { VERSION } from '../../src/version.genere.js';
|
||||
import urlCasLimites from '../../exemples/participants_cas_limites.csv?url';
|
||||
import { pointer, touche } from './entrees.js';
|
||||
import { monterApp } from './montage.js';
|
||||
|
||||
/** La base du fichier d'état de chaque événement d'épreuve. */
|
||||
export const BASE = 'soiree';
|
||||
|
||||
/** Le nom du CSV des cas limites que sert le dialogue d'import. */
|
||||
export const NOM_CAS_LIMITES = 'participants_cas_limites.csv';
|
||||
|
||||
/** Les sections d'un événement, dans l'ordre de la navigation. */
|
||||
export const SECTIONS_EVENEMENT = Object.freeze(['import', 'participants', 'tables', 'plan', 'diagnostic', 'generation', 'propositions']);
|
||||
|
||||
// L'exécutable publié du système simulé : son dossier data/ refuse
|
||||
// l'écriture, et la question du dossier de travail se pose au démarrage.
|
||||
const PUBLIE = 'E:\\soirees-epreuve';
|
||||
|
||||
// Images attendues au plus pour qu'une condition se réalise : bien au-delà
|
||||
// d'un rendu, d'une écriture simulée et d'une génération de la petite
|
||||
// démonstration.
|
||||
const IMAGES_MAX = 900;
|
||||
|
||||
// --- L'attente et les gestes -------------------------------------------------
|
||||
|
||||
const uneImage = () => new Promise((resoudre) => requestAnimationFrame(resoudre));
|
||||
|
||||
/** Attend, image après image, que condition() rende une valeur vraie sans
|
||||
* lever, et la rend ; lève en nommant quoi après IMAGES_MAX images. */
|
||||
export async function attendre(quoi, condition) {
|
||||
let derniere = null;
|
||||
for (let image = 0; image < IMAGES_MAX; image += 1) {
|
||||
flushSync();
|
||||
try {
|
||||
const valeur = condition();
|
||||
if (valeur) return valeur;
|
||||
} catch (erreur) {
|
||||
derniere = erreur;
|
||||
}
|
||||
await uneImage();
|
||||
}
|
||||
throw new Error(`écran : ${quoi} ne vient pas${derniere === null ? '' : ` (${derniere.message})`}`);
|
||||
}
|
||||
|
||||
// Le premier élément du montage qu'atteint le sélecteur, attendu.
|
||||
const element = (m, selecteur) => attendre(selecteur, () => m.racine.querySelector(selecteur));
|
||||
|
||||
/** Clique le bouton du montage que désigne le sélecteur, une fois présent,
|
||||
* par son clic HTML, puis laisse le rendu suivre. */
|
||||
export async function cliquer(m, selecteur) {
|
||||
const bouton = await element(m, selecteur);
|
||||
bouton.click();
|
||||
flushSync();
|
||||
await uneImage();
|
||||
return bouton;
|
||||
}
|
||||
|
||||
/** Le centre de la boîte d'un élément, en coordonnées de la fenêtre, une
|
||||
* fois l'élément amené dans la fenêtre. */
|
||||
export async function centre(cible) {
|
||||
const dedans = () => {
|
||||
const b = cible.getBoundingClientRect();
|
||||
return b.width > 0 && b.left >= 0 && b.top >= 0 && b.right <= innerWidth && b.bottom <= innerHeight;
|
||||
};
|
||||
if (!dedans()) {
|
||||
cible.scrollIntoView({ block: 'center', inline: 'center' });
|
||||
await attendre('l’élément dans la fenêtre', dedans);
|
||||
}
|
||||
const b = cible.getBoundingClientRect();
|
||||
return { x: b.left + b.width / 2, y: b.top + b.height / 2 };
|
||||
}
|
||||
|
||||
// Le plateau d'une table, qu'il signale un conflit ou non.
|
||||
const plateau = (m, table) =>
|
||||
m.racine.querySelector(`[data-table="${table}"] [data-marque="contourTable"], [data-table="${table}"] [data-marque="contourConflit"]`);
|
||||
const chaise = (m, table, siege) => m.racine.querySelector(`[data-siege="${table}:${siege}"]`);
|
||||
|
||||
// Un appui relâché sur place, au centre d'un élément : un clic de pointeur
|
||||
// réel, sous le seuil de glissement.
|
||||
async function cliquerPointeur(cible) {
|
||||
const { x, y } = await centre(cible);
|
||||
await pointer('presser', x, y);
|
||||
await pointer('relacher', x, y);
|
||||
flushSync();
|
||||
await uneImage();
|
||||
}
|
||||
|
||||
// --- Les événements d'épreuve -----------------------------------------------
|
||||
|
||||
const executer = (charge, nom, args) => appliquerCommande(nom, charge, args).charge;
|
||||
const demonstration = (cle) => analyser(texteLivre(cle)).charge;
|
||||
|
||||
/**
|
||||
* La petite démonstration, variante conflit, avec une désignation (la
|
||||
* personne 1, « hôte », à la table 1, siège 1), une réservation d'un tour (la
|
||||
* personne 2 à la table 2, siège 2, au tour 1), un titre non pourvu
|
||||
* (« animateur », table 3, siège 3) et une proposition, tirée de la graine 2
|
||||
* : au tour 1, les tables 1 et 2 sont en conflit, les tables 3 et 4 saines.
|
||||
*/
|
||||
export function chargePrincipale() {
|
||||
let charge = demonstration('petite-conflit');
|
||||
charge = executer(charge, 'designer', { participant: 1, table: 1, siege: 1, libelle: 'hôte' });
|
||||
charge = executer(charge, 'reserver', { participant: 2, table: 2, siege: 2, portee: 'tour', tour: 1 });
|
||||
charge = executer(charge, 'poserTitre', { table: 3, siege: 3, libelle: 'animateur' });
|
||||
const configuration = configurationDepuisCharge(charge);
|
||||
const propositions = rechercher(configuration, { graine: 2, nombre: 1, arret: 300, historique: 1000 });
|
||||
return appliquerCommande('enregistrerGeneration', charge, { propositions, configuration, produitVersion: VERSION.affichee }).charge;
|
||||
}
|
||||
|
||||
// La principale, sa proposition retenue, puis le plan bloqué.
|
||||
function chargeBloquee() {
|
||||
const principale = chargePrincipale();
|
||||
const charge = executer(principale, 'retenirProposition', { id: principale.propositions[0].id });
|
||||
return executer(charge, 'changerEtat', { etat: 'bloque' });
|
||||
}
|
||||
|
||||
// La principale, sa proposition retenue, puis une personne ajoutée : le
|
||||
// retenu ne la place pas, et l'ouverture avertit de la dérive.
|
||||
function chargeEnDerive() {
|
||||
const principale = chargePrincipale();
|
||||
const charge = executer(principale, 'retenirProposition', { id: principale.propositions[0].id });
|
||||
return executer(charge, 'ajouterParticipant', { champs: { nom: 'Ombrelle', prenom: 'Iris' } });
|
||||
}
|
||||
|
||||
// La petite démonstration, la table 4 réservée en entier pour tous les
|
||||
// tours : elle est gelée.
|
||||
function chargeGelee() {
|
||||
let charge = demonstration('petite');
|
||||
for (const participant of [1, 2, 3]) {
|
||||
charge = executer(charge, 'reserver', { participant, table: 4, siege: participant, portee: 'tous', tour: null });
|
||||
}
|
||||
return charge;
|
||||
}
|
||||
|
||||
/** Le fichier d'état d'une charge, posé dans les Documents sous BASE. */
|
||||
export const fichierEtat = (charge, base = BASE) => ({
|
||||
racine: 'documents',
|
||||
chemin: `${base}${SUFFIXES.etat}`,
|
||||
texte: serialiser(charge, { revision: 1, produitVersion: VERSION.affichee }),
|
||||
});
|
||||
|
||||
/** Les octets du CSV des cas limites, tels que le fichier les porte. */
|
||||
export async function octetsCasLimites() {
|
||||
return new Uint8Array(await (await fetch(urlCasLimites)).arrayBuffer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Un exécuteur de génération que l'épreuve ne résout pas : la génération
|
||||
* reste en cours tant que le montage vit ; annuler la rejette par
|
||||
* ErreurAnnulee.
|
||||
*/
|
||||
export function executeurSuspendu() {
|
||||
return {
|
||||
lancer() {
|
||||
let rejeter;
|
||||
const resultat = new Promise((_, ko) => {
|
||||
rejeter = ko;
|
||||
});
|
||||
return {
|
||||
surAvancement: () => () => {},
|
||||
resultat,
|
||||
annuler() {
|
||||
rejeter(new ErreurAnnulee());
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// --- Les parcours -------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Monte App sur la charge, ouvre l'événement depuis la liste, passe en
|
||||
* écriture si demandé, montre la première proposition au plan quand la
|
||||
* charge n'a pas de retenu, puis ouvre la section. Rend le montage.
|
||||
*
|
||||
* @param {Object} contexte celui de creerContexte
|
||||
* @param {Object} charge
|
||||
* @param {{ecriture?: boolean, section?: string, options?: Object}} [reglages]
|
||||
* options : celles de monterApp, ses fichiers ajoutés à celui de la charge
|
||||
*/
|
||||
export async function ouvrirEvenement(contexte, charge, { ecriture = false, section = 'plan', options = {} } = {}) {
|
||||
const m = await contexte.monter({ ...options, fichiers: [fichierEtat(charge), ...(options.fichiers ?? [])] });
|
||||
await cliquer(m, `[data-evenement="${BASE}"] [data-commande="ouvrir"]`);
|
||||
await element(m, '[data-ecran="evenement"]');
|
||||
if (ecriture) {
|
||||
await cliquer(m, '[data-place-fixe] [data-commande="modifier"]');
|
||||
await element(m, '[data-place-fixe] [data-commande="lecture"]');
|
||||
}
|
||||
if (charge.retenu === null && charge.propositions.length > 0) {
|
||||
await cliquer(m, '[data-commande="section"][data-cible="propositions"]');
|
||||
await cliquer(m, `[data-proposition="${charge.propositions[0].id}"] [data-commande="voir"]`);
|
||||
await element(m, '[data-section="plan"]');
|
||||
}
|
||||
await cliquer(m, `[data-commande="section"][data-cible="${section}"]`);
|
||||
await element(m, `[data-section="${section}"]`);
|
||||
if (section === 'plan') {
|
||||
await attendre('le plan posé', () => {
|
||||
const vue = m.racine.querySelector('[data-vue]');
|
||||
return vue?.getAttribute('transform') && m.racine.querySelectorAll('[data-table]').length === charge.tables.length;
|
||||
});
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
// Un écran d'événement sur la principale, sur la section donnée, au mode
|
||||
// donné ; la bloquée pour 'bloque'.
|
||||
function ecranDeSection(section, mode) {
|
||||
return {
|
||||
nom: `evenement.${section}.${mode}`,
|
||||
mode,
|
||||
async atteindre(contexte) {
|
||||
const charge = mode === 'bloque' ? chargeBloquee() : chargePrincipale();
|
||||
const m = await ouvrirEvenement(contexte, charge, { ecriture: mode === 'ecriture', section });
|
||||
return m.racine;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// La principale en écriture, sur le plan.
|
||||
const planEnEcriture = (contexte, options) => ouvrirEvenement(contexte, chargePrincipale(), { ecriture: true, section: 'plan', options });
|
||||
|
||||
/**
|
||||
* Le contexte d'un parcours : il monte App dans la langue et le thème
|
||||
* donnés, garde chaque montage et chaque nettoyage, et défait tout par
|
||||
* defaire(), les nettoyages d'abord, dans l'ordre inverse.
|
||||
*
|
||||
* @param {{langue?: 'fr'|'temoin', theme?: 'clair'|'sombre'}} [reglages]
|
||||
*/
|
||||
export function creerContexte({ langue = 'fr', theme = 'clair' } = {}) {
|
||||
const montages = [];
|
||||
const nettoyages = [];
|
||||
return {
|
||||
langue,
|
||||
montages,
|
||||
/** Le dernier montage : celui que l'écran montre. */
|
||||
get montage() {
|
||||
return montages.at(-1) ?? null;
|
||||
},
|
||||
async monter(options = {}) {
|
||||
const m = await monterApp({ langue, theme, ...options });
|
||||
montages.push(m);
|
||||
return m;
|
||||
},
|
||||
nettoyer(fn) {
|
||||
nettoyages.push(fn);
|
||||
},
|
||||
/** Ce que le collecteur a reçu depuis chaque montage. */
|
||||
erreurs() {
|
||||
return montages.flatMap((m) => m.erreurs());
|
||||
},
|
||||
async defaire() {
|
||||
for (const fn of nettoyages.splice(0).reverse()) await fn();
|
||||
for (const m of montages.splice(0).reverse()) m.demonter();
|
||||
window.scrollTo(0, 0);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Les écrans de l'itération. Leur union rend chaque sorte de MARQUES. */
|
||||
export const ECRANS = Object.freeze([
|
||||
{
|
||||
nom: 'accueil.liste',
|
||||
mode: null,
|
||||
async atteindre(contexte) {
|
||||
const m = await contexte.monter({ fichiers: [fichierEtat(chargePrincipale()), fichierEtat(chargeGelee(), 'gelee')] });
|
||||
await element(m, `[data-evenement="${BASE}"]`);
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'accueil.vide',
|
||||
mode: null,
|
||||
async atteindre(contexte) {
|
||||
const m = await contexte.monter();
|
||||
await element(m, '[data-ecran="accueil"] [data-dossier-travail]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'apropos',
|
||||
mode: null,
|
||||
async atteindre(contexte) {
|
||||
const m = await contexte.monter();
|
||||
await cliquer(m, '[data-bandeau] [data-commande="apropos"]');
|
||||
await element(m, '[data-ecran="apropos"]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'recuperation',
|
||||
mode: null,
|
||||
async atteindre(contexte) {
|
||||
const sain = fichierEtat(chargePrincipale());
|
||||
const tronque = { ...sain, texte: sain.texte.slice(0, Math.floor(sain.texte.length / 2)) };
|
||||
const precedent = { ...sain, chemin: `${BASE}${SUFFIXES.precedent}` };
|
||||
const m = await contexte.monter({ fichiers: [tronque, precedent] });
|
||||
await cliquer(m, `[data-evenement="${BASE}"] [data-commande="ouvrir"]`);
|
||||
await element(m, '[data-ecran="recuperation"]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'dossier.question',
|
||||
mode: null,
|
||||
async atteindre(contexte) {
|
||||
const m = await contexte.monter({
|
||||
systeme: { executable: PUBLIE, sonde: { portable: 'EACCES' } },
|
||||
fichiers: [{ ...fichierEtat(chargePrincipale()), racine: 'portable' }],
|
||||
});
|
||||
await element(m, '[data-question]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
...SECTIONS_EVENEMENT.flatMap((section) => ['lecture', 'ecriture', 'bloque'].map((mode) => ecranDeSection(section, mode))),
|
||||
{
|
||||
nom: 'plan.tableGelee',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
const m = await ouvrirEvenement(contexte, chargeGelee(), { ecriture: true, section: 'plan' });
|
||||
await element(m, '[data-table="4"] [data-marque="gel"]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'plan.miseEnEvidence',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
const m = await planEnEcriture(contexte);
|
||||
const { tour, proposition } = m.ui.etat();
|
||||
const occupant = etatPlaces(m.seance.etat().charge, { tour, proposition }).tables[3].chaises[0].occupant;
|
||||
m.ui.mettreEnEvidence(occupant);
|
||||
await element(m, '[data-marque="miseEnEvidence"]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'plan.focus',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
const m = await planEnEcriture(contexte);
|
||||
(await element(m, '[data-commande="pleinEcran"]')).focus();
|
||||
await touche('Tab');
|
||||
await element(m, '[data-plan] [data-marque="focus"]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'plan.panneauPlace',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
const m = await planEnEcriture(contexte);
|
||||
await cliquerPointeur(chaise(m, 3, 3));
|
||||
await element(m, '[data-panneau="place"]');
|
||||
await element(m, '[data-siege="3:3"] [data-marque="selection"]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'plan.menuPersonne',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
const m = await planEnEcriture(contexte);
|
||||
await cliquerPointeur(chaise(m, 4, 1));
|
||||
await element(m, '[data-panneau="place"] [data-menu]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'plan.panneauTable',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
const m = await planEnEcriture(contexte);
|
||||
await cliquerPointeur(plateau(m, 3));
|
||||
await element(m, '[data-panneau="table"]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'plan.infobulle',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
const m = await planEnEcriture(contexte);
|
||||
const { x, y } = await centre(plateau(m, 1));
|
||||
await pointer('bouger', x, y);
|
||||
await attendre('l’infobulle', () => {
|
||||
const infobulle = m.racine.querySelector('[role="tooltip"]');
|
||||
return infobulle !== null && !infobulle.hidden && infobulle.getBoundingClientRect().width > 0;
|
||||
});
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'plan.glissement',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
const m = await planEnEcriture(contexte);
|
||||
const depuis = await centre(chaise(m, 3, 2));
|
||||
const vers = await centre(chaise(m, 4, 2));
|
||||
await pointer('presser', depuis.x, depuis.y);
|
||||
contexte.nettoyer(async () => {
|
||||
await touche('Escape');
|
||||
await pointer('relacher', vers.x, vers.y);
|
||||
});
|
||||
for (let pas = 1; pas <= 10; pas += 1) {
|
||||
await pointer('bouger', depuis.x + ((vers.x - depuis.x) * pas) / 10, depuis.y + ((vers.y - depuis.y) * pas) / 10);
|
||||
}
|
||||
await element(m, '[data-fantome] [data-marque="fantome"]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'plan.refusPlace',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
const m = await planEnEcriture(contexte);
|
||||
const depuis = await centre(chaise(m, 3, 1));
|
||||
const vers = await centre(chaise(m, 1, 1));
|
||||
await pointer('presser', depuis.x, depuis.y);
|
||||
for (let pas = 1; pas <= 10; pas += 1) {
|
||||
await pointer('bouger', depuis.x + ((vers.x - depuis.x) * pas) / 10, depuis.y + ((vers.y - depuis.y) * pas) / 10);
|
||||
}
|
||||
await pointer('relacher', vers.x, vers.y);
|
||||
await element(m, '[data-surface] [data-refus="PLACE_RESERVEE"]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'participants.refusChamp',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
const m = await ouvrirEvenement(contexte, chargePrincipale(), { ecriture: true, section: 'participants' });
|
||||
await cliquer(m, '[data-section="participants"] [data-commande="ajouter"]');
|
||||
await element(m, '[data-section="participants"] [data-refus="NOM_REQUIS"]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'participants.fiche',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
const m = await ouvrirEvenement(contexte, chargePrincipale(), { ecriture: true, section: 'participants' });
|
||||
await cliquer(m, '[data-participant="1"] [data-commande="fiche"]');
|
||||
await element(m, '[data-fiche="1"]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'import.apercu',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
const m = await ouvrirEvenement(contexte, chargePrincipale(), { ecriture: true, section: 'import' });
|
||||
const octets = await octetsCasLimites();
|
||||
m.fs.choisirFichierAImporter = async () => ({ nom: NOM_CAS_LIMITES, octets });
|
||||
await cliquer(m, '[data-commande="choisirFichier"]');
|
||||
await element(m, '[data-apercu] [data-refusees]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'import.rapport',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
const m = await ouvrirEvenement(contexte, chargePrincipale(), { ecriture: true, section: 'import' });
|
||||
const octets = await octetsCasLimites();
|
||||
m.fs.choisirFichierAImporter = async () => ({ nom: NOM_CAS_LIMITES, octets });
|
||||
await cliquer(m, '[data-commande="choisirFichier"]');
|
||||
await cliquer(m, '[data-apercu] [data-commande="importer"]');
|
||||
await attendre('le rapport, ou la confirmation du remplacement', () =>
|
||||
m.racine.querySelector('[data-rapport]') ?? m.racine.querySelector('dialog[open]'),
|
||||
);
|
||||
if (m.racine.querySelector('[data-rapport]') === null) await cliquer(m, 'dialog[open] [data-commande="confirmer"]');
|
||||
await element(m, '[data-rapport]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'participants.confirmer',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
// Supprimer une personne qui tient une réservation se confirme dans
|
||||
// une fenêtre, qui nomme la réservation.
|
||||
const m = await ouvrirEvenement(contexte, chargePrincipale(), { ecriture: true, section: 'participants' });
|
||||
await cliquer(m, '[data-participant="1"] [data-commande="supprimerParticipant"]');
|
||||
await element(m, 'dialog[open]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'evenement.avertissements',
|
||||
mode: 'lecture',
|
||||
async atteindre(contexte) {
|
||||
const m = await ouvrirEvenement(contexte, chargeEnDerive(), { section: 'plan' });
|
||||
await element(m, '[data-ecran="evenement"] [data-avertissement]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'generation.enCours',
|
||||
mode: 'ecriture',
|
||||
async atteindre(contexte) {
|
||||
const m = await ouvrirEvenement(contexte, chargePrincipale(), {
|
||||
ecriture: true,
|
||||
section: 'generation',
|
||||
options: { executeur: executeurSuspendu() },
|
||||
});
|
||||
await cliquer(m, '[data-section="generation"] [data-commande="generer"]');
|
||||
await attendre('la génération lancée, ou sa question', () =>
|
||||
m.racine.querySelector('[data-avancement]') ?? m.racine.querySelector('[data-question="longue"]'),
|
||||
);
|
||||
if (m.racine.querySelector('[data-avancement]') === null) await cliquer(m, '[data-commande="confirmerGeneration"]');
|
||||
await element(m, '[data-avancement]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'refus.modeLecture',
|
||||
mode: 'lecture',
|
||||
async atteindre(contexte) {
|
||||
const m = await ouvrirEvenement(contexte, chargePrincipale(), { section: 'participants' });
|
||||
await cliquer(m, '[data-etat="brouillon"] [data-commande="etat"]');
|
||||
await element(m, '[data-refus="MODE_LECTURE"]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'refus.bloque',
|
||||
mode: 'bloque',
|
||||
async atteindre(contexte) {
|
||||
const m = await ouvrirEvenement(contexte, chargeBloquee(), { section: 'participants' });
|
||||
await cliquer(m, '[data-etat="propose"] [data-commande="etat"]');
|
||||
await element(m, '[data-refus="LECTURE_SEULE"]');
|
||||
return m.racine;
|
||||
},
|
||||
},
|
||||
{
|
||||
nom: 'refus.verrou',
|
||||
mode: 'lecture',
|
||||
async atteindre(contexte) {
|
||||
const premier = await ouvrirEvenement(contexte, chargePrincipale(), { ecriture: true, section: 'participants' });
|
||||
premier.racine.hidden = true;
|
||||
const second = await contexte.monter({ systeme: { disque: premier.disque } });
|
||||
await cliquer(second, `[data-evenement="${BASE}"] [data-commande="ouvrir"]`);
|
||||
await element(second, '[data-ecran="evenement"]');
|
||||
await cliquer(second, '[data-place-fixe] [data-commande="modifier"]');
|
||||
await element(second, '[data-refus="VERROU_PRIS"]');
|
||||
return second.racine;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
|
@ -226,11 +226,13 @@ function couleursDeLaPeinture(valeur, element) {
|
|||
|
||||
/** 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. */
|
||||
* rend les couleurs que peignent ses enfants, un dégradé ses arrêts. Une
|
||||
* <line> n'a pas d'aire : elle ne peint que par son trait, et son fill
|
||||
* calculé, noir par défaut, rend une liste vide. */
|
||||
export function couleursPeintes(element) {
|
||||
const style = getComputedStyle(element);
|
||||
return {
|
||||
fill: couleursDeLaPeinture(style.fill, element),
|
||||
fill: element instanceof SVGLineElement ? [] : couleursDeLaPeinture(style.fill, element),
|
||||
stroke: couleursDeLaPeinture(style.stroke, element),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,6 +176,24 @@ describe('couleursPeintes', () => {
|
|||
[20, 30, 90, 1],
|
||||
]);
|
||||
});
|
||||
|
||||
test("une ligne ne peint que son trait : le noir par défaut de son fill n'est compté ni seul, ni dans un motif", () => {
|
||||
monter(
|
||||
`<svg xmlns="${SVG}" width="80" height="80"><defs>` +
|
||||
`<pattern id="epreuve-hachure-ligne" width="8" height="8" patternUnits="userSpaceOnUse">` +
|
||||
`<rect width="8" height="8" fill="rgb(250, 240, 200)"/>` +
|
||||
`<line x1="4" y1="0" x2="4" y2="8" stroke="rgb(20, 30, 90)" stroke-width="3"/>` +
|
||||
`</pattern></defs>` +
|
||||
`<line data-ligne x1="0" y1="0" x2="80" y2="80" stroke="rgb(60, 70, 80)"/>` +
|
||||
`<rect data-forme width="80" height="80" fill="url(#epreuve-hachure-ligne)"/></svg>`,
|
||||
);
|
||||
expect(getComputedStyle($('[data-ligne]')).fill).toBe('rgb(0, 0, 0)');
|
||||
expect(couleursPeintes($('[data-ligne]'))).toEqual({ fill: [], stroke: [[60, 70, 80, 1]] });
|
||||
expect(couleursPeintes($('[data-forme]')).fill).toEqual([
|
||||
[250, 240, 200, 1],
|
||||
[20, 30, 90, 1],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fondPeintSous', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue