583 lines
22 KiB
JavaScript
583 lines
22 KiB
JavaScript
|
|
// © 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;
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
]);
|