[ADD] captures: print preview of a draft and of the retained plan
The walkthrough photographs the printed plan twice from the same scroll position: a proposal not retained carries BROUILLON and its own footer, the retained plan carries neither watermark nor proposal timestamp. The visible condition is judged on the whole box of each element, so a label cut at the window edge fails the walkthrough; capture 19 now aims at a seat that keeps the ghost and its label inside the plan. A failed preview step closes the preview before the next tests run. Checked: node 3777, browser 756, node-long 101 from the index alone. --- FR --- [ADD] captures : aperçu imprimé d'un brouillon et du placement retenu Le parcours photographie deux fois le plan imprimé au même défilement : une proposition non retenue porte BROUILLON et son propre pied, le retenu ne porte ni filigrane ni horodatage de la proposition. La condition visible se juge sur la boîte entière de chaque élément : un libellé coupé au bord de la fenêtre fait échouer le parcours ; la capture 19 vise un siège qui garde le fantôme et son libellé dans le plan. Un échec à l'aperçu le referme avant les épreuves suivantes. Vérifié : node 3777, navigateur 756, longues 101 depuis l'index seul. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
365561272e
commit
b72f3fe784
11 changed files with 376 additions and 36 deletions
|
|
@ -360,6 +360,24 @@ en filigrane ; une planche tirée d’une proposition l’est toujours. Chaque p
|
|||
imprimée porte en pied le nom et la date de l’événement, le tour, l’instant où
|
||||
le placement a pris sa forme, et la version du logiciel.
|
||||
|
||||
L’aperçu imprime ce que le plan montre. Une proposition ouverte depuis la
|
||||
section « Propositions » s’imprime en brouillon, même quand un autre placement
|
||||
est retenu :
|
||||
|
||||

|
||||
|
||||
*Le filigrane BROUILLON couvre chaque page. Sous le plan, le repère de la
|
||||
page, l’échelle graphique et la mention « disposition relative » ; en pied, le
|
||||
tour et l’instant où cette proposition a été générée.*
|
||||
|
||||
Revenu au placement retenu, l’aperçu sort sans filigrane :
|
||||
|
||||

