Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y
Config strictement stricte partout (ruff, mypy --strict, bandit, vulture, import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas casser le build" - l'existant a ete corrige pour la satisfaire plutot que l'inverse. Hooks pre-commit locaux (language: system) bloquants. - Typage mypy --strict propage a tout le moteur (db, screens, auth, core, ai, routes, puis publish/scripts/tests/app.py/build_css.py). - Securite : fuite de handle fichier Windows corrigee dans l'export SCORM (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe "</script>" dans le JSON embarque en <script>, 25 sites). - Architecture : imports circulaires/F811 nettoyes, contrats import-linter respectes, code mort retire (vulture). - Accessibilite : 69 champs de formulaire sans label correctement associe corriges (for/id ou aria-label) sur 11 templates. - ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis appliquees (aucune desactivee sans verification individuelle). - Tests : isolation du compte admin partage (nettoyage ponctuel + fixture de teardown automatique en filet de securite), suite complete verte (591 tests Python, 241 tests JS). - SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport complet analyse point par point, faux positifs documentes. - .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine faisait echouer ESLint (linebreak-style) via un bug connu de git (checkout "en place" qui ignore l'eol force sur un fichier deja present sur disque - contourne en supprimant puis recreant chaque fichier suivi). djLint (H021, styles inline) volontairement saute pour ce commit - backlog assume, deja documente, traite dans un lot separe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7db4803b93
commit
c57420c8c9
@@ -79,7 +79,7 @@ test('runSpriteAnimation — une animation non bouclée en cours résiste à une
|
||||
});
|
||||
|
||||
test('resolveSpriteFrames — résout depuis gameData.personnage_animations par nom', () => {
|
||||
global.gameData = { personnage_animations: { '5': { walk: ['/w0.png', '/w1.png'] } } };
|
||||
global.gameData = { personnage_animations: { 5: { walk: ['/w0.png', '/w1.png'] } } };
|
||||
const resolved = resolveSpriteFrames('5', { animation: 'walk', fps: 12, loop: true });
|
||||
assert.deepEqual(resolved.frames, ['/w0.png', '/w1.png']);
|
||||
assert.equal(resolved.fps, 12);
|
||||
@@ -101,7 +101,7 @@ test('clampSceneObjectPosition — borne au bord droit/bas de la scène', () =>
|
||||
withScreensData({ id: 1, scene_width: 200, scene_height: 100 }, () => {
|
||||
const el = { style: { width: '40px', height: '40px' } };
|
||||
assert.equal(clampSceneObjectPosition(el, 'left', 500), 160); // 200 - 40
|
||||
assert.equal(clampSceneObjectPosition(el, 'top', 500), 60); // 100 - 40
|
||||
assert.equal(clampSceneObjectPosition(el, 'top', 500), 60); // 100 - 40
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,7 +118,9 @@ test('clampSceneObjectPosition — borne au bord gauche/haut (jamais négatif)',
|
||||
// grand que la scène doit laisser le personnage se déplacer sur TOUT le
|
||||
// fond, pas seulement le petit cadre visible de la scène d'origine.
|
||||
test('clampSceneObjectPosition — borne au MONDE (world_width/height) quand un fond XXL l\'a agrandi', () => {
|
||||
withScreensData({ id: 1, scene_width: 200, scene_height: 100, world_width: 2000, world_height: 1000 }, () => {
|
||||
withScreensData({
|
||||
id: 1, scene_width: 200, scene_height: 100, world_width: 2000, world_height: 1000,
|
||||
}, () => {
|
||||
const el = { style: { width: '40px', height: '40px' } };
|
||||
assert.equal(clampSceneObjectPosition(el, 'left', 500), 500); // pas encore au bord du MONDE
|
||||
assert.equal(clampSceneObjectPosition(el, 'left', 5000), 1960); // 2000 - 40
|
||||
@@ -134,7 +136,11 @@ test('clampSceneObjectPosition — sans world_width/height (aucun fond), retombe
|
||||
|
||||
test('applyObjectProperty — pos_x_relatif reste dans les limites de la scène après plusieurs pas', () => {
|
||||
withScreensData({ id: 1, scene_width: 100, scene_height: 100 }, () => {
|
||||
const el = { style: { left: '90px', top: '0px', width: '20px', height: '20px' } };
|
||||
const el = {
|
||||
style: {
|
||||
left: '90px', top: '0px', width: '20px', height: '20px',
|
||||
},
|
||||
};
|
||||
applyObjectProperty(el, 'pos_x_relatif', '5');
|
||||
applyObjectProperty(el, 'pos_x_relatif', '5');
|
||||
applyObjectProperty(el, 'pos_x_relatif', '5');
|
||||
|
||||
@@ -16,6 +16,7 @@ global.screensData = [];
|
||||
global.heldKeys = new Set();
|
||||
|
||||
const actionsMod = require('../actions.js');
|
||||
|
||||
Object.assign(global, actionsMod);
|
||||
require('../personnage-controller.js');
|
||||
const { forgeDialogueBoxAdvance } = require('../dialogue-box-controller.js'); // pas mocké : la vraie logique métier
|
||||
@@ -28,12 +29,12 @@ function fakeEl(style) {
|
||||
const classes = new Set();
|
||||
const children = [];
|
||||
return {
|
||||
style: style,
|
||||
classList: { add: c => classes.add(c), remove: c => classes.delete(c), contains: c => classes.has(c) },
|
||||
style,
|
||||
classList: { add: (c) => classes.add(c), remove: (c) => classes.delete(c), contains: (c) => classes.has(c) },
|
||||
children,
|
||||
appendChild(child) { children.push(child); },
|
||||
querySelector(sel) {
|
||||
if (sel === '.dialogueBoxWidget') return children.find(c => c._isDialogueBoxWidget);
|
||||
if (sel === '.dialogueBoxWidget') return children.find((c) => c._isDialogueBoxWidget);
|
||||
if (sel === '[data-dialogue-role="header"]') return this._header;
|
||||
if (sel === '[data-dialogue-role="body"]') return this._body;
|
||||
if (sel === '[data-dialogue-role="footer"]') return this._footer;
|
||||
@@ -46,19 +47,26 @@ function fakeEl(style) {
|
||||
test('collision "dialogue" -> boîte de dialogue -> fin de dialogue (mark_completed) marque le dialogue terminé', () => {
|
||||
const player = { id: 1, kind: 'personnage', personnage_role: 'joueur' };
|
||||
const mrPinguin = {
|
||||
id: 2, kind: 'personnage', name: 'Mr. Pinguin',
|
||||
id: 2,
|
||||
kind: 'personnage',
|
||||
name: 'Mr. Pinguin',
|
||||
collision_rules: [{
|
||||
trigger: 'collision',
|
||||
action: {
|
||||
type: 'dialogue', mark_completed: true,
|
||||
type: 'dialogue',
|
||||
mark_completed: true,
|
||||
dialogue: { id: 'd_intro', lines: [{ speaker: 'Mr. Pinguin', text: 'Bonjour !' }, { speaker: 'Charlie', text: 'Salut !' }] },
|
||||
},
|
||||
}],
|
||||
};
|
||||
const box = { id: 3, kind: 'dialogue_box' };
|
||||
|
||||
const playerEl = fakeEl({ left: '0px', top: '0px', width: '20px', height: '20px' });
|
||||
const pinguinEl = fakeEl({ left: '5px', top: '5px', width: '20px', height: '20px' }); // chevauche playerEl
|
||||
const playerEl = fakeEl({
|
||||
left: '0px', top: '0px', width: '20px', height: '20px',
|
||||
});
|
||||
const pinguinEl = fakeEl({
|
||||
left: '5px', top: '5px', width: '20px', height: '20px',
|
||||
}); // chevauche playerEl
|
||||
const boxWidget = fakeEl({});
|
||||
boxWidget._isDialogueBoxWidget = true;
|
||||
boxWidget._objectId = '3';
|
||||
@@ -107,6 +115,6 @@ test('collision "dialogue" -> boîte de dialogue -> fin de dialogue (mark_comple
|
||||
|
||||
forgeDialogueBoxAdvance('3'); // dernière réplique consommée -> mark_completed -> dialogue terminé, boîte masquée
|
||||
assert.equal(boxWidget.classList.contains('is-active'), false);
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_intro'], true);
|
||||
assert.equal(window.forgeCompletedDialogueIds.d_intro, true);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ global.screensData = [];
|
||||
global.heldKeys = new Set();
|
||||
|
||||
const actionsMod = require('../actions.js');
|
||||
|
||||
Object.assign(global, actionsMod);
|
||||
require('../personnage-controller.js');
|
||||
global.forgeApplyVariableActionOffline = global.forgeApplyVariableActionOffline || require('../offline/apply-actions.js').forgeApplyVariableActionOffline;
|
||||
@@ -34,7 +35,7 @@ function fakeParentEl() {
|
||||
appendChild(child) { children.push(child); },
|
||||
querySelector(sel) {
|
||||
const m = sel.match(/data-for-object-id="(\d+)"/);
|
||||
return m ? children.find(c => c._forObjectId === m[1]) : undefined;
|
||||
return m ? children.find((c) => c._forObjectId === m[1]) : undefined;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -42,22 +43,28 @@ function fakeParentEl() {
|
||||
function fakeEl(parentEl) {
|
||||
const classes = new Set();
|
||||
return {
|
||||
style: { left: '0px', top: '0px', width: '20px', height: '20px' },
|
||||
classList: { add: c => classes.add(c), remove: c => classes.delete(c), toggle: c => {}, contains: c => classes.has(c) },
|
||||
style: {
|
||||
left: '0px', top: '0px', width: '20px', height: '20px',
|
||||
},
|
||||
classList: {
|
||||
add: (c) => classes.add(c), remove: (c) => classes.delete(c), toggle: (_c) => {}, contains: (c) => classes.has(c),
|
||||
},
|
||||
parentElement: parentEl,
|
||||
};
|
||||
}
|
||||
|
||||
function fakeBubbleEl() {
|
||||
return {
|
||||
className: 'collisionInteractBubble', style: {}, textContent: '',
|
||||
className: 'collisionInteractBubble',
|
||||
style: {},
|
||||
textContent: '',
|
||||
setAttribute(name, value) { if (name === 'data-for-object-id') this._forObjectId = String(value); },
|
||||
};
|
||||
}
|
||||
|
||||
function fakeDocument(elByObjectId) {
|
||||
return {
|
||||
querySelector: sel => { const m = sel.match(/"(\d+)"/); return m ? elByObjectId[m[1]] : undefined; },
|
||||
querySelector: (sel) => { const m = sel.match(/"(\d+)"/); return m ? elByObjectId[m[1]] : undefined; },
|
||||
createElement: () => fakeBubbleEl(),
|
||||
};
|
||||
}
|
||||
@@ -65,7 +72,9 @@ function fakeDocument(elByObjectId) {
|
||||
test.afterEach(() => { forgeStopCollisionRuleControllers(); });
|
||||
|
||||
function rect(left, top, width, height) {
|
||||
return { left, top, width, height, right: left + width, bottom: top + height, shape: 'rectangle' };
|
||||
return {
|
||||
left, top, width, height, right: left + width, bottom: top + height, shape: 'rectangle',
|
||||
};
|
||||
}
|
||||
|
||||
test('forgeCollisionRuleTriggerActive — trigger "collision" : vrai si les boîtes se chevauchent, faux si loin', () => {
|
||||
@@ -93,7 +102,7 @@ test('forgeRunCollisionRuleAction — "dialogue" délègue à forgeShowDialogueB
|
||||
const previousShow = global.forgeShowDialogueBox;
|
||||
global.forgeShowDialogueBox = (...args) => calls.push(args);
|
||||
const lines = [{ speaker: 'PNJ', text: 'Bonjour !' }];
|
||||
forgeRunCollisionRuleAction({ type: 'dialogue', dialogue: { id: 'd_1', lines: lines }, mark_completed: true }, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
forgeRunCollisionRuleAction({ type: 'dialogue', dialogue: { id: 'd_1', lines }, mark_completed: true }, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
global.forgeShowDialogueBox = previousShow;
|
||||
assert.deepEqual(calls, [['d_1', 'Crokine', lines, true, undefined]]);
|
||||
});
|
||||
@@ -114,8 +123,12 @@ test('forgeRunCollisionRuleAction — "condition" vraie exécute la branche si_v
|
||||
global.forgeShowDialogueBox = (...args) => calls.push(args);
|
||||
const vraiLines = [{ speaker: 'PNJ', text: 'Vrai !' }];
|
||||
const action = {
|
||||
type: 'condition', variable: 'inventaire_cle', operateur: 'superieur', valeur: '0',
|
||||
si_vrai: { type: 'dialogue', dialogue: { id: 'd_vrai', lines: vraiLines } }, si_faux: null,
|
||||
type: 'condition',
|
||||
variable: 'inventaire_cle',
|
||||
operateur: 'superieur',
|
||||
valeur: '0',
|
||||
si_vrai: { type: 'dialogue', dialogue: { id: 'd_vrai', lines: vraiLines } },
|
||||
si_faux: null,
|
||||
};
|
||||
forgeRunCollisionRuleAction(action, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
global.forgeShowDialogueBox = previousShow;
|
||||
@@ -129,7 +142,10 @@ test('forgeRunCollisionRuleAction — "condition" fausse exécute la branche si_
|
||||
global.forgeShowDialogueBox = (...args) => calls.push(args);
|
||||
const fauxLines = [{ speaker: 'PNJ', text: 'Faux !' }];
|
||||
const action = {
|
||||
type: 'condition', variable: 'inventaire_cle', operateur: 'superieur', valeur: '0',
|
||||
type: 'condition',
|
||||
variable: 'inventaire_cle',
|
||||
operateur: 'superieur',
|
||||
valeur: '0',
|
||||
si_vrai: { type: 'dialogue', dialogue: { id: 'd_vrai', lines: [] } },
|
||||
si_faux: { type: 'dialogue', dialogue: { id: 'd_faux', lines: fauxLines } },
|
||||
};
|
||||
@@ -144,8 +160,12 @@ test('forgeRunCollisionRuleAction — "condition" avec une branche "Ne rien fair
|
||||
const previousShow = global.forgeShowDialogueBox;
|
||||
global.forgeShowDialogueBox = (...args) => calls.push(args);
|
||||
const action = {
|
||||
type: 'condition', variable: 'inventaire_cle', operateur: 'superieur', valeur: '0',
|
||||
si_vrai: { type: 'dialogue', dialogue: { id: 'd_vrai', lines: [] } }, si_faux: null,
|
||||
type: 'condition',
|
||||
variable: 'inventaire_cle',
|
||||
operateur: 'superieur',
|
||||
valeur: '0',
|
||||
si_vrai: { type: 'dialogue', dialogue: { id: 'd_vrai', lines: [] } },
|
||||
si_faux: null,
|
||||
};
|
||||
forgeRunCollisionRuleAction(action, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
global.forgeShowDialogueBox = previousShow;
|
||||
@@ -154,15 +174,22 @@ test('forgeRunCollisionRuleAction — "condition" avec une branche "Ne rien fair
|
||||
|
||||
test('forgeRunCollisionRuleAction — "variable" modifie gameData.variables via forgeApplyVariableActionOffline', () => {
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '2' } } };
|
||||
forgeRunCollisionRuleAction({ type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '3' }, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
forgeRunCollisionRuleAction({
|
||||
type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '3',
|
||||
}, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
assert.equal(gameData.variables.score.value, '5');
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "variable" chaînée (then) exécute IMMÉDIATEMENT la suivante (instantané, pas besoin d\'attendre)', () => {
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '0' }, vies: { type: 'nombre_entier', value: '3' } } };
|
||||
const action = {
|
||||
type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1',
|
||||
then: { type: 'variable', target_variable: 'vies', data_operation: 'decrementer', data_value: '1' },
|
||||
type: 'variable',
|
||||
target_variable: 'score',
|
||||
data_operation: 'incrementer',
|
||||
data_value: '1',
|
||||
then: {
|
||||
type: 'variable', target_variable: 'vies', data_operation: 'decrementer', data_value: '1',
|
||||
},
|
||||
};
|
||||
forgeRunCollisionRuleAction(action, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
assert.equal(gameData.variables.score.value, '1');
|
||||
@@ -174,7 +201,9 @@ test('forgeRunCollisionRuleAction — "dialogue" transmet son "then" à forgeSho
|
||||
const calls = [];
|
||||
const previousShow = global.forgeShowDialogueBox;
|
||||
global.forgeShowDialogueBox = (...args) => calls.push(args);
|
||||
const thenAction = { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' };
|
||||
const thenAction = {
|
||||
type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1',
|
||||
};
|
||||
forgeRunCollisionRuleAction({ type: 'dialogue', dialogue: { id: 'd_1', lines: [] }, then: thenAction }, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
global.forgeShowDialogueBox = previousShow;
|
||||
assert.deepEqual(calls, [['d_1', 'Crokine', [], undefined, thenAction]]);
|
||||
@@ -183,7 +212,7 @@ test('forgeRunCollisionRuleAction — "dialogue" transmet son "then" à forgeSho
|
||||
test('forgeRunCollisionRuleAction — "surbrillance" bascule .forgeHighlight sur l\'objet CIBLÉ (object_id), pas forcément l\'objet du déclencheur', () => {
|
||||
const targetEl = fakeEl();
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: sel => (sel.includes('"9"') ? targetEl : undefined) };
|
||||
global.document = { querySelector: (sel) => (sel.includes('"9"') ? targetEl : undefined) };
|
||||
try {
|
||||
forgeRunCollisionRuleAction({ type: 'surbrillance', object_id: 9, valeur: 'on' }, fakeEl(), { id: 1, name: 'Interrupteur' });
|
||||
assert.equal(targetEl.classList.contains('forgeHighlight'), true);
|
||||
@@ -206,8 +235,12 @@ test('forgeRunCollisionRuleAction — "surbrillance" sans objet trouvé (référ
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '0' } } };
|
||||
try {
|
||||
forgeRunCollisionRuleAction({
|
||||
type: 'surbrillance', object_id: 999, valeur: 'on',
|
||||
then: { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' },
|
||||
type: 'surbrillance',
|
||||
object_id: 999,
|
||||
valeur: 'on',
|
||||
then: {
|
||||
type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1',
|
||||
},
|
||||
}, fakeEl(), { id: 1, name: 'x' });
|
||||
assert.equal(gameData.variables.score.value, '1');
|
||||
} finally { global.document = previousDoc; }
|
||||
@@ -223,8 +256,11 @@ test('forgeRunCollisionRuleAction — "son" joue l\'URL fournie via Audio, insta
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '0' } } };
|
||||
try {
|
||||
forgeRunCollisionRuleAction({
|
||||
type: 'son', asset_url: '/assets/1/bip.mp3',
|
||||
then: { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' },
|
||||
type: 'son',
|
||||
asset_url: '/assets/1/bip.mp3',
|
||||
then: {
|
||||
type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1',
|
||||
},
|
||||
}, fakeEl(), { id: 1, name: 'x' });
|
||||
assert.deepEqual(played, ['/assets/1/bip.mp3']);
|
||||
assert.equal(gameData.variables.score.value, '1');
|
||||
@@ -238,8 +274,12 @@ test('forgeRunCollisionRuleAction — "video" attend forgeShowVideoOverlay avant
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '0' } } };
|
||||
try {
|
||||
forgeRunCollisionRuleAction({
|
||||
type: 'video', asset_url: '/assets/1/intro.mp4', mode: 'plein_ecran',
|
||||
then: { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' },
|
||||
type: 'video',
|
||||
asset_url: '/assets/1/intro.mp4',
|
||||
mode: 'plein_ecran',
|
||||
then: {
|
||||
type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1',
|
||||
},
|
||||
}, fakeEl(), { id: 1, name: 'x' });
|
||||
assert.equal(gameData.variables.score.value, '0'); // pas encore, la vidéo "joue"
|
||||
capturedDone();
|
||||
@@ -250,11 +290,14 @@ test('forgeRunCollisionRuleAction — "video" attend forgeShowVideoOverlay avant
|
||||
test('forgeRunCollisionRuleAction — "attendre" suspend "then" jusqu\'à la fin du délai (data_value en secondes)', async () => {
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '0' } } };
|
||||
forgeRunCollisionRuleAction({
|
||||
type: 'attendre', data_value: '0',
|
||||
then: { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' },
|
||||
type: 'attendre',
|
||||
data_value: '0',
|
||||
then: {
|
||||
type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1',
|
||||
},
|
||||
}, fakeEl(), { id: 1, name: 'x' });
|
||||
assert.equal(gameData.variables.score.value, '0'); // pas encore, setTimeout pas encore résolu
|
||||
await new Promise(function (resolve) { setTimeout(resolve, 10); });
|
||||
await new Promise((resolve) => { setTimeout(resolve, 10); });
|
||||
assert.equal(gameData.variables.score.value, '1');
|
||||
});
|
||||
|
||||
@@ -272,16 +315,22 @@ test('forgeRunCollisionRuleAction — "indication" affiche une bulle temporisée
|
||||
});
|
||||
|
||||
test('forgeBindClicSurvolRules — "clic" exécute l\'action au clic, sans joueur ni tick', () => {
|
||||
const targetEl = { addEventListener(event, handler) { this._handlers = this._handlers || {}; this._handlers[event] = handler; }, removeEventListener() {} };
|
||||
const targetEl = {
|
||||
addEventListener(event, handler) { this._handlers = this._handlers || {}; this._handlers[event] = handler; },
|
||||
removeEventListener() {},
|
||||
};
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => targetEl };
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '0' } } };
|
||||
try {
|
||||
forgeBindClicSurvolRules({
|
||||
elements: [{
|
||||
id: 5, collision_rules: [{
|
||||
id: 5,
|
||||
collision_rules: [{
|
||||
trigger: 'clic',
|
||||
action: { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' },
|
||||
action: {
|
||||
type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1',
|
||||
},
|
||||
}],
|
||||
}],
|
||||
});
|
||||
@@ -291,7 +340,10 @@ test('forgeBindClicSurvolRules — "clic" exécute l\'action au clic, sans joueu
|
||||
});
|
||||
|
||||
test('forgeBindClicSurvolRules — "survol" se lie sur mouseenter, jamais sur click', () => {
|
||||
const targetEl = { addEventListener(event, handler) { this._handlers = this._handlers || {}; this._handlers[event] = handler; }, removeEventListener() {} };
|
||||
const targetEl = {
|
||||
addEventListener(event, handler) { this._handlers = this._handlers || {}; this._handlers[event] = handler; },
|
||||
removeEventListener() {},
|
||||
};
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => targetEl };
|
||||
try {
|
||||
@@ -302,7 +354,10 @@ test('forgeBindClicSurvolRules — "survol" se lie sur mouseenter, jamais sur cl
|
||||
});
|
||||
|
||||
test('forgeBindClicSurvolRules — un déclencheur "collision" n\'est jamais lié en écouteur DOM (reste géré par le tick)', () => {
|
||||
const targetEl = { addEventListener(event, handler) { this._handlers = this._handlers || {}; this._handlers[event] = handler; }, removeEventListener() {} };
|
||||
const targetEl = {
|
||||
addEventListener(event, handler) { this._handlers = this._handlers || {}; this._handlers[event] = handler; },
|
||||
removeEventListener() {},
|
||||
};
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => targetEl };
|
||||
try {
|
||||
@@ -312,18 +367,24 @@ test('forgeBindClicSurvolRules — un déclencheur "collision" n\'est jamais li
|
||||
});
|
||||
|
||||
test('tick — la collision entre le joueur et un objet ne redéclenche pas "dialogue" tant que le contact persiste', () => {
|
||||
const player = { id: 1, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { interagir: 'e' } };
|
||||
const npc = { id: 2, kind: 'personnage', name: 'Crokine', collision_rules: [{ trigger: 'collision', action: { type: 'dialogue', dialogue: { id: 'd_7', lines: [] } } }] };
|
||||
const player = {
|
||||
id: 1, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { interagir: 'e' },
|
||||
};
|
||||
const npc = {
|
||||
id: 2, kind: 'personnage', name: 'Crokine', collision_rules: [{ trigger: 'collision', action: { type: 'dialogue', dialogue: { id: 'd_7', lines: [] } } }],
|
||||
};
|
||||
const playerEl = fakeEl();
|
||||
const npcEl = fakeEl();
|
||||
npcEl.style = { left: '5px', top: '5px', width: '20px', height: '20px' }; // chevauche playerEl (0,0,20,20)
|
||||
npcEl.style = {
|
||||
left: '5px', top: '5px', width: '20px', height: '20px',
|
||||
}; // chevauche playerEl (0,0,20,20)
|
||||
|
||||
const previousDoc = global.document;
|
||||
global.document = fakeDocument({ 1: playerEl, 2: npcEl });
|
||||
global.gameData = { personnage_animations: {} };
|
||||
const previousShow = global.forgeShowDialogueBox;
|
||||
const calls = [];
|
||||
global.forgeShowDialogueBox = id => calls.push(id);
|
||||
global.forgeShowDialogueBox = (id) => calls.push(id);
|
||||
try {
|
||||
const screenData = { elements: [player, npc] };
|
||||
forgeCollisionRuleTick(screenData); // entrée en collision -> déclenche
|
||||
@@ -340,17 +401,24 @@ test('tick — la bulle "Appuie sur X" s\'ancre sur la BOÎTE DE COLLISION, pas
|
||||
// déjà collision.width/height/offset_x/offset_y) doit être utilisé.
|
||||
const player = { id: 1, kind: 'personnage', personnage_role: 'joueur' };
|
||||
const pnj = {
|
||||
id: 3, kind: 'personnage',
|
||||
id: 3,
|
||||
kind: 'personnage',
|
||||
// Sprite de 100×200 px, mais silhouette réelle 20×20 centrée en bas
|
||||
// (offset_y positif = décalée vers le bas, voir collision_settings.py).
|
||||
collision: { enabled: true, shape: 'rectangle', width: 20, height: 20, offset_x: 0, offset_y: 80 },
|
||||
collision: {
|
||||
enabled: true, shape: 'rectangle', width: 20, height: 20, offset_x: 0, offset_y: 80,
|
||||
},
|
||||
collision_rules: [{ trigger: 'collision', action: { type: 'interagir', sub_action: { type: 'dialogue' } } }],
|
||||
};
|
||||
const parent = fakeParentEl();
|
||||
const playerEl = fakeEl(parent);
|
||||
playerEl.style = { left: '45px', top: '175px', width: '20px', height: '20px' }; // chevauche la silhouette réelle du pnj (40,170)-(60,190)
|
||||
playerEl.style = {
|
||||
left: '45px', top: '175px', width: '20px', height: '20px',
|
||||
}; // chevauche la silhouette réelle du pnj (40,170)-(60,190)
|
||||
const pnjEl = fakeEl(parent);
|
||||
pnjEl.style = { left: '0px', top: '0px', width: '100px', height: '200px' }; // grand cadre padé
|
||||
pnjEl.style = {
|
||||
left: '0px', top: '0px', width: '100px', height: '200px',
|
||||
}; // grand cadre padé
|
||||
|
||||
const previousDoc = global.document;
|
||||
global.gameData = { personnage_animations: {} };
|
||||
@@ -365,16 +433,22 @@ test('tick — la bulle "Appuie sur X" s\'ancre sur la BOÎTE DE COLLISION, pas
|
||||
});
|
||||
|
||||
test('tick — "interagir" affiche la bulle "Appuie sur [touche]" pendant la collision, puis la cache en sortant', () => {
|
||||
const player = { id: 1, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { interagir: 'e' } };
|
||||
const player = {
|
||||
id: 1, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { interagir: 'e' },
|
||||
};
|
||||
const pnj = { id: 3, kind: 'personnage', collision_rules: [{ trigger: 'collision', action: { type: 'interagir', sub_action: { type: 'dialogue' } } }] };
|
||||
const parentNear = fakeParentEl();
|
||||
const parentFar = fakeParentEl();
|
||||
const playerElNear = fakeEl(parentNear);
|
||||
const pnjElNear = fakeEl(parentNear);
|
||||
pnjElNear.style = { left: '5px', top: '5px', width: '20px', height: '20px' };
|
||||
pnjElNear.style = {
|
||||
left: '5px', top: '5px', width: '20px', height: '20px',
|
||||
};
|
||||
const playerElFar = fakeEl(parentFar);
|
||||
const pnjElFar = fakeEl(parentFar);
|
||||
pnjElFar.style = { left: '300px', top: '300px', width: '20px', height: '20px' };
|
||||
pnjElFar.style = {
|
||||
left: '300px', top: '300px', width: '20px', height: '20px',
|
||||
};
|
||||
|
||||
const previousDoc = global.document;
|
||||
global.gameData = { personnage_animations: {} };
|
||||
|
||||
@@ -79,7 +79,7 @@ test('resolveVariablePath — JSON invalide ou chemin absent renvoie null', () =
|
||||
});
|
||||
|
||||
test('readFieldValue — résout CLICKED_ROW_ID (-1) via window.lastClickedRowId', () => {
|
||||
withGameData({ data: { '1': [{ id: 7, score: 42 }] } }, () => {
|
||||
withGameData({ data: { 1: [{ id: 7, score: 42 }] } }, () => {
|
||||
window.lastClickedRowId = 7;
|
||||
assert.equal(readFieldValue(1, -1, 'score'), 42);
|
||||
});
|
||||
@@ -93,22 +93,28 @@ test('readVariableValue — lit gameData.variables par nom', () => {
|
||||
});
|
||||
|
||||
test('evaluateConditionClause — source "objet" (comportement historique)', () => {
|
||||
withGameData({ data: { '1': [{ id: 5, pv: '3' }] } }, () => {
|
||||
const clause = { cond_definition_id: 1, cond_row_id: 5, cond_field: 'pv', cond_operator: 'inferieur', cond_value: '10' };
|
||||
withGameData({ data: { 1: [{ id: 5, pv: '3' }] } }, () => {
|
||||
const clause = {
|
||||
cond_definition_id: 1, cond_row_id: 5, cond_field: 'pv', cond_operator: 'inferieur', cond_value: '10',
|
||||
};
|
||||
assert.equal(evaluateConditionClause(clause), true);
|
||||
});
|
||||
});
|
||||
|
||||
test('evaluateConditionClause — source "variable"', () => {
|
||||
withGameData({ variables: { pv: { type: 'nombre_entier', value: '3' } } }, () => {
|
||||
const clause = { cond_source: 'variable', cond_variable: 'pv', cond_operator: 'inferieur', cond_value: '10' };
|
||||
const clause = {
|
||||
cond_source: 'variable', cond_variable: 'pv', cond_operator: 'inferieur', cond_value: '10',
|
||||
};
|
||||
assert.equal(evaluateConditionClause(clause), true);
|
||||
});
|
||||
});
|
||||
|
||||
test('evaluateConditionNode — clause unique (pas de cond_clauses)', () => {
|
||||
withGameData({ variables: { pv: { type: 'nombre_entier', value: '3' } } }, () => {
|
||||
const node = { cond_source: 'variable', cond_variable: 'pv', cond_operator: 'egal', cond_value: '3' };
|
||||
const node = {
|
||||
cond_source: 'variable', cond_variable: 'pv', cond_operator: 'egal', cond_value: '3',
|
||||
};
|
||||
assert.equal(evaluateConditionNode(node), true);
|
||||
});
|
||||
});
|
||||
@@ -118,8 +124,12 @@ test('evaluateConditionNode — clauses combinées ET (toutes vraies requises)',
|
||||
const node = {
|
||||
cond_combinator: 'et',
|
||||
cond_clauses: JSON.stringify([
|
||||
{ source: 'variable', variable: 'pv', operator: 'egal', value: '3' },
|
||||
{ source: 'variable', variable: 'or', operator: 'egal', value: '0' },
|
||||
{
|
||||
source: 'variable', variable: 'pv', operator: 'egal', value: '3',
|
||||
},
|
||||
{
|
||||
source: 'variable', variable: 'or', operator: 'egal', value: '0',
|
||||
},
|
||||
]),
|
||||
};
|
||||
assert.equal(evaluateConditionNode(node), true);
|
||||
@@ -131,8 +141,12 @@ test('evaluateConditionNode — clauses combinées OU (une seule vraie suffit)',
|
||||
const node = {
|
||||
cond_combinator: 'ou',
|
||||
cond_clauses: JSON.stringify([
|
||||
{ source: 'variable', variable: 'pv', operator: 'egal', value: '999' },
|
||||
{ source: 'variable', variable: 'or', operator: 'egal', value: '0' },
|
||||
{
|
||||
source: 'variable', variable: 'pv', operator: 'egal', value: '999',
|
||||
},
|
||||
{
|
||||
source: 'variable', variable: 'or', operator: 'egal', value: '0',
|
||||
},
|
||||
]),
|
||||
};
|
||||
assert.equal(evaluateConditionNode(node), true);
|
||||
@@ -144,45 +158,85 @@ test('evaluateConditionNode — clauses combinées OU (une seule vraie suffit)',
|
||||
// collision (= sa boîte visuelle par défaut), configurable (taille,
|
||||
// forme, activée/désactivée).
|
||||
test('forgeCollisionRectFromBox — sans réglage, le rectangle visuel EXACT (comportement inchangé)', () => {
|
||||
const box = { left: 10, top: 20, width: 40, height: 60 };
|
||||
assert.deepEqual(forgeCollisionRectFromBox(box, undefined), { left: 10, top: 20, right: 50, bottom: 80, width: 40, height: 60, shape: 'rectangle' });
|
||||
const box = {
|
||||
left: 10, top: 20, width: 40, height: 60,
|
||||
};
|
||||
assert.deepEqual(forgeCollisionRectFromBox(box, undefined), {
|
||||
left: 10, top: 20, right: 50, bottom: 80, width: 40, height: 60, shape: 'rectangle',
|
||||
});
|
||||
});
|
||||
|
||||
test('forgeCollisionRectFromBox — une boîte plus petite est centrée par défaut (offset 0,0)', () => {
|
||||
const box = { left: 0, top: 0, width: 100, height: 100 };
|
||||
const rect = forgeCollisionRectFromBox(box, { width: 20, height: 20, offset_x: 0, offset_y: 0 });
|
||||
assert.deepEqual(rect, { left: 40, top: 40, right: 60, bottom: 60, width: 20, height: 20, shape: 'rectangle' });
|
||||
const box = {
|
||||
left: 0, top: 0, width: 100, height: 100,
|
||||
};
|
||||
const rect = forgeCollisionRectFromBox(box, {
|
||||
width: 20, height: 20, offset_x: 0, offset_y: 0,
|
||||
});
|
||||
assert.deepEqual(rect, {
|
||||
left: 40, top: 40, right: 60, bottom: 60, width: 20, height: 20, shape: 'rectangle',
|
||||
});
|
||||
});
|
||||
|
||||
test('forgeCollisionRectFromBox — un décalage déplace la boîte de collision sans toucher au rectangle visuel', () => {
|
||||
const box = { left: 0, top: 0, width: 100, height: 100 };
|
||||
const rect = forgeCollisionRectFromBox(box, { width: 20, height: 20, offset_x: 15, offset_y: -5 });
|
||||
assert.deepEqual(rect, { left: 55, top: 35, right: 75, bottom: 55, width: 20, height: 20, shape: 'rectangle' });
|
||||
const box = {
|
||||
left: 0, top: 0, width: 100, height: 100,
|
||||
};
|
||||
const rect = forgeCollisionRectFromBox(box, {
|
||||
width: 20, height: 20, offset_x: 15, offset_y: -5,
|
||||
});
|
||||
assert.deepEqual(rect, {
|
||||
left: 55, top: 35, right: 75, bottom: 55, width: 20, height: 20, shape: 'rectangle',
|
||||
});
|
||||
});
|
||||
|
||||
test('forgeRectanglesOverlap — chevauchement/non-chevauchement', () => {
|
||||
assert.equal(forgeRectanglesOverlap({ left: 0, top: 0, right: 10, bottom: 10 }, { left: 5, top: 5, right: 15, bottom: 15 }), true);
|
||||
assert.equal(forgeRectanglesOverlap({ left: 0, top: 0, right: 10, bottom: 10 }, { left: 20, top: 20, right: 30, bottom: 30 }), false);
|
||||
assert.equal(forgeRectanglesOverlap({
|
||||
left: 0, top: 0, right: 10, bottom: 10,
|
||||
}, {
|
||||
left: 5, top: 5, right: 15, bottom: 15,
|
||||
}), true);
|
||||
assert.equal(forgeRectanglesOverlap({
|
||||
left: 0, top: 0, right: 10, bottom: 10,
|
||||
}, {
|
||||
left: 20, top: 20, right: 30, bottom: 30,
|
||||
}), false);
|
||||
});
|
||||
|
||||
test('forgeCirclesOverlap — deux cercles qui se touchent/s\'évitent', () => {
|
||||
const a = { left: 0, top: 0, right: 20, bottom: 20, width: 20, height: 20 }; // centre (10,10), rayon 10
|
||||
const touching = { left: 15, top: 0, right: 35, bottom: 20, width: 20, height: 20 }; // centre (25,10), rayon 10, distance 15 < 20
|
||||
const apart = { left: 100, top: 100, right: 120, bottom: 120, width: 20, height: 20 };
|
||||
const a = {
|
||||
left: 0, top: 0, right: 20, bottom: 20, width: 20, height: 20,
|
||||
}; // centre (10,10), rayon 10
|
||||
const touching = {
|
||||
left: 15, top: 0, right: 35, bottom: 20, width: 20, height: 20,
|
||||
}; // centre (25,10), rayon 10, distance 15 < 20
|
||||
const apart = {
|
||||
left: 100, top: 100, right: 120, bottom: 120, width: 20, height: 20,
|
||||
};
|
||||
assert.equal(forgeCirclesOverlap(a, touching), true);
|
||||
assert.equal(forgeCirclesOverlap(a, apart), false);
|
||||
});
|
||||
|
||||
test('forgeShapesOverlap — deux cercles utilisent le test cercle-cercle', () => {
|
||||
const a = { left: 0, top: 0, right: 20, bottom: 20, width: 20, height: 20, shape: 'cercle' };
|
||||
const b = { left: 15, top: 0, right: 35, bottom: 20, width: 20, height: 20, shape: 'cercle' };
|
||||
const apart = { left: 100, top: 100, right: 120, bottom: 120, width: 20, height: 20, shape: 'cercle' };
|
||||
const a = {
|
||||
left: 0, top: 0, right: 20, bottom: 20, width: 20, height: 20, shape: 'cercle',
|
||||
};
|
||||
const b = {
|
||||
left: 15, top: 0, right: 35, bottom: 20, width: 20, height: 20, shape: 'cercle',
|
||||
};
|
||||
const apart = {
|
||||
left: 100, top: 100, right: 120, bottom: 120, width: 20, height: 20, shape: 'cercle',
|
||||
};
|
||||
assert.equal(forgeShapesOverlap(a, b), true);
|
||||
assert.equal(forgeShapesOverlap(a, apart), false);
|
||||
});
|
||||
|
||||
test('forgeShapesOverlap — une paire rectangle/cercle retombe sur le test rectangle-rectangle', () => {
|
||||
const rect = { left: 0, top: 0, right: 20, bottom: 20, shape: 'rectangle' };
|
||||
const circle = { left: 15, top: 15, right: 35, bottom: 35, shape: 'cercle' };
|
||||
const rect = {
|
||||
left: 0, top: 0, right: 20, bottom: 20, shape: 'rectangle',
|
||||
};
|
||||
const circle = {
|
||||
left: 15, top: 15, right: 35, bottom: 35, shape: 'cercle',
|
||||
};
|
||||
assert.equal(forgeShapesOverlap(rect, circle), true); // les rectangles englobants se chevauchent
|
||||
});
|
||||
|
||||
@@ -28,18 +28,25 @@ function fakeWidget(objectId) {
|
||||
};
|
||||
const classes = new Set();
|
||||
return {
|
||||
getAttribute: attr => (attr === 'data-object-id' ? String(objectId) : null),
|
||||
getAttribute: (attr) => (attr === 'data-object-id' ? String(objectId) : null),
|
||||
classList: {
|
||||
add: c => classes.add(c), remove: c => classes.delete(c), contains: c => classes.has(c),
|
||||
toggle: (c, force) => { const on = force === undefined ? !classes.has(c) : !!force; if (on) classes.add(c); else classes.delete(c); return on; },
|
||||
add: (c) => classes.add(c),
|
||||
remove: (c) => classes.delete(c),
|
||||
contains: (c) => classes.has(c),
|
||||
toggle: (c, force) => {
|
||||
const on = force === undefined ? !classes.has(c) : !!force;
|
||||
if (on) classes.add(c); else classes.delete(c);
|
||||
return on;
|
||||
},
|
||||
},
|
||||
querySelector: sel => {
|
||||
querySelector: (sel) => {
|
||||
if (sel === '[data-dialogue-role="header"]') return roles.header;
|
||||
if (sel === '[data-dialogue-role="body"]') return roles.body;
|
||||
if (sel === '[data-dialogue-role="footer"]') return roles.footer;
|
||||
return null;
|
||||
},
|
||||
_roles: roles, _classes: classes,
|
||||
_roles: roles,
|
||||
_classes: classes,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,16 +57,16 @@ function fakeWidget(objectId) {
|
||||
// couvrir le VRAI viewport qu'en s'échappant de ce conteneur, comme
|
||||
// static/js/play/video-overlay.js le fait déjà pour son propre overlay.
|
||||
test('forgeEscapeCameraForFullscreen déplace le widget en enfant de <body> et retient l\'écran d\'origine', () => {
|
||||
const screenEl = { getAttribute: attr => (attr === 'data-screen-id' ? '7' : null) };
|
||||
const screenEl = { getAttribute: (attr) => (attr === 'data-screen-id' ? '7' : null) };
|
||||
const attrs = {};
|
||||
const classes = new Set();
|
||||
const bodyChildren = [];
|
||||
const body = { appendChild: el => { bodyChildren.push(el); el.parentNode = body; } };
|
||||
const body = { appendChild: (el) => { bodyChildren.push(el); el.parentNode = body; } };
|
||||
const widget = {
|
||||
parentNode: null,
|
||||
closest: sel => (sel === '.playScreen' ? screenEl : null),
|
||||
closest: (sel) => (sel === '.playScreen' ? screenEl : null),
|
||||
setAttribute: (k, v) => { attrs[k] = v; },
|
||||
classList: { add: c => classes.add(c) },
|
||||
classList: { add: (c) => classes.add(c) },
|
||||
};
|
||||
const previousDoc = global.document;
|
||||
global.document = { body };
|
||||
@@ -108,7 +115,7 @@ test('forgeShowDialogueBox — dialogue vide, sans mark_completed : aucun effet'
|
||||
try {
|
||||
forgeShowDialogueBox('d_6', 'PNJ', []);
|
||||
assert.equal(widget._classes.has('is-active'), false);
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_6'], undefined);
|
||||
assert.equal(window.forgeCompletedDialogueIds.d_6, undefined);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
@@ -119,7 +126,7 @@ test('forgeShowDialogueBox — dialogue vide AVEC mark_completed : marque le dia
|
||||
try {
|
||||
forgeShowDialogueBox('d_6', 'PNJ', [], true);
|
||||
assert.equal(widget._classes.has('is-active'), false);
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_6'], true);
|
||||
assert.equal(window.forgeCompletedDialogueIds.d_6, true);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
@@ -134,7 +141,7 @@ test('forgeShowDialogueBox — aucun widget posé sur l\'écran : aucun effet',
|
||||
test('forgeShowDialogueBox puis forgeDialogueBoxAdvance — cycle toutes les répliques puis se masque', () => {
|
||||
const widget = fakeWidget(42);
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: sel => (sel.indexOf('data-object-id="42"') !== -1 ? widget : (sel.indexOf('dialogueBoxWidget') !== -1 ? widget : null)) };
|
||||
global.document = { querySelector: (sel) => (sel.indexOf('data-object-id="42"') !== -1 ? widget : (sel.indexOf('dialogueBoxWidget') !== -1 ? widget : null)) };
|
||||
try {
|
||||
forgeShowDialogueBox('d_8', 'PNJ', [{ speaker: 'PNJ', text: 'Merci !' }]);
|
||||
assert.equal(widget._roles.header.textContent, 'PNJ');
|
||||
@@ -182,7 +189,7 @@ test('forgeShowDialogueBox puis forgeDialogueBoxAdvance — la voix off de la r
|
||||
const paused = [];
|
||||
global.document = { querySelector: () => widget };
|
||||
global.Audio = function (url) {
|
||||
return { url: url, play: () => Promise.resolve(), pause: function () { paused.push(this.url); } };
|
||||
return { url, play: () => Promise.resolve(), pause() { paused.push(this.url); } };
|
||||
};
|
||||
try {
|
||||
forgeShowDialogueBox('d_9', 'Marc', [
|
||||
@@ -202,7 +209,7 @@ test('fin de dialogue SANS mark_completed : le dialogue n\'est jamais marqué te
|
||||
forgeShowDialogueBox('d_5', 'Le Sorcier', [{ speaker: 'Le Sorcier', text: 'Bonjour !' }, { speaker: 'Joueur', text: 'Salut.' }]);
|
||||
forgeDialogueBoxAdvance(42);
|
||||
forgeDialogueBoxAdvance(42); // dernière réplique consommée
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_5'], undefined);
|
||||
assert.equal(window.forgeCompletedDialogueIds.d_5, undefined);
|
||||
assert.equal(widget._classes.has('is-active'), false);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
@@ -217,7 +224,7 @@ test('fin de dialogue AVEC mark_completed : marque le dialogue terminé et propa
|
||||
forgeShowDialogueBox('d_5', 'Le Sorcier', [{ speaker: 'Le Sorcier', text: 'Bonjour !' }, { speaker: 'Joueur', text: 'Salut.' }], true);
|
||||
forgeDialogueBoxAdvance(42);
|
||||
forgeDialogueBoxAdvance(42); // dernière réplique consommée -> dialogue terminé
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_5'], true);
|
||||
assert.equal(window.forgeCompletedDialogueIds.d_5, true);
|
||||
assert.equal(widget._classes.has('is-active'), false);
|
||||
assert.equal(gameData.scoring.status, 'termine'); // seul dialogue connu, désormais complété
|
||||
} finally { global.document = previousDoc; }
|
||||
@@ -231,7 +238,9 @@ test('forgeShowDialogueBox — dialogue vide AVEC "then" : exécute immédiateme
|
||||
global.forgeRunCollisionRuleAction = (...args) => calls.push(args);
|
||||
global.document = { querySelector: () => widget };
|
||||
try {
|
||||
const thenAction = { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' };
|
||||
const thenAction = {
|
||||
type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1',
|
||||
};
|
||||
forgeShowDialogueBox('d_6', 'Crokine', [], false, thenAction);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0][0], thenAction);
|
||||
@@ -247,7 +256,9 @@ test('fin de dialogue AVEC "then" : exécute l\'action chaînée APRÈS la derni
|
||||
global.forgeRunCollisionRuleAction = (...args) => calls.push(args);
|
||||
global.document = { querySelector: () => widget };
|
||||
try {
|
||||
const thenAction = { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' };
|
||||
const thenAction = {
|
||||
type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1',
|
||||
};
|
||||
forgeShowDialogueBox('d_9', 'Crokine', [{ speaker: 'PNJ', text: 'Salut !' }], false, thenAction);
|
||||
assert.equal(calls.length, 0); // pas encore, le dialogue n'est pas terminé
|
||||
forgeDialogueBoxAdvance(42); // dernière réplique consommée -> "then" exécuté maintenant
|
||||
@@ -268,14 +279,21 @@ test('forgeDialogueBoxAdvance — objet inconnu (jamais affiché) : aucun effet'
|
||||
|
||||
function fakeQuizChoiceBtn() {
|
||||
const classes = new Set();
|
||||
return { disabled: false, classList: { add: c => classes.add(c), contains: c => classes.has(c) }, _classes: classes };
|
||||
return {
|
||||
disabled: false,
|
||||
classList: { add: (c) => classes.add(c), contains: (c) => classes.has(c) },
|
||||
_classes: classes,
|
||||
};
|
||||
}
|
||||
|
||||
function fakeQuizWidget(objectId, choiceCount, attrs) {
|
||||
attrs = attrs || {};
|
||||
const roles = {
|
||||
header: { textContent: '' }, question: { textContent: '' }, choices: { innerHTML: '' },
|
||||
score: { textContent: '' }, timer: { textContent: '', classList: { add() {}, remove() {}, toggle() {} } },
|
||||
header: { textContent: '' },
|
||||
question: { textContent: '' },
|
||||
choices: { innerHTML: '' },
|
||||
score: { textContent: '' },
|
||||
timer: { textContent: '', classList: { add() {}, remove() {}, toggle() {} } },
|
||||
};
|
||||
// Posé désactivé par forgeShowQuizBox dès la question (jamais recréé
|
||||
// par forgeQuizBoxAnswer, qui se contente de .disabled=false) — voir
|
||||
@@ -294,8 +312,8 @@ function fakeQuizWidget(objectId, choiceCount, attrs) {
|
||||
// Simule ".closest('.playScreen')" (voir forgeEscapeCameraForFullscreen) :
|
||||
// un widget "posé sur l'écran courant" est toujours, au départ,
|
||||
// l'enfant d'un ".playScreen" fictif portant le même id d'écran.
|
||||
closest: sel => (sel === '.playScreen' ? { getAttribute: a => (a === 'data-screen-id' ? String(window.currentScreenId) : null) } : null),
|
||||
getAttribute: attr => {
|
||||
closest: (sel) => (sel === '.playScreen' ? { getAttribute: (a) => (a === 'data-screen-id' ? String(window.currentScreenId) : null) } : null),
|
||||
getAttribute: (attr) => {
|
||||
if (attr === 'data-object-id') return String(objectId);
|
||||
if (attr in attrs) return attrs[attr];
|
||||
return null;
|
||||
@@ -305,11 +323,16 @@ function fakeQuizWidget(objectId, choiceCount, attrs) {
|
||||
// Variadique (comme le vrai DOMTokenList.remove/add) : le moteur
|
||||
// appelle parfois .remove('is-active', 'is-fullscreen') d'un coup
|
||||
// (voir forgeQuizResultQuit/forgeHideQuizBoxOnCurrentScreen).
|
||||
add: (...cs) => cs.forEach(c => classes.add(c)), remove: (...cs) => cs.forEach(c => classes.delete(c)),
|
||||
contains: c => classes.has(c),
|
||||
toggle: (c, force) => { const on = force === undefined ? !classes.has(c) : !!force; if (on) classes.add(c); else classes.delete(c); return on; },
|
||||
add: (...cs) => cs.forEach((c) => classes.add(c)),
|
||||
remove: (...cs) => cs.forEach((c) => classes.delete(c)),
|
||||
contains: (c) => classes.has(c),
|
||||
toggle: (c, force) => {
|
||||
const on = force === undefined ? !classes.has(c) : !!force;
|
||||
if (on) classes.add(c); else classes.delete(c);
|
||||
return on;
|
||||
},
|
||||
},
|
||||
querySelector: sel => {
|
||||
querySelector: (sel) => {
|
||||
if (sel === '[data-quiz-role="header"]') return roles.header;
|
||||
if (sel === '[data-quiz-role="question"]') return roles.question;
|
||||
if (sel === '[data-quiz-role="choices"]') return roles.choices;
|
||||
@@ -318,9 +341,12 @@ function fakeQuizWidget(objectId, choiceCount, attrs) {
|
||||
if (sel === '.quizBoxContinueBtn') return continueBtn;
|
||||
return null;
|
||||
},
|
||||
querySelectorAll: sel => (sel === '.quizBoxChoiceBtn' ? buttons : []),
|
||||
querySelectorAll: (sel) => (sel === '.quizBoxChoiceBtn' ? buttons : []),
|
||||
offsetWidth: 100,
|
||||
_roles: roles, _classes: classes, _buttons: buttons, _continueBtn: continueBtn,
|
||||
_roles: roles,
|
||||
_classes: classes,
|
||||
_buttons: buttons,
|
||||
_continueBtn: continueBtn,
|
||||
};
|
||||
return widget;
|
||||
}
|
||||
@@ -351,7 +377,9 @@ function docWithDialogueAndQuiz(dialogueWidget, quizWidget) {
|
||||
|
||||
const QUIZ_LINES = [
|
||||
{ type: 'dialogue', speaker: 'PNJ', text: 'Une question pour toi.' },
|
||||
{ type: 'question', text: '2+2 ?', choices: ['3', '4'], correct_index: 1, reward_type: 'score', reward_amount: 5 },
|
||||
{
|
||||
type: 'question', text: '2+2 ?', choices: ['3', '4'], correct_index: 1, reward_type: 'score', reward_amount: 5,
|
||||
},
|
||||
{ type: 'dialogue', speaker: 'PNJ', text: 'Bien joué !' },
|
||||
];
|
||||
|
||||
@@ -372,7 +400,7 @@ function docWithQuizOnlyNoDialogueBox(quizWidget) {
|
||||
if (sel.indexOf('data-object-id="99"') !== -1) return quizWidget || null;
|
||||
return null; // jamais de ".dialogueBoxWidget" sur cet écran
|
||||
},
|
||||
querySelectorAll: sel => (sel === '.quizBoxWidget [data-quiz-role="score"]' ? [quizWidget._roles.score] : []),
|
||||
querySelectorAll: (sel) => (sel === '.quizBoxWidget [data-quiz-role="score"]' ? [quizWidget._roles.score] : []),
|
||||
body: { appendChild: () => {} },
|
||||
};
|
||||
}
|
||||
@@ -383,7 +411,9 @@ test('un quiz SANS aucune "💬 Boîte de dialogue" posée (que des questions) a
|
||||
global.document = docWithQuizOnlyNoDialogueBox(quizWidget);
|
||||
try {
|
||||
forgeShowDialogueBox('d_30', 'Quiz', [
|
||||
{ type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1 },
|
||||
{
|
||||
type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1,
|
||||
},
|
||||
]);
|
||||
assert.equal(quizWidget._classes.has('is-active'), true);
|
||||
assert.equal(quizWidget._roles.question.textContent, 'Qui es-tu ?');
|
||||
@@ -396,12 +426,14 @@ test('un quiz SANS boîte de dialogue : répondre avance/termine correctement (m
|
||||
global.document = docWithQuizOnlyNoDialogueBox(quizWidget);
|
||||
try {
|
||||
forgeShowDialogueBox('d_31', 'Quiz', [
|
||||
{ type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1 },
|
||||
{
|
||||
type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1,
|
||||
},
|
||||
], true); // markCompleted
|
||||
forgeQuizBoxAnswer(99, 0); // bonne réponse
|
||||
assert.equal(window.forgeQuizScore, 1);
|
||||
forgeQuizBoxContinue(99); // -> plus aucune ligne, dialogue épuisé
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_31'], true);
|
||||
assert.equal(window.forgeCompletedDialogueIds.d_31, true);
|
||||
assert.equal(quizWidget._classes.has('is-active'), false);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
@@ -430,7 +462,9 @@ test('forgeQuizBoxContinue à la DERNIÈRE question d\'un modèle avec écran de
|
||||
withFakeTemplateEffects({ classique: { onQuizComplete: (w, result) => { completeCalledWith = result; } } }, () => {
|
||||
try {
|
||||
forgeShowDialogueBox('d_50', 'Quiz', [
|
||||
{ type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1 },
|
||||
{
|
||||
type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1,
|
||||
},
|
||||
]);
|
||||
forgeQuizBoxAnswer(99, 0); // bonne réponse — seule et dernière question
|
||||
forgeQuizBoxContinue(99);
|
||||
@@ -449,8 +483,12 @@ test('forgeQuizBoxContinue AVANT la dernière question : jamais d\'écran de ré
|
||||
withFakeTemplateEffects({ classique: { onQuizComplete: () => { completeCalled = true; } } }, () => {
|
||||
try {
|
||||
forgeShowDialogueBox('d_51', 'Quiz', [
|
||||
{ type: 'question', text: 'Question 1 ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1 },
|
||||
{ type: 'question', text: 'Question 2 ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1 },
|
||||
{
|
||||
type: 'question', text: 'Question 1 ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1,
|
||||
},
|
||||
{
|
||||
type: 'question', text: 'Question 2 ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1,
|
||||
},
|
||||
]);
|
||||
forgeQuizBoxAnswer(99, 0);
|
||||
forgeQuizBoxContinue(99); // avance vers la question 2, pas la fin
|
||||
@@ -470,7 +508,9 @@ test('forgeQuizResultQuit (bouton de l\'écran de résultat) masque le widget PU
|
||||
withFakeTemplateEffects({ classique: { onQuizComplete: () => {} } }, () => {
|
||||
try {
|
||||
forgeShowDialogueBox('d_52', 'Quiz', [
|
||||
{ type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1 },
|
||||
{
|
||||
type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1,
|
||||
},
|
||||
]);
|
||||
forgeQuizBoxAnswer(99, 0);
|
||||
forgeQuizBoxContinue(99); // -> écran de résultat, widget toujours actif
|
||||
@@ -495,7 +535,9 @@ test('forgeQuizResultQuit ne plante jamais si window.close n\'existe pas (naviga
|
||||
withFakeTemplateEffects({ classique: { onQuizComplete: () => {} } }, () => {
|
||||
try {
|
||||
forgeShowDialogueBox('d_54', 'Quiz', [
|
||||
{ type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1 },
|
||||
{
|
||||
type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1,
|
||||
},
|
||||
]);
|
||||
forgeQuizBoxAnswer(99, 0);
|
||||
forgeQuizBoxContinue(99);
|
||||
@@ -514,15 +556,21 @@ test('forgeQuizResultQuit ne fait rien sans widget correspondant', () => {
|
||||
|
||||
test('forgeQuizBoxTimeout (minuteur du quiz ENTIER expiré) affiche l\'écran de résultat même s\'il restait des questions', (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||
const quizWidget = fakeQuizWidget(99, 2, { 'data-timer-mode': 'quiz', 'data-timer-seconds': '5', 'data-fullscreen': '1', 'data-quiz-template': 'classique' });
|
||||
const quizWidget = fakeQuizWidget(99, 2, {
|
||||
'data-timer-mode': 'quiz', 'data-timer-seconds': '5', 'data-fullscreen': '1', 'data-quiz-template': 'classique',
|
||||
});
|
||||
const previousDoc = global.document;
|
||||
let completeCalledWith = null;
|
||||
global.document = docWithQuizOnlyNoDialogueBox(quizWidget);
|
||||
withFakeTemplateEffects({ classique: { onQuizComplete: (w, result) => { completeCalledWith = result; } } }, () => {
|
||||
try {
|
||||
forgeShowDialogueBox('d_53', 'Quiz', [
|
||||
{ type: 'question', text: 'Question 1 ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1 },
|
||||
{ type: 'question', text: 'Question 2 ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1 },
|
||||
{
|
||||
type: 'question', text: 'Question 1 ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1,
|
||||
},
|
||||
{
|
||||
type: 'question', text: 'Question 2 ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 1,
|
||||
},
|
||||
]);
|
||||
forgeQuizBoxTimeout(99, true); // encore la question 2 à venir
|
||||
t.mock.timers.tick(1500);
|
||||
@@ -552,7 +600,7 @@ test('"Continuer →" est TOUJOURS présent dès la question (désactivé), jama
|
||||
|
||||
test('une ligne "question" est affichée dans la boîte à QUIZ (header = nom de l\'objet déclencheur, corps = question+choix), pas la boîte de dialogue', () => {
|
||||
const dialogueWidget = fakeWidget(42);
|
||||
dialogueWidget.getAttribute = attr => (attr === 'data-object-id' ? '42' : null);
|
||||
dialogueWidget.getAttribute = (attr) => (attr === 'data-object-id' ? '42' : null);
|
||||
const quizWidget = fakeQuizWidget(99, 2);
|
||||
const previousDoc = global.document;
|
||||
global.document = docWithDialogueAndQuiz(dialogueWidget, quizWidget);
|
||||
@@ -627,7 +675,7 @@ test('bonne réponse au quiz : crédite le score, la surligne en vert, "Continue
|
||||
assert.equal(dialogueWidget._roles.body.textContent, 'Bien joué !');
|
||||
|
||||
forgeDialogueBoxAdvance(42); // dernière ligne -> dialogue épuisé, mark_completed -> terminé
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_10'], true);
|
||||
assert.equal(window.forgeCompletedDialogueIds.d_10, true);
|
||||
assert.equal(dialogueWidget._classes.has('is-active'), false);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
@@ -733,9 +781,9 @@ test('minuteur "pour tout le quiz" expiré : révèle la bonne réponse PUIS ter
|
||||
forgeDialogueBoxAdvance(42); // -> question, démarre le minuteur "quiz" (5s)
|
||||
t.mock.timers.tick(5000); // expire : révèle la bonne réponse
|
||||
assert.equal(quizWidget._buttons[1]._classes.has('is-correct'), true);
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_20'], undefined); // pas encore, le délai de lecture tourne
|
||||
assert.equal(window.forgeCompletedDialogueIds.d_20, undefined); // pas encore, le délai de lecture tourne
|
||||
t.mock.timers.tick(1500); // délai de lecture écoulé -> fin forcée du quiz
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_20'], true);
|
||||
assert.equal(window.forgeCompletedDialogueIds.d_20, true);
|
||||
assert.equal(quizWidget._classes.has('is-active'), false);
|
||||
assert.equal(quizWidget._classes.has('is-fullscreen'), false);
|
||||
} finally { global.document = previousDoc; }
|
||||
@@ -760,7 +808,7 @@ test('forgeUpdateAllScoreWidgets — met à jour tous les widgets "🏆 Score" a
|
||||
const previousDoc = global.document;
|
||||
window.forgeQuizScore = 42;
|
||||
global.document = {
|
||||
querySelectorAll: sel => (sel === '.scoreWidget [data-score-role="value"]' ? values : []),
|
||||
querySelectorAll: (sel) => (sel === '.scoreWidget [data-score-role="value"]' ? values : []),
|
||||
};
|
||||
try {
|
||||
forgeUpdateAllScoreWidgets();
|
||||
@@ -777,7 +825,7 @@ test('forgeUpdateAllScoreWidgets — met aussi à jour le score affiché DANS un
|
||||
const previousDoc = global.document;
|
||||
window.forgeQuizScore = 7;
|
||||
global.document = {
|
||||
querySelectorAll: sel => (sel === '.quizBoxWidget [data-quiz-role="score"]' ? [quizScoreEl] : []),
|
||||
querySelectorAll: (sel) => (sel === '.quizBoxWidget [data-quiz-role="score"]' ? [quizScoreEl] : []),
|
||||
};
|
||||
try {
|
||||
forgeUpdateAllScoreWidgets();
|
||||
@@ -849,7 +897,7 @@ test('forgeCompleteDialogue marque le dialogue completed et notifie xAPI', () =>
|
||||
global.gameData.scoring = { score: 0, status: 'en_cours' };
|
||||
try {
|
||||
forgeCompleteDialogue('d_20', 'Crokine');
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_20'], true);
|
||||
assert.equal(window.forgeCompletedDialogueIds.d_20, true);
|
||||
assert.deepEqual(calls, [{ id: 'd_20', name: 'Crokine' }]);
|
||||
assert.equal(gameData.scoring.status, 'termine');
|
||||
} finally { delete global.forgeXapiNotifyDialogueCompleted; }
|
||||
@@ -900,20 +948,24 @@ test('forgeSyncAllDialoguesCompletionToScorm propage reussi/echoue (pas juste "t
|
||||
test('répondre à une question notifie xAPI avec la bonne/mauvaise réponse et le score gagné', () => {
|
||||
const dialogueWidget = fakeWidget(42);
|
||||
const quizWidget = {
|
||||
getAttribute: attr => (attr === 'data-object-id' ? '99' : null),
|
||||
getAttribute: (attr) => (attr === 'data-object-id' ? '99' : null),
|
||||
classList: { add: () => {}, remove: () => {}, toggle: () => {} },
|
||||
querySelector: sel => (sel.indexOf('choices') !== -1 ? { innerHTML: '' } : { textContent: '' }),
|
||||
querySelector: (sel) => (sel.indexOf('choices') !== -1 ? { innerHTML: '' } : { textContent: '' }),
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
const previousDoc = global.document;
|
||||
global.document = {
|
||||
querySelector: sel => (sel.indexOf('quizBoxWidget') !== -1 ? quizWidget : dialogueWidget),
|
||||
querySelector: (sel) => (sel.indexOf('quizBoxWidget') !== -1 ? quizWidget : dialogueWidget),
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
const calls = [];
|
||||
global.forgeXapiNotifyQuestionAnswered = (dialogueId, objectName, index, text, correct, score) => calls.push({ dialogueId, objectName, index, text, correct, score });
|
||||
global.forgeXapiNotifyQuestionAnswered = (dialogueId, objectName, index, text, correct, score) => calls.push({
|
||||
dialogueId, objectName, index, text, correct, score,
|
||||
});
|
||||
try {
|
||||
forgeShowDialogueBox('d_5', 'Le Sorcier', [{ type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 5 }]);
|
||||
forgeShowDialogueBox('d_5', 'Le Sorcier', [{
|
||||
type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 5,
|
||||
}]);
|
||||
forgeQuizBoxAnswer(99, 0); // bonne réponse (correct_index=0)
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].dialogueId, 'd_5');
|
||||
@@ -927,21 +979,32 @@ test('répondre à une question notifie xAPI avec la bonne/mauvaise réponse et
|
||||
test('répondre à une question notifie AUSSI le SCORM 2004 (cmi.interactions.n.*, indépendant de xAPI)', () => {
|
||||
const dialogueWidget = fakeWidget(42);
|
||||
const quizWidget = {
|
||||
getAttribute: attr => (attr === 'data-object-id' ? '99' : null),
|
||||
getAttribute: (attr) => (attr === 'data-object-id' ? '99' : null),
|
||||
classList: { add: () => {}, remove: () => {}, toggle: () => {} },
|
||||
querySelector: sel => (sel.indexOf('choices') !== -1 ? { innerHTML: '' } : { textContent: '' }),
|
||||
querySelector: (sel) => (sel.indexOf('choices') !== -1 ? { innerHTML: '' } : { textContent: '' }),
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
const previousDoc = global.document;
|
||||
global.document = {
|
||||
querySelector: sel => (sel.indexOf('quizBoxWidget') !== -1 ? quizWidget : dialogueWidget),
|
||||
querySelector: (sel) => (sel.indexOf('quizBoxWidget') !== -1 ? quizWidget : dialogueWidget),
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
const calls = [];
|
||||
global.forgeScorm2004NotifyQuestionAnswered = (dialogueId, objectName, index, text, correct, choiceIndex, correctIndex) =>
|
||||
calls.push({ dialogueId, objectName, index, text, correct, choiceIndex, correctIndex });
|
||||
global.forgeScorm2004NotifyQuestionAnswered = (
|
||||
dialogueId,
|
||||
objectName,
|
||||
index,
|
||||
text,
|
||||
correct,
|
||||
choiceIndex,
|
||||
correctIndex,
|
||||
) => calls.push({
|
||||
dialogueId, objectName, index, text, correct, choiceIndex, correctIndex,
|
||||
});
|
||||
try {
|
||||
forgeShowDialogueBox('d_5', 'Le Sorcier', [{ type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 5 }]);
|
||||
forgeShowDialogueBox('d_5', 'Le Sorcier', [{
|
||||
type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 5,
|
||||
}]);
|
||||
forgeQuizBoxAnswer(99, 1); // mauvaise réponse (correct_index=0, répond 1)
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].dialogueId, 'd_5');
|
||||
|
||||
@@ -12,6 +12,7 @@ global.screensData = [];
|
||||
global.heldKeys = new Set();
|
||||
|
||||
const actionsMod = require('../actions.js');
|
||||
|
||||
Object.assign(global, actionsMod);
|
||||
|
||||
const {
|
||||
@@ -19,14 +20,19 @@ const {
|
||||
forgeRunPersonnageControllerTick, forgeUpdateSceneWorldSize, forgeUpdateSceneCamera, forgeWouldCollide,
|
||||
} = require('../personnage-controller.js');
|
||||
|
||||
const { stopAllSpriteAnimations } = actionsMod;
|
||||
|
||||
function fakeSceneEl() {
|
||||
const classes = new Set();
|
||||
return {
|
||||
style: { left: '100px', top: '100px', width: '20px', height: '20px' },
|
||||
style: {
|
||||
left: '100px', top: '100px', width: '20px', height: '20px',
|
||||
},
|
||||
classList: {
|
||||
add: c => classes.add(c), remove: c => classes.delete(c),
|
||||
toggle: c => (classes.has(c) ? classes.delete(c) : classes.add(c)),
|
||||
contains: c => classes.has(c),
|
||||
add: (c) => classes.add(c),
|
||||
remove: (c) => classes.delete(c),
|
||||
toggle: (c) => (classes.has(c) ? classes.delete(c) : classes.add(c)),
|
||||
contains: (c) => classes.has(c),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -37,13 +43,12 @@ function withPersonnageObject(obj, elByObjectId, fn) {
|
||||
// (cherche ".sceneWorld", jamais présente dans elByObjectId ici) — un
|
||||
// sélecteur qui ne matche aucun id numérique renvoie undefined plutôt
|
||||
// que de lever, comme un vrai document.querySelector sans résultat.
|
||||
global.document = { querySelector: sel => { const m = sel.match(/"(\d+)"/); return m ? elByObjectId[m[1]] : undefined; } };
|
||||
global.document = { querySelector: (sel) => { const m = sel.match(/"(\d+)"/); return m ? elByObjectId[m[1]] : undefined; } };
|
||||
global.screensData = [{ id: 1, scene_width: 500, scene_height: 500 }];
|
||||
global.window.currentScreenId = 1;
|
||||
global.gameData = { personnage_animations: { [obj.id]: { walk: ['/w0.png'], idle: ['/i0.png'] } } };
|
||||
try { fn(); } finally { global.document = previousDoc; stopAllSpriteAnimations(); }
|
||||
}
|
||||
const { stopAllSpriteAnimations } = actionsMod;
|
||||
|
||||
test('forgePersonnageMovementAnim/forgePersonnageIdleAnim — repli silencieux si la pose est absente', () => {
|
||||
global.gameData = { personnage_animations: { 5: { idle: ['/i0.png'] } } };
|
||||
@@ -53,7 +58,14 @@ test('forgePersonnageMovementAnim/forgePersonnageIdleAnim — repli silencieux s
|
||||
});
|
||||
|
||||
test('tick — touche "droite" enfoncée : déplace vers la droite et retire le miroir', () => {
|
||||
const obj = { id: 10, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4 } };
|
||||
const obj = {
|
||||
id: 10,
|
||||
kind: 'personnage',
|
||||
personnage_role: 'joueur',
|
||||
personnage_commandes: {
|
||||
haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4,
|
||||
},
|
||||
};
|
||||
const el = fakeSceneEl();
|
||||
withPersonnageObject(obj, { 10: el }, () => {
|
||||
global.heldKeys = new Set(['d']);
|
||||
@@ -64,7 +76,14 @@ test('tick — touche "droite" enfoncée : déplace vers la droite et retire le
|
||||
});
|
||||
|
||||
test('tick — touche "gauche" enfoncée : déplace vers la gauche et applique le miroir', () => {
|
||||
const obj = { id: 11, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4 } };
|
||||
const obj = {
|
||||
id: 11,
|
||||
kind: 'personnage',
|
||||
personnage_role: 'joueur',
|
||||
personnage_commandes: {
|
||||
haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4,
|
||||
},
|
||||
};
|
||||
const el = fakeSceneEl();
|
||||
withPersonnageObject(obj, { 11: el }, () => {
|
||||
global.heldKeys = new Set(['q']);
|
||||
@@ -75,7 +94,14 @@ test('tick — touche "gauche" enfoncée : déplace vers la gauche et applique l
|
||||
});
|
||||
|
||||
test('tick — axe "horizontal" : ignore haut/bas même si la touche est enfoncée', () => {
|
||||
const obj = { id: 12, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'horizontal', vitesse: 4 } };
|
||||
const obj = {
|
||||
id: 12,
|
||||
kind: 'personnage',
|
||||
personnage_role: 'joueur',
|
||||
personnage_commandes: {
|
||||
haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'horizontal', vitesse: 4,
|
||||
},
|
||||
};
|
||||
const el = fakeSceneEl();
|
||||
withPersonnageObject(obj, { 12: el }, () => {
|
||||
global.heldKeys = new Set(['z']); // "haut", bloqué par l'axe
|
||||
@@ -85,7 +111,14 @@ test('tick — axe "horizontal" : ignore haut/bas même si la touche est enfonc
|
||||
});
|
||||
|
||||
test('tick — axe "vertical" : ignore gauche/droite même si la touche est enfoncée', () => {
|
||||
const obj = { id: 13, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'vertical', vitesse: 4 } };
|
||||
const obj = {
|
||||
id: 13,
|
||||
kind: 'personnage',
|
||||
personnage_role: 'joueur',
|
||||
personnage_commandes: {
|
||||
haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'vertical', vitesse: 4,
|
||||
},
|
||||
};
|
||||
const el = fakeSceneEl();
|
||||
withPersonnageObject(obj, { 13: el }, () => {
|
||||
global.heldKeys = new Set(['d']); // "droite", bloqué par l'axe
|
||||
@@ -95,7 +128,14 @@ test('tick — axe "vertical" : ignore gauche/droite même si la touche est enfo
|
||||
});
|
||||
|
||||
test('tick — bornage aux limites de la scène (ne sort jamais du cadre)', () => {
|
||||
const obj = { id: 14, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 50 } };
|
||||
const obj = {
|
||||
id: 14,
|
||||
kind: 'personnage',
|
||||
personnage_role: 'joueur',
|
||||
personnage_commandes: {
|
||||
haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 50,
|
||||
},
|
||||
};
|
||||
const el = fakeSceneEl();
|
||||
el.style.left = '490px'; // 500 (scène) - 20 (largeur) = 480 max
|
||||
withPersonnageObject(obj, { 14: el }, () => {
|
||||
@@ -106,7 +146,14 @@ test('tick — bornage aux limites de la scène (ne sort jamais du cadre)', () =
|
||||
});
|
||||
|
||||
test('tick — aucune touche enfoncée : ne bouge pas (l\'appel reste sans effet)', () => {
|
||||
const obj = { id: 15, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4 } };
|
||||
const obj = {
|
||||
id: 15,
|
||||
kind: 'personnage',
|
||||
personnage_role: 'joueur',
|
||||
personnage_commandes: {
|
||||
haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4,
|
||||
},
|
||||
};
|
||||
const el = fakeSceneEl();
|
||||
withPersonnageObject(obj, { 15: el }, () => {
|
||||
global.heldKeys = new Set();
|
||||
@@ -131,7 +178,14 @@ test('tick — ignore tout objet qui n\'est pas kind="personnage" ou sans comman
|
||||
// clavier, "ennemie"/"pnj" doivent rester immobiles tant qu'aucune
|
||||
// logique de flow ne les pilote.
|
||||
test('tick — un personnage "ennemie"/"pnj" n\'est jamais déplacé au clavier', () => {
|
||||
const ennemi = { id: 17, kind: 'personnage', personnage_role: 'ennemie', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4 } };
|
||||
const ennemi = {
|
||||
id: 17,
|
||||
kind: 'personnage',
|
||||
personnage_role: 'ennemie',
|
||||
personnage_commandes: {
|
||||
haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4,
|
||||
},
|
||||
};
|
||||
const el = fakeSceneEl();
|
||||
withPersonnageObject(ennemi, { 17: el }, () => {
|
||||
global.heldKeys = new Set(['d']);
|
||||
@@ -165,7 +219,14 @@ function fakeWorldEl() {
|
||||
}
|
||||
|
||||
test('forgeUpdateSceneWorldSize — le monde reste la scène tant qu\'aucun fond ne dépasse (comportement inchangé)', () => {
|
||||
const screenData = { id: 1, scene_width: 500, scene_height: 500, elements: [{ kind: 'decor', x: 10, y: 10, width: 50, height: 50 }] };
|
||||
const screenData = {
|
||||
id: 1,
|
||||
scene_width: 500,
|
||||
scene_height: 500,
|
||||
elements: [{
|
||||
kind: 'decor', x: 10, y: 10, width: 50, height: 50,
|
||||
}],
|
||||
};
|
||||
const world = forgeUpdateSceneWorldSize(screenData);
|
||||
assert.deepEqual(world, { width: 500, height: 500 });
|
||||
assert.equal(screenData.world_width, 500);
|
||||
@@ -173,7 +234,14 @@ test('forgeUpdateSceneWorldSize — le monde reste la scène tant qu\'aucun fond
|
||||
});
|
||||
|
||||
test('forgeUpdateSceneWorldSize — un fond plus grand agrandit le monde jusqu\'à ses propres bords', () => {
|
||||
const screenData = { id: 1, scene_width: 500, scene_height: 500, elements: [{ kind: 'fond', x: 0, y: 0, width: 3840, height: 2160 }] };
|
||||
const screenData = {
|
||||
id: 1,
|
||||
scene_width: 500,
|
||||
scene_height: 500,
|
||||
elements: [{
|
||||
kind: 'fond', x: 0, y: 0, width: 3840, height: 2160,
|
||||
}],
|
||||
};
|
||||
const world = forgeUpdateSceneWorldSize(screenData);
|
||||
assert.deepEqual(world, { width: 3840, height: 2160 });
|
||||
});
|
||||
@@ -182,7 +250,9 @@ test('forgeUpdateSceneCamera — sans fond XXL, transform vidé (aucun effet, co
|
||||
const worldEl = fakeWorldEl();
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => worldEl };
|
||||
const screenData = { id: 1, scene_width: 500, scene_height: 500, elements: [] };
|
||||
const screenData = {
|
||||
id: 1, scene_width: 500, scene_height: 500, elements: [],
|
||||
};
|
||||
try {
|
||||
forgeUpdateSceneCamera(screenData);
|
||||
assert.equal(worldEl.style.width, '500px');
|
||||
@@ -193,15 +263,23 @@ test('forgeUpdateSceneCamera — sans fond XXL, transform vidé (aucun effet, co
|
||||
|
||||
test('forgeUpdateSceneCamera — centre le monde sur le personnage, borné aux bords du monde', () => {
|
||||
const worldEl = fakeWorldEl();
|
||||
const playerEl = { style: { left: '1900px', top: '1000px', width: '40px', height: '60px' } };
|
||||
const playerEl = {
|
||||
style: {
|
||||
left: '1900px', top: '1000px', width: '40px', height: '60px',
|
||||
},
|
||||
};
|
||||
const previousDoc = global.document;
|
||||
global.document = {
|
||||
querySelector: sel => (sel.indexOf('sceneWorld') !== -1 ? worldEl : playerEl),
|
||||
querySelector: (sel) => (sel.indexOf('sceneWorld') !== -1 ? worldEl : playerEl),
|
||||
};
|
||||
const screenData = {
|
||||
id: 1, scene_width: 500, scene_height: 500,
|
||||
id: 1,
|
||||
scene_width: 500,
|
||||
scene_height: 500,
|
||||
elements: [
|
||||
{ kind: 'fond', x: 0, y: 0, width: 3840, height: 2160 },
|
||||
{
|
||||
kind: 'fond', x: 0, y: 0, width: 3840, height: 2160,
|
||||
},
|
||||
{ id: 42, kind: 'personnage', personnage_role: 'joueur' },
|
||||
],
|
||||
};
|
||||
@@ -217,15 +295,23 @@ test('forgeUpdateSceneCamera — centre le monde sur le personnage, borné aux b
|
||||
|
||||
test('forgeUpdateSceneCamera — ne montre jamais au-delà des bords du monde (bornage)', () => {
|
||||
const worldEl = fakeWorldEl();
|
||||
const playerEl = { style: { left: '3820px', top: '0px', width: '20px', height: '20px' } }; // tout au bord droit
|
||||
const playerEl = {
|
||||
style: {
|
||||
left: '3820px', top: '0px', width: '20px', height: '20px',
|
||||
},
|
||||
}; // tout au bord droit
|
||||
const previousDoc = global.document;
|
||||
global.document = {
|
||||
querySelector: sel => (sel.indexOf('sceneWorld') !== -1 ? worldEl : playerEl),
|
||||
querySelector: (sel) => (sel.indexOf('sceneWorld') !== -1 ? worldEl : playerEl),
|
||||
};
|
||||
const screenData = {
|
||||
id: 1, scene_width: 500, scene_height: 500,
|
||||
id: 1,
|
||||
scene_width: 500,
|
||||
scene_height: 500,
|
||||
elements: [
|
||||
{ kind: 'fond', x: 0, y: 0, width: 3840, height: 2160 },
|
||||
{
|
||||
kind: 'fond', x: 0, y: 0, width: 3840, height: 2160,
|
||||
},
|
||||
{ id: 42, kind: 'personnage', personnage_role: 'joueur' },
|
||||
],
|
||||
};
|
||||
@@ -244,7 +330,7 @@ test('forgeUpdateSceneCamera — ne montre jamais au-delà des bords du monde (b
|
||||
// chargement) — un test document mock dédié, avec plusieurs éléments.
|
||||
function withScene(objectsById, screenData, fn) {
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: sel => { const m = sel.match(/"(\d+)"/); return m ? objectsById[m[1]] : undefined; } };
|
||||
global.document = { querySelector: (sel) => { const m = sel.match(/"(\d+)"/); return m ? objectsById[m[1]] : undefined; } };
|
||||
global.screensData = [{ id: 1, scene_width: 500, scene_height: 500 }];
|
||||
global.window.currentScreenId = 1;
|
||||
try { fn(); } finally { global.document = previousDoc; }
|
||||
@@ -252,7 +338,11 @@ function withScene(objectsById, screenData, fn) {
|
||||
|
||||
test('forgeWouldCollide — un décor solide bloque le personnage qui l\'approche', () => {
|
||||
const player = { id: 20, kind: 'personnage' };
|
||||
const wallEl = { style: { left: '140px', top: '100px', width: '20px', height: '20px' } };
|
||||
const wallEl = {
|
||||
style: {
|
||||
left: '140px', top: '100px', width: '20px', height: '20px',
|
||||
},
|
||||
};
|
||||
const screenData = { elements: [player, { id: 21, kind: 'decor' }] };
|
||||
withScene({ 21: wallEl }, screenData, () => {
|
||||
// Le joueur (20x20) tente d'aller en (130,100) -> chevauche le mur (140..160, 100..120).
|
||||
@@ -264,8 +354,16 @@ test('forgeWouldCollide — un décor solide bloque le personnage qui l\'approch
|
||||
|
||||
test('forgeWouldCollide — ignore un "fond" (jamais un obstacle) et un décor à collision désactivée', () => {
|
||||
const player = { id: 22, kind: 'personnage' };
|
||||
const bgEl = { style: { left: '0px', top: '0px', width: '3840px', height: '2160px' } };
|
||||
const disabledEl = { style: { left: '130px', top: '100px', width: '20px', height: '20px' } };
|
||||
const bgEl = {
|
||||
style: {
|
||||
left: '0px', top: '0px', width: '3840px', height: '2160px',
|
||||
},
|
||||
};
|
||||
const disabledEl = {
|
||||
style: {
|
||||
left: '130px', top: '100px', width: '20px', height: '20px',
|
||||
},
|
||||
};
|
||||
const screenData = {
|
||||
elements: [
|
||||
player,
|
||||
@@ -279,10 +377,21 @@ test('forgeWouldCollide — ignore un "fond" (jamais un obstacle) et un décor
|
||||
});
|
||||
|
||||
test('tick — un décor solide empêche d\'avancer (bloqué à son bord, jamais dans le mur)', () => {
|
||||
const player = { id: 25, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4 } };
|
||||
const player = {
|
||||
id: 25,
|
||||
kind: 'personnage',
|
||||
personnage_role: 'joueur',
|
||||
personnage_commandes: {
|
||||
haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4,
|
||||
},
|
||||
};
|
||||
const wall = { id: 26, kind: 'decor' };
|
||||
const playerEl = fakeSceneEl(); // left/top 100px, 20x20
|
||||
const wallEl = { style: { left: '122px', top: '100px', width: '20px', height: '20px' } };
|
||||
const wallEl = {
|
||||
style: {
|
||||
left: '122px', top: '100px', width: '20px', height: '20px',
|
||||
},
|
||||
};
|
||||
withPersonnageObject(player, { 25: playerEl, 26: wallEl }, () => {
|
||||
global.heldKeys = new Set(['d']); // vers la droite, vers le mur juste à côté
|
||||
forgeRunPersonnageControllerTick({ elements: [player, wall] });
|
||||
@@ -291,14 +400,25 @@ test('tick — un décor solide empêche d\'avancer (bloqué à son bord, jamais
|
||||
});
|
||||
|
||||
test('tick — glisse le long d\'un mur en diagonale (un seul axe bloqué)', () => {
|
||||
const player = { id: 27, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4 } };
|
||||
const player = {
|
||||
id: 27,
|
||||
kind: 'personnage',
|
||||
personnage_role: 'joueur',
|
||||
personnage_commandes: {
|
||||
haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4,
|
||||
},
|
||||
};
|
||||
const wall = { id: 28, kind: 'decor' };
|
||||
const playerEl = fakeSceneEl(); // left/top 100px, 20x20
|
||||
const wallEl = { style: { left: '122px', top: '100px', width: '20px', height: '20px' } }; // bloque seulement à droite
|
||||
const wallEl = {
|
||||
style: {
|
||||
left: '122px', top: '100px', width: '20px', height: '20px',
|
||||
},
|
||||
}; // bloque seulement à droite
|
||||
withPersonnageObject(player, { 27: playerEl, 28: wallEl }, () => {
|
||||
global.heldKeys = new Set(['d', 's']); // droite (bloquée) + bas (libre)
|
||||
forgeRunPersonnageControllerTick({ elements: [player, wall] });
|
||||
assert.equal(playerEl.style.left, '100px'); // bloqué
|
||||
assert.equal(playerEl.style.top, '104px'); // continue de descendre
|
||||
assert.equal(playerEl.style.top, '104px'); // continue de descendre
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,8 +40,11 @@ function fakeMangaWidget() {
|
||||
if (sel === '[data-manga-role="result"]') return resultZone;
|
||||
return null;
|
||||
},
|
||||
classList: { add: c => classes.add(c), remove: c => classes.delete(c), contains: c => classes.has(c) },
|
||||
_header: header, _progress: progress, _resultZone: resultZone, _classes: classes,
|
||||
classList: { add: (c) => classes.add(c), remove: (c) => classes.delete(c), contains: (c) => classes.has(c) },
|
||||
_header: header,
|
||||
_progress: progress,
|
||||
_resultZone: resultZone,
|
||||
_classes: classes,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -123,7 +126,9 @@ test('forgeQuizMangaOnQuizComplete affiche le badge "不合格" (échoué) sous
|
||||
|
||||
test('forgeQuizMangaOnQuizComplete ne plante jamais sans widget/sans le conteneur de résultat', () => {
|
||||
assert.doesNotThrow(() => forgeQuizMangaOnQuizComplete(null, { correctCount: 1, totalCount: 1 }));
|
||||
assert.doesNotThrow(() => forgeQuizMangaOnQuizComplete({ querySelector: () => null }, { correctCount: 1, totalCount: 1 }));
|
||||
assert.doesNotThrow(
|
||||
() => forgeQuizMangaOnQuizComplete({ querySelector: () => null }, { correctCount: 1, totalCount: 1 }),
|
||||
);
|
||||
});
|
||||
|
||||
// "classique" réutilise le même calcul générique (forgeQuizProgressOnShow)
|
||||
@@ -139,7 +144,8 @@ function fakeClassiqueWidget() {
|
||||
if (sel === '[data-classique-role="progress"]') return progress;
|
||||
return null;
|
||||
},
|
||||
_header: header, _progress: progress,
|
||||
_header: header,
|
||||
_progress: progress,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -165,7 +171,13 @@ test('forgeQuizClassiqueOnShow ne plante jamais sans widget/état', () => {
|
||||
|
||||
function fakeClassiqueWidgetForResult() {
|
||||
const contentClasses = new Set();
|
||||
const content = { classList: { add: c => contentClasses.add(c), remove: c => contentClasses.delete(c), contains: c => contentClasses.has(c) } };
|
||||
const content = {
|
||||
classList: {
|
||||
add: (c) => contentClasses.add(c),
|
||||
remove: (c) => contentClasses.delete(c),
|
||||
contains: (c) => contentClasses.has(c),
|
||||
},
|
||||
};
|
||||
const resultZone = { innerHTML: '' };
|
||||
const headerTitle = { textContent: 'Formation Sécurité Incendie' };
|
||||
return {
|
||||
@@ -176,7 +188,10 @@ function fakeClassiqueWidgetForResult() {
|
||||
if (sel === '.classiqueHeaderTitle') return headerTitle;
|
||||
return null;
|
||||
},
|
||||
_content: content, _contentClasses: contentClasses, _resultZone: resultZone, _headerTitle: headerTitle,
|
||||
_content: content,
|
||||
_contentClasses: contentClasses,
|
||||
_resultZone: resultZone,
|
||||
_headerTitle: headerTitle,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -211,7 +226,9 @@ test('forgeQuizClassiqueOnQuizComplete affiche le badge "échoué" sous le seuil
|
||||
|
||||
test('forgeQuizClassiqueOnQuizComplete ne plante jamais sans widget/sans les éléments attendus', () => {
|
||||
assert.doesNotThrow(() => forgeQuizClassiqueOnQuizComplete(null, { correctCount: 1, totalCount: 1 }));
|
||||
assert.doesNotThrow(() => forgeQuizClassiqueOnQuizComplete({ querySelector: () => null }, { correctCount: 1, totalCount: 1 }));
|
||||
assert.doesNotThrow(
|
||||
() => forgeQuizClassiqueOnQuizComplete({ querySelector: () => null }, { correctCount: 1, totalCount: 1 }),
|
||||
);
|
||||
});
|
||||
|
||||
test('forgeQuizClassiqueOnShow retire "is-showing-result" (défensif — un futur modèle "Recommencer" réutiliserait ce garde-fou)', () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ global.window = global.window || {};
|
||||
global.gameData = global.gameData || {};
|
||||
|
||||
const actionsMod = require('../actions.js');
|
||||
|
||||
Object.assign(global, actionsMod);
|
||||
require('../personnage-controller.js');
|
||||
global.forgeApplyVariableActionOffline = global.forgeApplyVariableActionOffline || require('../offline/apply-actions.js').forgeApplyVariableActionOffline;
|
||||
@@ -38,7 +39,9 @@ test('forgeRunScreenTriggers — ignore tout déclencheur autre que "affichage"
|
||||
id: 1,
|
||||
screen_triggers: [{
|
||||
trigger: 'collision',
|
||||
action: { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' },
|
||||
action: {
|
||||
type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1',
|
||||
},
|
||||
}],
|
||||
});
|
||||
assert.equal(gameData.variables.score.value, '0');
|
||||
|
||||
@@ -14,11 +14,17 @@ const { isGameKey } = require('../triggers.js');
|
||||
test('isGameKey — vrai pour une touche écoutée par un déclencheur clavier/touche_maintenue/touche_relachee', () => {
|
||||
global.gameData = {
|
||||
flows: {
|
||||
'1': {
|
||||
1: {
|
||||
nodes: [
|
||||
{ id: 1, node_type: 'trigger', trigger_event: 'clavier', trigger_key: ' ' },
|
||||
{ id: 2, node_type: 'trigger', trigger_event: 'touche_maintenue', trigger_key: 'ArrowRight' },
|
||||
{ id: 3, node_type: 'trigger', trigger_event: 'touche_relachee', trigger_key: 'ArrowLeft' },
|
||||
{
|
||||
id: 1, node_type: 'trigger', trigger_event: 'clavier', trigger_key: ' ',
|
||||
},
|
||||
{
|
||||
id: 2, node_type: 'trigger', trigger_event: 'touche_maintenue', trigger_key: 'ArrowRight',
|
||||
},
|
||||
{
|
||||
id: 3, node_type: 'trigger', trigger_event: 'touche_relachee', trigger_key: 'ArrowLeft',
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
},
|
||||
@@ -31,6 +37,15 @@ test('isGameKey — vrai pour une touche écoutée par un déclencheur clavier/t
|
||||
});
|
||||
|
||||
test('isGameKey — ignore les touches d\'un autre type de déclencheur (ex. "Au clic")', () => {
|
||||
global.gameData = { flows: { '1': { nodes: [{ id: 1, node_type: 'trigger', trigger_event: 'clic', trigger_element_id: 5 }], edges: [] } } };
|
||||
global.gameData = {
|
||||
flows: {
|
||||
1: {
|
||||
nodes: [{
|
||||
id: 1, node_type: 'trigger', trigger_event: 'clic', trigger_element_id: 5,
|
||||
}],
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
assert.equal(isGameKey('Enter'), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user