|
||||
|
||||
*Le même cadrage, sur le placement retenu : le filigrane a disparu, et le pied
|
||||
porte l’instant de ce placement. C’est cette page qui part à l’imprimante pour
|
||||
le montage de la salle.*
|
||||
|
||||
Une fois le plan imprimé, bloquez-le : choisissez l’état « Bloqué » en haut de
|
||||
l’événement. Plus rien ne le modifie, jusqu’à « Débloquer ».
|
||||
|
||||
|
|
|
|||
|
|
@ -86,11 +86,12 @@ sur les captures.
|
|||
| `--couleur-cadre-ecriture` | marque | 3 | 4,78 | 02-import-apercu | 9,10 | 02-import-apercu |
|
||||
| `--couleur-cadre-lecture` | marque | 3 | 5,29 | 01b-passer-en-ecriture | 7,34 | 01b-passer-en-ecriture |
|
||||
| `--couleur-chaise-pleine` | marque | 3 | 15,38 | 07a-cibles-sieges | 14,94 | 07a-cibles-sieges |
|
||||
| `--couleur-chaise-trait` | marque | 3 | 7,58 | 07-plan-tables | 8,89 | 07-plan-tables |
|
||||
| `--couleur-conflit-appartenance` | marque | 3 | 8,92 | 19-ajustement | 6,81 | 19-ajustement |
|
||||
| `--couleur-contour-fort` | marque | 3 | 12,54 | 19-ajustement | 11,62 | 19-ajustement |
|
||||
| `--couleur-chaise-trait` | marque | 3 | 7,58 | 07-plan-tables | 8,13 | 22-plan-brouillon |
|
||||
| `--couleur-conflit-appartenance` | marque | 3 | 8,92 | 20-mode-lecture | 6,81 | 20-mode-lecture |
|
||||
| `--couleur-contour-fort` | marque | 3 | 12,54 | 20-mode-lecture | 11,62 | 20-mode-lecture |
|
||||
| `--couleur-echelle` | marque | 3 | 15,10 | 07-plan-tables | 14,62 | 07-plan-tables |
|
||||
| `--couleur-fantome` | marque | 3 | 4,34 | 19-ajustement | 4,10 | 19-ajustement |
|
||||
| `--couleur-filigrane` | marque | 3 | 5,10 | 22-plan-brouillon | 5,10 | 22-plan-brouillon |
|
||||
| `--couleur-hachure` | marque | 3 | 16,48 | 08-animateurs-designes | 15,21 | 08-animateurs-designes |
|
||||
| `--couleur-marque` | marque | 3 | 3,63 | 07a-cibles-sieges | 3,50 | 07a-cibles-sieges |
|
||||
| `--couleur-page-texte` | texte | 4,5 | 15,10 | 01-premier-ecran | 11,39 | 19-ajustement |
|
||||
|
|
@ -103,6 +104,7 @@ sur les captures.
|
|||
| `--couleur-selection` | marque | 3 | 4,66 | 08-animateurs-designes | 4,18 | 08-animateurs-designes |
|
||||
| `--couleur-table-trait` | marque | 3 | 7,58 | 07-plan-tables | 5,59 | 07-plan-tables |
|
||||
| `rgb(0, 0, 0)` | texte | 4,5 | 18,26 | 09-refus-sur-place | — | — |
|
||||
| `rgb(28, 33, 40)` | texte | 4,5 | — | — | 16,18 | 22-plan-brouillon |
|
||||
|
||||
<!-- engendre:fin contrastes -->
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 151 KiB |
BIN
doc/captures/22-plan-brouillon.png
Normal file
BIN
doc/captures/22-plan-brouillon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
BIN
doc/captures/23-plan-retenu.png
Normal file
BIN
doc/captures/23-plan-retenu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
File diff suppressed because one or more lines are too long
|
|
@ -80,7 +80,7 @@ export const REPORTEES = Object.freeze([
|
|||
|
||||
// Captures déclarées dans CAPTURES et pas encore photographiées : la
|
||||
// publication et le contrôle d'ordre les admettent absentes du manifeste.
|
||||
export const A_VENIR = Object.freeze(['22-plan-brouillon', '23-plan-retenu', '24-badges-controle', '25-badges-planche']);
|
||||
export const A_VENIR = Object.freeze(['24-badges-controle', '25-badges-planche']);
|
||||
|
||||
// --- Forme canonique et empreintes ---------------------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -142,8 +142,9 @@ describe('manifeste : la liste déclarée (§ 19.3, Q8 a)', () => {
|
|||
for (const reportee of REPORTEES) assert.ok(Object.isFrozen(reportee), reportee.fichier);
|
||||
});
|
||||
|
||||
test('A_VENIR nomme les quatre captures de l’étape 12, toutes déclarées', () => {
|
||||
assert.deepEqual(A_VENIR, ['22-plan-brouillon', '23-plan-retenu', '24-badges-controle', '25-badges-planche']);
|
||||
test('A_VENIR nomme les deux captures de la planche, à l’étape 12, déclarées ; 22 et 23 sont photographiées', () => {
|
||||
assert.deepEqual(A_VENIR, ['24-badges-controle', '25-badges-planche']);
|
||||
for (const fichier of A_VENIR) assert.equal(CAPTURES.find((c) => c.fichier === fichier)?.etape, '12', fichier);
|
||||
const declarees = new Set(CAPTURES.map((c) => c.fichier));
|
||||
for (const fichier of A_VENIR) assert.ok(declarees.has(fichier), fichier);
|
||||
assert.ok(Object.isFrozen(A_VENIR));
|
||||
|
|
@ -319,9 +320,15 @@ describe('manifeste : l’ordre contre les étapes du § 2.1 (§ 19.3)', () => {
|
|||
assert.deepEqual(verifierOrdre(manifesteDeclare({ sans: A_VENIR })), []);
|
||||
});
|
||||
|
||||
test('A_VENIR vidé, l’étape 12 sans capture est une faute', () => {
|
||||
const fautes = verifierOrdre(manifesteDeclare({ sans: A_VENIR }), { aVenir: [] });
|
||||
assert.deepEqual(fautes, [{ chemin: CHEMIN_MANIFESTE, faute: 'étape 12 : aucune capture ni reportée' }]);
|
||||
test('A_VENIR vidé, les captures d’A_VENIR absentes : 22 et 23 couvrent l’étape 12', () => {
|
||||
assert.deepEqual(verifierOrdre(manifesteDeclare({ sans: A_VENIR }), { aVenir: [] }), []);
|
||||
});
|
||||
|
||||
test('A_VENIR vidé, sans 22 ni 23 : l’étape 12 sans capture est une faute', () => {
|
||||
const photographiees = ['22-plan-brouillon', '23-plan-retenu'];
|
||||
assert.deepEqual(verifierOrdre(manifesteDeclare({ sans: [...photographiees, ...A_VENIR] }), { aVenir: [] }), [
|
||||
{ chemin: CHEMIN_MANIFESTE, faute: 'étape 12 : aucune capture ni reportée' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('une étape sans capture ni reportée est une faute qui la nomme', () => {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@
|
|||
// Le parcours est le parcours photographié (§ 19.1, § 19.3) : il suit les
|
||||
// captures du § 19.3 que l'itération photographie — 01, 01b, 02, 02b, 03 à
|
||||
// 07, 07b, 08 à 10, les deux 07a sur la grande démonstration que 10 a
|
||||
// chargée, 11, 19, 20 —, puis l'ordre de lecture du § 2.1 sur
|
||||
// chargée, 11, 19, 20, puis 22 et 23, l'aperçu avant impression d'une
|
||||
// proposition et du retenu —, puis l'ordre de lecture du § 2.1 sur
|
||||
// l'événement retenu, les fenêtres — aucune pendant un geste non
|
||||
// destructeur, une qui nomme ce qui part pour chaque geste qui détruit —,
|
||||
// enfin chaque famille de geste modifiant en lecture puis sur un plan
|
||||
|
|
@ -39,10 +40,12 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } fr
|
|||
import { cdp, commands } from 'vitest/browser';
|
||||
import { flushSync } from 'svelte';
|
||||
import { bilanRemplacement, cleNormalisee } from '../application/csv.js';
|
||||
import { creerFormat } from '../application/format.js';
|
||||
import { creerExecuteurDirect } from '../application/generation.js';
|
||||
import { TABLES_APPLICATION } from '../application/libelles.js';
|
||||
import { nomAffiche } from '../application/personnes.js';
|
||||
import { etatPlaces } from '../application/places.js';
|
||||
import { estBrouillon } from '../application/sorties.js';
|
||||
import { creerTraduction } from '../application/traduction.js';
|
||||
import { versionVisible } from '../application/version.js';
|
||||
import { apercevoir } from '../csv/apercu.js';
|
||||
|
|
@ -66,7 +69,7 @@ import { SUFFIXES } from '../stockage/noms.js';
|
|||
import { planDepuisFichier } from '../stockage/placements.js';
|
||||
import { VERSION } from '../version.genere.js';
|
||||
import urlCp1252 from '../../exemples/participants_cas_limites_cp1252.csv?url';
|
||||
import { CADRE, capturesPrises, cloreCaptures, modeFourni, photographier } from '../../test/navigateur/captures.js';
|
||||
import { CADRE, capturesPrises, cloreCaptures, entiereDansLaFenetre, modeFourni, photographier } from '../../test/navigateur/captures.js';
|
||||
import { NOM_CAS_LIMITES, attendre, centre, octetsCasLimites } from '../../test/navigateur/ecrans.js';
|
||||
import { fixerCadre, pointer, touche } from '../../test/navigateur/entrees.js';
|
||||
import { poserFeuille } from '../../test/navigateur/feuilles.js';
|
||||
|
|
@ -84,6 +87,10 @@ const NOM_SOIREE = 'Soirée d\u{2019}essai';
|
|||
const GRAINE = 4242;
|
||||
const ARRET = 300;
|
||||
|
||||
// L'écart de l'horloge pilotée entre la génération et la retenue : plus
|
||||
// d'une minute, la précision d'un horodatage mis en forme.
|
||||
const ECART_RETENUE_MS = 2 * 60 * 1000;
|
||||
|
||||
// Le CSV des cas limites enregistré en windows-1252 (§ 10.3, capture 02b).
|
||||
const NOM_CP1252 = 'participants_cas_limites_cp1252.csv';
|
||||
|
||||
|
|
@ -272,9 +279,8 @@ async function cliquerPointeur(element) {
|
|||
await images(2);
|
||||
}
|
||||
|
||||
// Presse au point depuis et avance par pas jusqu'au point vers, sans relâcher.
|
||||
async function presserEtBouger(depuis, vers, pas = 10) {
|
||||
await pointer('presser', depuis.x, depuis.y);
|
||||
// Avance le pointeur par pas du point depuis au point vers.
|
||||
async function bouger(depuis, vers, pas = 10) {
|
||||
for (let i = 1; i <= pas; i += 1) {
|
||||
await pointer('bouger', depuis.x + ((vers.x - depuis.x) * i) / pas, depuis.y + ((vers.y - depuis.y) * i) / pas);
|
||||
await uneImage();
|
||||
|
|
@ -282,6 +288,12 @@ async function presserEtBouger(depuis, vers, pas = 10) {
|
|||
flushSync();
|
||||
}
|
||||
|
||||
// Presse au point depuis et avance par pas jusqu'au point vers, sans relâcher.
|
||||
async function presserEtBouger(depuis, vers, pas = 10) {
|
||||
await pointer('presser', depuis.x, depuis.y);
|
||||
await bouger(depuis, vers, pas);
|
||||
}
|
||||
|
||||
async function glisserVers(depuis, vers) {
|
||||
await presserEtBouger(depuis, vers);
|
||||
await pointer('relacher', vers.x, vers.y);
|
||||
|
|
@ -438,6 +450,21 @@ function dansLeCadre(element) {
|
|||
return b.width > 0 && x >= cadre.left + 8 && x <= cadre.right - 8 && y >= cadre.top + 8 && y <= cadre.bottom - 8;
|
||||
}
|
||||
|
||||
// La marge, en px, que le libellé du fantôme garde au bord droit du plan.
|
||||
const MARGE_FANTOME_PX = 24;
|
||||
|
||||
// Fait glisser la vue de dx px, par un appui sur le sol du plan : un point
|
||||
// de la bande haute du cadre où le pointeur n'atteint ni table, ni chaise,
|
||||
// ni poignée.
|
||||
async function glisserLaVue(dx) {
|
||||
const cadre = dans('[data-plan]').getBoundingClientRect();
|
||||
const sol = { x: cadre.left + cadre.width / 2, y: cadre.top + 12 };
|
||||
const atteint = atteintAuPoint(sol);
|
||||
expect(dans('[data-plan]').contains(atteint), 'le sol, dans le plan').toBe(true);
|
||||
expect(atteint.closest('[data-table], [data-siege], [data-poignee]'), 'le sol, hors de toute table').toBeNull();
|
||||
await glisserVers(sol, { x: sol.x + dx, y: sol.y });
|
||||
}
|
||||
|
||||
// La première chaise du tour montré qui répond au prédicat et dont le
|
||||
// dessin est dans le cadre.
|
||||
function chaiseVue(predicat) {
|
||||
|
|
@ -506,6 +533,63 @@ async function refusPres(porteur, code, remede, geste) {
|
|||
return refus;
|
||||
}
|
||||
|
||||
// --- L'aperçu avant impression ----------------------------------------------------------
|
||||
|
||||
// Ce que chaque photographie de l'aperçu affirme de la première feuille :
|
||||
// le cartouche, l'échelle graphique, le pied qui porte l'horodatage et la
|
||||
// marque de provenance.
|
||||
const CONDITIONS_FEUILLE = Object.freeze([
|
||||
'[data-impression] [data-feuille] [data-cartouche]',
|
||||
'[data-impression] [data-feuille] [data-marque="echelle"]',
|
||||
'[data-impression] [data-feuille] [data-pied]',
|
||||
]);
|
||||
|
||||
const format = creerFormat(t);
|
||||
|
||||
// Les chemins que les primitives d'écriture ont reçus depuis le rang donné
|
||||
// de la trace, sans doublon, dans l'ordre du premier appel.
|
||||
const cheminsEcrits = (rang) => [...new Set(m.trace.slice(rang).map(([, , chemin]) => chemin).filter((chemin) => chemin !== ''))];
|
||||
|
||||
// Les feuilles du plan imprimé de la sortie {source, proposition, tour},
|
||||
// chacune affirmée : son cartouche, son échelle graphique et la mention
|
||||
// « disposition relative » (§ 7.7) ; son pied — le nom de l'événement, le
|
||||
// tour, l'horodatage lu sur le placement qui s'imprime, mis en forme comme
|
||||
// l'application le met, la marque de provenance et jamais la version
|
||||
// (§ 11.7, § 18.6). Rend les feuilles.
|
||||
async function feuillesDuPlan(sortie) {
|
||||
const placement = sortie.source === 'retenu' ? charge().retenu : charge().propositions.find(({ id }) => id === sortie.proposition);
|
||||
const horodatage = placement?.horodatage ?? null;
|
||||
expect(horodatage, 'l’horodatage de la sortie').not.toBeNull();
|
||||
const feuilles = await attendre('les feuilles du plan', () => {
|
||||
const lues = tous('[data-impression][data-vue-impression="plan"] [data-feuille]');
|
||||
return lues.length > 0 && lues.every((feuille) => feuille.querySelector('svg[role="img"] [data-table]')) ? lues : null;
|
||||
});
|
||||
for (const feuille of feuilles) {
|
||||
const page = `feuille ${feuille.dataset.feuille}`;
|
||||
expect(texteDe(feuille.querySelector('[data-cartouche]')).length, page).toBeGreaterThan(0);
|
||||
expect(feuille.querySelector('[data-marque="echelle"]').getBoundingClientRect().width, page).toBeGreaterThan(0);
|
||||
expect(texteDe(feuille.querySelector('[data-mention="relative"]')), page).toBe(t('plan.echelle.mention'));
|
||||
const pied = texteDe(feuille.querySelector('[data-pied]'));
|
||||
expect(pied, page).toContain(charge().evenement.nom);
|
||||
expect(pied, page).toContain(t('impression.pied.tour', { tour: sortie.tour }));
|
||||
expect(pied, page).toContain(format.horodatage(horodatage));
|
||||
expect(pied, page).toContain(MARQUE_PROVENANCE);
|
||||
expect(pied, page).not.toContain(VERSION.affichee);
|
||||
}
|
||||
return feuilles;
|
||||
}
|
||||
|
||||
// Fait défiler la fenêtre jusqu'à centrer la réunion des boîtes des
|
||||
// éléments, qui doit tenir dans sa hauteur : l'image les montre ensemble.
|
||||
async function cadrerSur(elements) {
|
||||
const boites = elements.map((element) => element.getBoundingClientRect());
|
||||
const haut = Math.min(...boites.map((b) => b.top));
|
||||
const bas = Math.max(...boites.map((b) => b.bottom));
|
||||
expect(bas - haut, 'les éléments à montrer tiennent dans la hauteur de la fenêtre').toBeLessThanOrEqual(innerHeight);
|
||||
window.scrollBy(0, Math.round((haut + bas - innerHeight) / 2));
|
||||
await images(2);
|
||||
}
|
||||
|
||||
// --- Le parcours (§ 2.1, § 19.3) ------------------------------------------------------
|
||||
|
||||
describe('le parcours par gestes, depuis un répertoire neuf', { timeout: DUREE }, () => {
|
||||
|
|
@ -959,7 +1043,9 @@ describe('le parcours par gestes, depuis un répertoire neuf', { timeout: DUREE
|
|||
await zoomerJusqua('sieges', 'zoomer');
|
||||
for (let cran = 0; cran < CRANS_AU_DESSUS; cran += 1) await cliquer('[data-commande="zoomer"]');
|
||||
await images(2);
|
||||
const vue = chaiseVue(() => true);
|
||||
// Une chaise que l'image montre entière : la condition de la capture
|
||||
// juge la boîte, pas son seul centre.
|
||||
const vue = chaiseVue((c) => chaise(c.table, c.siege) !== null && entiereDansLaFenetre(chaise(c.table, c.siege)));
|
||||
const disque = chaise(vue.table, vue.siege).querySelector('circle');
|
||||
const point = await centre(disque);
|
||||
expect(atteintAuPoint(point)?.closest('[data-siege]'), 'l’élément au centre de la chaise').toBe(chaise(vue.table, vue.siege));
|
||||
|
|
@ -1023,6 +1109,9 @@ describe('le parcours par gestes, depuis un répertoire neuf', { timeout: DUREE
|
|||
expect(colonnes).not.toContain('redondance');
|
||||
for (const th of section.querySelectorAll('th')) expect(texteDe(th)).not.toMatch(/moyenne/i);
|
||||
|
||||
// Le retenu se date à l'instant où il est retenu, après la génération :
|
||||
// le pied de l'aperçu (étape 12) distingue les deux horodatages.
|
||||
m.horloge.avancer(ECART_RETENUE_MS);
|
||||
r = revision();
|
||||
memoire.retenue = ordre[0];
|
||||
const vues = await sousObservation(async () => {
|
||||
|
|
@ -1041,17 +1130,46 @@ describe('le parcours par gestes, depuis un répertoire neuf', { timeout: DUREE
|
|||
memoire.avantAjustement = serialiserCharge(charge());
|
||||
const [t1, t2] = charge().tables.map(({ id }) => id);
|
||||
const depuis = chaiseOccupee(t1);
|
||||
// La chaise libre de t2 la plus à gauche : le libellé du fantôme, à
|
||||
// droite du pointeur, reste dans le cadre du plan.
|
||||
// La chaise libre de t2 la plus à gauche : la plus proche de t1.
|
||||
const libres = chaisesMontrees().filter((c) => c.table === t2 && c.occupant === null && c.reservation === null && c.titre === null);
|
||||
expect(libres.length).toBeGreaterThan(0);
|
||||
const gauche = (c) => chaise(t2, c.siege).getBoundingClientRect().left;
|
||||
const vers = libres.reduce((a, b) => (gauche(b) < gauche(a) ? b : a));
|
||||
const glisser = async () => presserEtBouger(await centre(chaise(t1, depuis.siege)), await centre(chaise(t2, vers.siege)));
|
||||
const libelleEtat = () => dans('[data-fantome] [data-partie="etat"]');
|
||||
|
||||
// Le libellé du fantôme se pose à droite du pointeur, et le panneau du
|
||||
// diagnostic borne le plan à droite. Un premier glissement, ramené à sa
|
||||
// chaise de départ, mesure ce que le libellé déborde du plan ; la vue
|
||||
// glisse ensuite par le sol, vers la gauche, d'autant et d'une marge,
|
||||
// pour que l'image le montre entier.
|
||||
const r = revision();
|
||||
await glisser();
|
||||
const deborde = (await attendre('le libellé mesuré', libelleEtat)).getBoundingClientRect().right - dans('[data-plan]').getBoundingClientRect().right;
|
||||
// Le libellé que le bord du plan coupe refuse la capture, avant toute
|
||||
// image : le parcours ne photographie pas un libellé illisible. Le
|
||||
// débordement mesuré est la condition de ce refus, et s'affirme : une
|
||||
// disposition où le libellé tient déjà dans le plan rend l'épreuve
|
||||
// rouge, au lieu d'en sauter le contrôle.
|
||||
expect(deborde, 'le libellé mesuré déborde du plan').toBeGreaterThan(0);
|
||||
await expect(photographier(m, '19-ajustement', { condition: '[data-fantome] [data-partie="etat"]', graine: GRAINE })).rejects.toThrow(
|
||||
'l’image la coupe',
|
||||
);
|
||||
const depart = await centre(chaise(t1, depuis.siege));
|
||||
await bouger(await centre(chaise(t2, vers.siege)), depart);
|
||||
await pointer('relacher', depart.x, depart.y);
|
||||
await images(3);
|
||||
await attendre('le glissement ramené', () => libelleEtat() === null);
|
||||
expect(revision()).toBe(r);
|
||||
expect(m.ui.etat().pose).toBeNull();
|
||||
await glisserLaVue(-(deborde + MARGE_FANTOME_PX));
|
||||
expect(dansLeCadre(chaise(t1, depuis.siege)), 'la chaise de départ, dans le cadre').toBe(true);
|
||||
|
||||
memoire.ajustement = { depuis, vers, revision: revision() };
|
||||
await presserEtBouger(await centre(chaise(t1, depuis.siege)), await centre(chaise(t2, vers.siege)));
|
||||
const etatDonnees = await attendre('le libellé de l’état des données', () => dans('[data-fantome] [data-partie="etat"]'));
|
||||
await glisser();
|
||||
const etatDonnees = await attendre('le libellé de l’état des données', libelleEtat);
|
||||
expect(texteDe(etatDonnees)).toBe(t('plan.fantome.retenu'));
|
||||
await photographier(m, '19-ajustement', { condition: ['[data-fantome] [data-partie="etat"]'], graine: GRAINE });
|
||||
await photographier(m, '19-ajustement', { condition: ['[data-fantome] [data-partie="nom"]', '[data-fantome] [data-partie="etat"]'], graine: GRAINE });
|
||||
});
|
||||
|
||||
test('19 le dépôt : une entrée qui nomme la personne, le retenu changé', async () => {
|
||||
|
|
@ -1063,6 +1181,8 @@ describe('le parcours par gestes, depuis un répertoire neuf', { timeout: DUREE
|
|||
expect(libelle).toContain(nomAffiche(charge().participants.find((p) => p.id === depuis.occupant)));
|
||||
expect(serialiserCharge(charge())).not.toBe(memoire.avantAjustement);
|
||||
expect(charge().retenu.proposition).toBe(memoire.retenue);
|
||||
// La vue revient à l'ajustement que les étapes suivantes attendent.
|
||||
await cliquer('[data-commande="ajuster"]');
|
||||
});
|
||||
|
||||
// Glisser une personne, une table, tirer une poignée, poser une réservation.
|
||||
|
|
@ -1111,6 +1231,107 @@ describe('le parcours par gestes, depuis un répertoire neuf', { timeout: DUREE
|
|||
await uneEntree(r, 'défaire');
|
||||
expect(serialiserCharge(charge())).toBe(memoire.avantAjustement);
|
||||
});
|
||||
|
||||
// Étape 12 (§ 11.7, § 19.3) : l'aperçu avant impression du plan, d'une
|
||||
// proposition que le retenu ne porte pas, puis du retenu. La paire montre
|
||||
// la règle du filigrane — présent, puis absent — sur la première page, au
|
||||
// même cadrage, de deux placements distincts ; le pied porte l'horodatage
|
||||
// de ce qui s'imprime et la marque de provenance à la place de la version.
|
||||
test('22 l’aperçu d’une proposition non retenue : le papier demandé une fois, BROUILLON sur chaque feuille, le pied de la proposition', async () => {
|
||||
expect(charge().evenement.etat).toBe('retenu');
|
||||
const proposition = charge().propositions.find(({ id }) => id !== charge().retenu.proposition).id;
|
||||
const avant = fichiers();
|
||||
const rang = m.trace.length;
|
||||
await ouvrirSection('propositions');
|
||||
await cliquer(`[data-proposition="${proposition}"] [data-commande="voir"]`);
|
||||
await attendre('la proposition montrée au plan', () => m.ui.etat().proposition === proposition && dans('[data-section="plan"]'));
|
||||
await cliquer('[data-ecran="evenement"] [data-commande="apercu"]');
|
||||
// Le premier aperçu de la machine pose la question du papier, avant tout
|
||||
// document (§ 8.5).
|
||||
await attendre('la question du papier', () => dans('[data-ecran="apercu"] [data-papier="question"]'));
|
||||
expect(dans('[data-impression]')).toBeNull();
|
||||
await cliquer('[data-papier="question"] [data-cible="A4"] [data-commande="choisirPapier"]');
|
||||
await attendre('le document imprimé', () => dans('[data-impression] [data-feuille]') && dans('[data-papier="choisi"]'));
|
||||
expect(etat().papier).toBe('A4');
|
||||
expect(m.ui.etat().apercu).toMatchObject({ vue: 'plan', tour: 1 });
|
||||
|
||||
const sortie = { source: 'proposition', proposition, tour: 1 };
|
||||
expect(estBrouillon(charge(), sortie)).toBe(true);
|
||||
memoire.horodatageProposition = charge().propositions.find(({ id }) => id === proposition).horodatage;
|
||||
const feuilles = await feuillesDuPlan(sortie);
|
||||
for (const feuille of feuilles) {
|
||||
const filigranes = feuille.querySelectorAll('[data-filigrane]');
|
||||
expect(filigranes, `feuille ${feuille.dataset.feuille}`).toHaveLength(1);
|
||||
expect(texteDe(filigranes[0])).toBe(t('impression.filigrane'));
|
||||
}
|
||||
// Seul le papier de la machine s'écrit ; rien de l'événement.
|
||||
expect(cheminsEcrits(rang)).toEqual(['reglages_locaux.json']);
|
||||
await rienEcrit(avant, 'l’aperçu d’une proposition');
|
||||
|
||||
// Le cadrage : le filigrane et le pied de la première feuille, ensemble
|
||||
// dans la fenêtre.
|
||||
const filigrane = dans('[data-impression] [data-feuille] [data-filigrane] [data-marque="filigrane"]');
|
||||
await cadrerSur([filigrane, dans('[data-impression] [data-feuille] [data-pied]')]);
|
||||
memoire.cadrage = window.scrollY;
|
||||
await photographier(m, '22-plan-brouillon', {
|
||||
condition: [
|
||||
'[data-impression] [data-feuille] [data-filigrane] [data-marque="filigrane"]',
|
||||
...CONDITIONS_FEUILLE,
|
||||
],
|
||||
graine: GRAINE,
|
||||
});
|
||||
});
|
||||
|
||||
test('23 l’aperçu du retenu : le papier gardé, aucun filigrane, le pied du retenu ; rien d’écrit', async () => {
|
||||
await cliquer('[data-ecran="apercu"] [data-commande="fermerApercu"]');
|
||||
await attendre('le plan, de retour', () => dans('[data-ecran="evenement"] [data-section="plan"]'));
|
||||
await cliquer('[data-commande="quitterProposition"]');
|
||||
await attendre('le retenu montré', () => m.ui.etat().proposition === null);
|
||||
const avant = fichiers();
|
||||
const rang = m.trace.length;
|
||||
await cliquer('[data-ecran="evenement"] [data-commande="apercu"]');
|
||||
await attendre('le document imprimé', () => dans('[data-impression] [data-feuille]'));
|
||||
// Le papier choisi au premier aperçu n'est pas redemandé.
|
||||
expect(dans('[data-papier="question"]')).toBeNull();
|
||||
expect(dans('[data-papier="choisi"]')).not.toBeNull();
|
||||
|
||||
const sortie = { source: 'retenu', proposition: null, tour: 1 };
|
||||
expect(estBrouillon(charge(), sortie)).toBe(false);
|
||||
const feuilles = await feuillesDuPlan(sortie);
|
||||
// Le pied date le retenu, pas la proposition de 22.
|
||||
expect(charge().retenu.horodatage).not.toBe(memoire.horodatageProposition);
|
||||
for (const feuille of feuilles) expect(texteDe(feuille.querySelector('[data-pied]'))).not.toContain(format.horodatage(memoire.horodatageProposition));
|
||||
expect(tous('[data-impression] [data-filigrane]')).toHaveLength(0);
|
||||
expect(cheminsEcrits(rang)).toEqual([]);
|
||||
await rienEcrit(avant, 'l’aperçu du retenu');
|
||||
|
||||
// Le cadrage de 22 : la même position de défilement, le placement
|
||||
// retenu à la place de la proposition.
|
||||
window.scrollTo(0, memoire.cadrage);
|
||||
await images(2);
|
||||
expect(window.scrollY).toBe(memoire.cadrage);
|
||||
await photographier(m, '23-plan-retenu', { condition: CONDITIONS_FEUILLE, graine: GRAINE });
|
||||
|
||||
await cliquer('[data-ecran="apercu"] [data-commande="fermerApercu"]');
|
||||
await attendre('le plan, de retour', () => dans('[data-ecran="evenement"] [data-section="plan"]'));
|
||||
window.scrollTo(0, 0);
|
||||
await images(2);
|
||||
});
|
||||
|
||||
// Un échec de 22 ou de 23 laisse l'aperçu ouvert, la proposition montrée
|
||||
// et la fenêtre défilée : l'état est rendu ici, pour que les épreuves
|
||||
// suivantes jugent leurs propres gardes et non ce reste.
|
||||
afterAll(async () => {
|
||||
if (dans('[data-ecran="apercu"] [data-commande="fermerApercu"]') !== null) {
|
||||
await cliquer('[data-ecran="apercu"] [data-commande="fermerApercu"]');
|
||||
await attendre('le plan, de retour', () => dans('[data-ecran="evenement"]'));
|
||||
}
|
||||
if (m.ui.etat().proposition !== null && dans('[data-commande="quitterProposition"]') !== null) {
|
||||
await cliquer('[data-commande="quitterProposition"]');
|
||||
await attendre('le retenu montré', () => m.ui.etat().proposition === null);
|
||||
}
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
});
|
||||
|
||||
// --- L'ordre de lecture (§ 2.1) ---------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1232,6 +1232,60 @@ describe('les captures d’écran : chaque contrôle échoue en nommant la captu
|
|||
});
|
||||
});
|
||||
|
||||
// Une condition se juge sur la boîte entière de l'élément (§ 19.11) :
|
||||
// un élément dont le centre se voit, mais que le bord de la fenêtre ou un
|
||||
// ancêtre qui rogne coupe, refuse la capture ; le même, entier, passe. Le
|
||||
// bord de la fenêtre coupe un élément HTML avant même la boîte entière :
|
||||
// visible de rendu.js le refuse déjà, et nomme la fenêtre.
|
||||
test('condition : un élément HTML ou SVG dont le centre se voit, coupé par la fenêtre ou par un ancêtre qui rogne, refuse la capture ; entier, il passe', async () => {
|
||||
await avecMontage(async (m, { ecrire }) => {
|
||||
const fixe = 'position: fixed; top: 300px';
|
||||
const coupe = 'sa boîte déborde de la fenêtre ou d’un ancêtre qui la rogne : l’image la coupe';
|
||||
const cas = [
|
||||
[
|
||||
'HTML rogné par un ancêtre overflow: hidden',
|
||||
`<div style="${fixe}; left: 100px; width: 200px; height: 40px; overflow: hidden"><div data-sonde style="width: 300px; height: 20px"></div></div>`,
|
||||
`<div style="${fixe}; left: 100px; width: 200px; height: 40px; overflow: hidden"><div data-sonde style="width: 160px; height: 20px"></div></div>`,
|
||||
coupe,
|
||||
],
|
||||
[
|
||||
'HTML coupé par le bord de la fenêtre',
|
||||
`<div data-sonde style="${fixe}; left: -40px; width: 200px; height: 20px"></div>`,
|
||||
`<div data-sonde style="${fixe}; left: 40px; width: 200px; height: 20px"></div>`,
|
||||
'hors de la fenêtre',
|
||||
],
|
||||
[
|
||||
'SVG rogné par son <svg>',
|
||||
`<svg style="${fixe}; left: 100px" width="200" height="40"><rect data-sonde x="100" y="10" width="160" height="20" fill="none" /></svg>`,
|
||||
`<svg style="${fixe}; left: 100px" width="200" height="40"><rect data-sonde x="20" y="10" width="160" height="20" fill="none" /></svg>`,
|
||||
coupe,
|
||||
],
|
||||
[
|
||||
'SVG coupé par le bord de la fenêtre',
|
||||
`<svg style="${fixe}; left: -60px" width="200" height="40"><rect data-sonde x="0" y="10" width="160" height="20" fill="none" /></svg>`,
|
||||
`<svg style="${fixe}; left: 60px" width="200" height="40"><rect data-sonde x="0" y="10" width="160" height="20" fill="none" /></svg>`,
|
||||
coupe,
|
||||
],
|
||||
];
|
||||
for (const [nom, rogne, entier, nature] of cas) {
|
||||
let element = poser(m, rogne);
|
||||
try {
|
||||
await expect(photographier(m, '01-premier-ecran', { ...ecrire, condition: '[data-sonde]' }), nom).rejects.toThrow(
|
||||
`capture 01-premier-ecran : « [data-sonde] » invisible : ${nature}`,
|
||||
);
|
||||
} finally {
|
||||
element.remove();
|
||||
}
|
||||
element = poser(m, entier);
|
||||
try {
|
||||
expect((await photographier(m, '01-premier-ecran', { ...ecrire, condition: '[data-sonde]' })).fichier, nom).toBe('01-premier-ecran');
|
||||
} finally {
|
||||
element.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('publication : zéro capture échoue ; une étape en échec ne publie rien — attente jetée, manifeste et images inchangés', async () => {
|
||||
await avecMontage(async (m, { racine, racineNeutre, ecrire }) => {
|
||||
const declarees = ['01-premier-ecran', '01b-passer-en-ecriture'];
|
||||
|
|
|
|||
|
|
@ -85,10 +85,10 @@ function coupeLaFenetre(element) {
|
|||
return b.right > 0 && b.bottom > 0 && b.left < innerWidth && b.top < innerHeight;
|
||||
}
|
||||
|
||||
// Vrai quand la boîte de l'élément tient entière dans la fenêtre et dans
|
||||
// chaque ancêtre qui rogne ce qui le déborde — overflow autre que visible,
|
||||
// ou un <svg> —, à un demi-pixel près.
|
||||
function entiereDansLaFenetre(element) {
|
||||
/** Vrai quand la boîte de l'élément tient entière dans la fenêtre et dans
|
||||
* chaque ancêtre qui rogne ce qui le déborde — overflow autre que visible,
|
||||
* ou un <svg> —, à un demi-pixel près : l'image le montre entier. */
|
||||
export function entiereDansLaFenetre(element) {
|
||||
const b = element.getBoundingClientRect();
|
||||
const dedans = (r) => b.left >= r.left - 0.5 && b.top >= r.top - 0.5 && b.right <= r.right + 0.5 && b.bottom <= r.bottom + 0.5;
|
||||
if (!dedans({ left: 0, top: 0, right: innerWidth, bottom: innerHeight })) return false;
|
||||
|
|
@ -632,12 +632,49 @@ export async function empreinteEtat(m) {
|
|||
// de rendu.js, son centre atteint et non couvert. Un élément SVG se lit par
|
||||
// la géométrie du dessin, comme ses textes au relevé des contrastes : rendu,
|
||||
// et le centre de sa boîte dans le <svg> extérieur et dans la fenêtre — un
|
||||
// dessin sous le pointeur ou hors d'atteinte reste visible. Rend true, ou la
|
||||
// raison.
|
||||
// dessin sous le pointeur ou hors d'atteinte reste visible. Dans les deux
|
||||
// cas, la boîte entière tient dans la fenêtre et dans chaque ancêtre qui
|
||||
// rogne : un libellé dont le centre se voit mais que le bord du plan coupe
|
||||
// n'est pas lisible sur l'image. Rend true, ou la raison.
|
||||
function conditionVisible(element) {
|
||||
if (!(element instanceof SVGElement)) return visible(element);
|
||||
if (!rendu(element)) return 'checkVisibility faux';
|
||||
return centreDansLeDessin(element) ? true : 'le centre de sa boîte est hors du dessin ou de la fenêtre';
|
||||
if (element instanceof SVGElement) {
|
||||
if (!rendu(element)) return 'checkVisibility faux';
|
||||
if (!centreDansLeDessin(element)) return 'le centre de sa boîte est hors du dessin ou de la fenêtre';
|
||||
} else {
|
||||
const vu = visible(element);
|
||||
if (vu !== true) return vu;
|
||||
}
|
||||
return entiereDansLaFenetre(element) ? true : 'sa boîte déborde de la fenêtre ou d’un ancêtre qui la rogne : l’image la coupe';
|
||||
}
|
||||
|
||||
// L'identifiant du cadre que pose cliche.
|
||||
const CADRE_DE_CAPTURE = 'gtt-cadre-de-capture';
|
||||
|
||||
// L'image de l'écran du montage m, telle que la fenêtre la montre ; rend
|
||||
// { chemin }. Un montage dont la boîte est la fenêtre se photographie
|
||||
// lui-même. Un montage plus haut — l'aperçu avant impression, dont le
|
||||
// document n'est jamais tronqué — se photographie par un cadre fixe de la
|
||||
// taille de la fenêtre, posé le temps de la capture, sans peinture, sans
|
||||
// atteinte du pointeur, caché aux technologies d'assistance : l'image est
|
||||
// la fenêtre, à la position où le document a défilé. Le cadre ne sert que
|
||||
// là : la capture d'un autre élément que le montage peint autrement
|
||||
// l'élément qui a le focus — le contour de focus du plan, le curseur d'un
|
||||
// champ —, et l'image d'un écran qui n'a pas changé changerait.
|
||||
async function cliche(m, fichier, racine) {
|
||||
const b = m.racine.getBoundingClientRect();
|
||||
if (b.left === 0 && b.top === 0 && b.width === innerWidth && b.height === innerHeight) {
|
||||
return commands.prendreCliche({ fichier, selecteur: `[data-montage="${m.racine.dataset.montage}"]`, racine });
|
||||
}
|
||||
const cadre = document.createElement('div');
|
||||
cadre.id = CADRE_DE_CAPTURE;
|
||||
cadre.setAttribute('aria-hidden', 'true');
|
||||
Object.assign(cadre.style, { position: 'fixed', inset: '0', pointerEvents: 'none' });
|
||||
document.body.append(cadre);
|
||||
try {
|
||||
return await commands.prendreCliche({ fichier, selecteur: `#${CADRE_DE_CAPTURE}`, racine });
|
||||
} finally {
|
||||
cadre.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -650,8 +687,8 @@ function conditionVisible(element) {
|
|||
* dans le texte relevé ; aucune faute de libellé ; styles relevés dans les
|
||||
* deux thèmes, sous leurs seuils ; empreinte d'état. L'entrée se compose,
|
||||
* puis 'verifier' la compare au manifeste — un écart nomme l'étape et sa
|
||||
* nature —, et 'ecrire' dépose l'image, prise avant les relevés, et
|
||||
* l'entrée.
|
||||
* nature —, et 'ecrire' dépose l'image de la fenêtre, prise avant les
|
||||
* relevés, et l'entrée.
|
||||
*
|
||||
* @param {Object} m un montage de monterApp
|
||||
* @param {string} fichier le nom de la capture, sans extension
|
||||
|
|
@ -692,8 +729,7 @@ export async function photographier(m, fichier, { condition = [], graine = null,
|
|||
throw echec(`chaîne témoin à ${lue} px, ${attendue} px attendus à ${tolerance} près : la police rendue n’est pas l’embarquée`);
|
||||
}
|
||||
|
||||
const selecteur = `[data-montage="${m.racine.dataset.montage}"]`;
|
||||
const image = mode === 'ecrire' ? await commands.prendreCliche({ fichier, selecteur, racine }) : null;
|
||||
const image = mode === 'ecrire' ? await cliche(m, fichier, racine) : null;
|
||||
|
||||
const texte = texteReleve(m.racine);
|
||||
const racineNeutre = m.seance.etat().racine?.chemin ?? null;
|
||||
|
|
|
|||
Loading…
Reference in a new issue