Lot 4 modernisation JS (SonarLint) + restauration CI sonarqube non-bloquante
Build and deploy / test-python (push) Successful in 11m18s
Build and deploy / test-js (push) Successful in 1m16s
Build and deploy / lint-python (push) Successful in 4m24s
Build and deploy / lint-js (push) Successful in 2m51s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Successful in 5m30s
Build and deploy / test-python (push) Successful in 11m18s
Build and deploy / test-js (push) Successful in 1m16s
Build and deploy / lint-python (push) Successful in 4m24s
Build and deploy / lint-js (push) Successful in 2m51s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Successful in 5m30s
Corrige les findings SonarQube (via SonarLint IDE, fichier par fichier) sur ~24 fichiers static/js/ : parseFloat/parseInt -> Number.*, .replace(/x/g,y) -> .replaceAll, .indexOf() -> .includes()/.startsWith(), getAttribute/setAttribute -> .dataset, tableaux -> Set, x && x.y -> x?.y (verifie site par site), extraction de template litteraux imbriques, ternaires imbriquees, refactors de complexite cognitive (S3776) via tables de dispatch, Object.hasOwn, .at(), et deduplication de fonctions identiques (S4144). Deux exceptions S2486 documentees/corrigees (filter-repeater-rows.js) et un cas S2703 de partage inter-scripts complete (_collisionWizard, trigger-editor.js <-> collision-rules-editor.js). Details complets dans CODE_QUALITY.md section 5. Restaure aussi le job CI "sonarqube" (non-bloquant) dans .gitea/workflows/deploy.yml maintenant que l'instance prod est operationnelle. Suites vertes : 276/276 JS (node --test), 591/591 Python (pytest). SKIP=djlint sur ce commit : hook djlint bloquant sur le backlog H021 (styles inline, 49 occurrences/6 templates) deja documente comme dette assumee non traitee dans CODE_QUALITY.md section 6, aucun rapport avec ce commit (aucun template touche ici) - valide explicitement avec l'utilisateur avant de contourner ce hook. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
66d8eaeae8
commit
ccf836f2c5
@@ -11,13 +11,32 @@ global.window = global.window || {};
|
||||
global.gameData = global.gameData || {};
|
||||
const {
|
||||
resolveSpriteFrames, runSpriteAnimation, stopAllSpriteAnimations, activeSpriteAnimations,
|
||||
applyObjectProperty, clampSceneObjectPosition, runActionNode,
|
||||
applyElementProperty, applyObjectProperty, clampSceneObjectPosition, runActionNode,
|
||||
} = require('../actions.js');
|
||||
|
||||
function fakeImg() {
|
||||
return { src: '' };
|
||||
}
|
||||
|
||||
// Mock DOM minimal pour applyElementProperty() — classList/style/dataset
|
||||
// réels (Set + objet), pas juste des espions, pour pouvoir vérifier l'état
|
||||
// après coup (ex. "la classe a bien été retirée", pas seulement "la
|
||||
// méthode a été appelée").
|
||||
function fakeElement() {
|
||||
const classes = new Set();
|
||||
return {
|
||||
textContent: '',
|
||||
style: { display: '', visibility: '' },
|
||||
dataset: {},
|
||||
classList: {
|
||||
add: (c) => classes.add(c),
|
||||
remove: (c) => classes.delete(c),
|
||||
toggle: (c) => { if (classes.has(c)) classes.delete(c); else classes.add(c); },
|
||||
has: (c) => classes.has(c),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('runSpriteAnimation — un appel répété avec la même animation ne redémarre pas', () => {
|
||||
const el = fakeImg();
|
||||
const spriteData = { frames: ['/a0.png', '/a1.png', '/a2.png'], fps: 8, loop: true };
|
||||
@@ -89,6 +108,113 @@ test('resolveSpriteFrames — résout depuis gameData.personnage_animations par
|
||||
// maintenue, voir triggers.js) pouvait sortir du cadre de la scène, rien ne
|
||||
// l'empêchant. applyObjectProperty()/clampSceneObjectPosition() le bornent
|
||||
// désormais à [0, scene_width/height - sa propre taille].
|
||||
// Couverture de TOUTES les branches d'applyElementProperty() (39 -> lot 4
|
||||
// "modernisation JS", S3776) AVANT tout refactor, pour garantir que
|
||||
// l'extraction en sous-fonctions ne change aucun comportement — voir
|
||||
// CODE_QUALITY.md.
|
||||
test('applyElementProperty — "contenu" : texte littéral, null/undefined -> vidé', () => {
|
||||
const el = fakeElement();
|
||||
applyElementProperty(el, 'contenu', 'Bonjour');
|
||||
assert.equal(el.textContent, 'Bonjour');
|
||||
applyElementProperty(el, 'contenu', null);
|
||||
assert.equal(el.textContent, '');
|
||||
});
|
||||
|
||||
test('applyElementProperty — "surbrillance" : toggle/off/valeur par défaut (activer)', () => {
|
||||
const el = fakeElement();
|
||||
applyElementProperty(el, 'surbrillance', 'toggle');
|
||||
assert.ok(el.classList.has('forgeHighlight'));
|
||||
applyElementProperty(el, 'surbrillance', 'toggle');
|
||||
assert.ok(!el.classList.has('forgeHighlight'));
|
||||
applyElementProperty(el, 'surbrillance', 'on');
|
||||
assert.ok(el.classList.has('forgeHighlight'));
|
||||
applyElementProperty(el, 'surbrillance', 'off');
|
||||
assert.ok(!el.classList.has('forgeHighlight'));
|
||||
});
|
||||
|
||||
test('applyElementProperty — "desactive" : toggle/off/valeur par défaut (désactiver)', () => {
|
||||
const el = fakeElement();
|
||||
applyElementProperty(el, 'desactive', 'toggle');
|
||||
assert.ok(el.classList.has('forgeDisabled'));
|
||||
applyElementProperty(el, 'desactive', 'toggle');
|
||||
assert.ok(!el.classList.has('forgeDisabled'));
|
||||
applyElementProperty(el, 'desactive', 'on');
|
||||
assert.ok(el.classList.has('forgeDisabled'));
|
||||
applyElementProperty(el, 'desactive', 'off');
|
||||
assert.ok(!el.classList.has('forgeDisabled'));
|
||||
});
|
||||
|
||||
test('applyElementProperty — "visibilite" : toggle (selon état actuel), masque, invisible, valeur par défaut (visible)', () => {
|
||||
const el = fakeElement();
|
||||
applyElementProperty(el, 'visibilite', 'toggle');
|
||||
assert.equal(el.style.display, 'none');
|
||||
applyElementProperty(el, 'visibilite', 'toggle');
|
||||
assert.equal(el.style.display, '');
|
||||
applyElementProperty(el, 'visibilite', 'masque');
|
||||
assert.equal(el.style.display, 'none');
|
||||
applyElementProperty(el, 'visibilite', 'invisible');
|
||||
assert.equal(el.style.display, '');
|
||||
assert.equal(el.style.visibility, 'hidden');
|
||||
applyElementProperty(el, 'visibilite', 'visible');
|
||||
assert.equal(el.style.display, '');
|
||||
assert.equal(el.style.visibility, '');
|
||||
});
|
||||
|
||||
test('applyElementProperty — "orientation" : toggle/gauche/valeur par défaut (droite)', () => {
|
||||
const el = fakeElement();
|
||||
applyElementProperty(el, 'orientation', 'toggle');
|
||||
assert.ok(el.classList.has('forgeFlipped'));
|
||||
applyElementProperty(el, 'orientation', 'toggle');
|
||||
assert.ok(!el.classList.has('forgeFlipped'));
|
||||
applyElementProperty(el, 'orientation', 'gauche');
|
||||
assert.ok(el.classList.has('forgeFlipped'));
|
||||
applyElementProperty(el, 'orientation', 'droite');
|
||||
assert.ok(!el.classList.has('forgeFlipped'));
|
||||
});
|
||||
|
||||
test('applyElementProperty — "pos_x_relatif"/"pos_y_relatif" : ajoute au pixel/pourcentage actuel, ignore une valeur non numérique', () => {
|
||||
const el = fakeElement();
|
||||
el.style.left = '10%';
|
||||
applyElementProperty(el, 'pos_x_relatif', '5');
|
||||
assert.equal(el.style.left, '15%');
|
||||
el.style.top = '20%';
|
||||
applyElementProperty(el, 'pos_y_relatif', '-5');
|
||||
assert.equal(el.style.top, '15%');
|
||||
applyElementProperty(el, 'pos_x_relatif', 'pas-un-nombre');
|
||||
assert.equal(el.style.left, '15%', 'valeur non numérique : aucun changement');
|
||||
});
|
||||
|
||||
test('applyElementProperty — propriété inconnue ou valeur null/undefined : aucun effet', () => {
|
||||
const el = fakeElement();
|
||||
applyElementProperty(el, 'propriete_qui_n_existe_pas', 'x');
|
||||
applyElementProperty(el, 'width', undefined);
|
||||
applyElementProperty(el, 'width', null);
|
||||
assert.equal(el.style.width, undefined, 'jamais touché');
|
||||
});
|
||||
|
||||
test('applyElementProperty — valeur "a||b" (va-et-vient) : bascule à chaque appel, persiste via dataset', () => {
|
||||
// Premier appel (dataset vide, "pas encore à la 2e valeur") -> bascule
|
||||
// sur la SECONDE valeur (parts[1]) et mémorise l'état "à la 2e" (voir
|
||||
// le code : atSecond=false -> next=parts[1], dataset mis à '1').
|
||||
const el = fakeElement();
|
||||
applyElementProperty(el, 'width', '10||90');
|
||||
assert.equal(el.style.width, '90%');
|
||||
assert.equal(el.dataset.togglewidth, '1');
|
||||
applyElementProperty(el, 'width', '10||90');
|
||||
assert.equal(el.style.width, '10%');
|
||||
assert.equal(el.dataset.togglewidth, '0');
|
||||
applyElementProperty(el, 'width', '10||90');
|
||||
assert.equal(el.style.width, '90%');
|
||||
});
|
||||
|
||||
test('applyElementProperty — valeur directe (pas de "||") : assignation simple avec suffixe', () => {
|
||||
const el = fakeElement();
|
||||
applyElementProperty(el, 'width', '42');
|
||||
assert.equal(el.style.width, '42%');
|
||||
applyElementProperty(el, 'bg_color', '#ff0000');
|
||||
assert.equal(el.style.backgroundColor, '#ff0000');
|
||||
});
|
||||
|
||||
function withScreensData(screenData, fn) {
|
||||
const previousScreens = global.screensData;
|
||||
const previousId = global.window.currentScreenId;
|
||||
@@ -97,6 +223,276 @@ function withScreensData(screenData, fn) {
|
||||
try { fn(); } finally { global.screensData = previousScreens; global.window.currentScreenId = previousId; }
|
||||
}
|
||||
|
||||
// Couverture de TOUTES les branches de runActionNode() (58 -> lot 4
|
||||
// "modernisation JS", S3776) AVANT tout refactor — un seul appel par
|
||||
// action_type, avec des dépendances globales (showScreen, fetch,
|
||||
// FORGE_PLAY_URLS, les fonctions forgeApply*Offline...) remplacées par
|
||||
// des espions qui enregistrent l'appel plutôt que par leur vraie
|
||||
// implémentation (déjà testée ailleurs : screens.js, apply-actions.js).
|
||||
// Objectif : garantir que l'extraction en sous-fonctions n'envoie pas le
|
||||
// mauvais argument ou n'appelle pas la mauvaise fonction — pas retester
|
||||
// le comportement de ces dépendances elles-mêmes.
|
||||
const ACTION_GLOBAL_KEYS = [
|
||||
'showScreen', 'screenIndexById', 'applyOpenRowBindings', 'bindClicks', 'bindHoverTexts', 'bindHoverTriggers',
|
||||
'refreshRuntimeData', 'forgeApplyDataActionOffline', 'forgeApplyVariableActionOffline',
|
||||
'forgeApplyScoreActionOffline', 'forgeApplyStatusActionOffline', 'forgeApplyAddRowActionOffline',
|
||||
'FORGE_PLAY_URLS', 'fetch', 'Audio',
|
||||
];
|
||||
|
||||
// `fn` peut être async (branches qui font un vrai fetch) — restaurer les
|
||||
// globales seulement APRÈS la résolution de son résultat (via .finally()
|
||||
// sur la promesse renvoyée, pas un await bloquant ici) est obligatoire :
|
||||
// un simple `try/finally` synchrone les restaurait pendant que la chaîne
|
||||
// .then() de runActionNode tournait encore (bug rencontré en écrivant ce
|
||||
// test : "refreshRuntimeData is not a function" alors que le stub était
|
||||
// bien posé — juste déjà retiré par le moment où le .then() s'exécutait).
|
||||
// Reste appelable SANS await pour les cas synchrones (pas de changement
|
||||
// de signature pour la majorité des tests déjà écrits).
|
||||
function withActionNodeGlobals(fn) {
|
||||
const calls = [];
|
||||
const previous = {};
|
||||
ACTION_GLOBAL_KEYS.forEach((key) => { previous[key] = global[key]; });
|
||||
const previousDispatch = global.window.dispatchGameEvent;
|
||||
const previousOffline = global.window.FORGE_OFFLINE;
|
||||
|
||||
global.showScreen = (id) => calls.push(['showScreen', id]);
|
||||
global.screenIndexById = (id) => global.screensData.findIndex((s) => s.id === id);
|
||||
global.applyOpenRowBindings = () => calls.push(['applyOpenRowBindings']);
|
||||
global.bindClicks = () => calls.push(['bindClicks']);
|
||||
global.bindHoverTexts = () => calls.push(['bindHoverTexts']);
|
||||
global.bindHoverTriggers = () => calls.push(['bindHoverTriggers']);
|
||||
global.window.dispatchGameEvent = (id) => calls.push(['dispatchGameEvent', id]);
|
||||
global.refreshRuntimeData = () => { calls.push(['refreshRuntimeData']); return Promise.resolve('refreshed'); };
|
||||
global.forgeApplyDataActionOffline = (gd, node) => calls.push(['forgeApplyDataActionOffline', node.id]);
|
||||
global.forgeApplyVariableActionOffline = (gd, node) => calls.push(['forgeApplyVariableActionOffline', node.id]);
|
||||
global.forgeApplyScoreActionOffline = (gd, node) => calls.push(['forgeApplyScoreActionOffline', node.id]);
|
||||
global.forgeApplyStatusActionOffline = (gd, node) => calls.push(['forgeApplyStatusActionOffline', node.id]);
|
||||
global.forgeApplyAddRowActionOffline = (gd, node) => { calls.push(['forgeApplyAddRowActionOffline', node.id]); return 4242; };
|
||||
global.FORGE_PLAY_URLS = {
|
||||
flowNodeRunData: '/run-data/999999',
|
||||
flowNodeRunVariable: '/run-variable/999999',
|
||||
flowNodeRunScore: '/run-score/999999',
|
||||
flowNodeRunStatus: '/run-status/999999',
|
||||
flowNodeRunAddRow: '/run-add-row/999999',
|
||||
};
|
||||
global.fetch = (url, opts) => {
|
||||
calls.push(['fetch', url, (opts && opts.method) || 'GET']);
|
||||
return Promise.resolve({ json: () => Promise.resolve({ row_id: 4343 }) });
|
||||
};
|
||||
global.Audio = function FakeAudio(src) {
|
||||
calls.push(['Audio', src]);
|
||||
return { play: () => ({ catch: () => {} }) };
|
||||
};
|
||||
|
||||
const restore = () => {
|
||||
ACTION_GLOBAL_KEYS.forEach((key) => { global[key] = previous[key]; });
|
||||
global.window.dispatchGameEvent = previousDispatch;
|
||||
global.window.FORGE_OFFLINE = previousOffline;
|
||||
};
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = fn(calls);
|
||||
} catch (e) {
|
||||
restore();
|
||||
throw e;
|
||||
}
|
||||
if (result && typeof result.then === 'function') {
|
||||
return result.finally(restore);
|
||||
}
|
||||
restore();
|
||||
return result;
|
||||
}
|
||||
|
||||
test('runActionNode — ecran_suivant : avance si un écran suivant existe, ne fait rien en fin de liste', () => {
|
||||
withScreensData({ id: 1 }, () => {
|
||||
global.screensData = [{ id: 1 }, { id: 2 }];
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'ecran_suivant' });
|
||||
assert.deepEqual(calls, [['showScreen', 2]]);
|
||||
});
|
||||
global.window.currentScreenId = 2;
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'ecran_suivant' });
|
||||
assert.deepEqual(calls, [], 'déjà le dernier écran : aucun changement');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('runActionNode — ecran_precedent : recule si un écran précédent existe, ne fait rien en tête de liste', () => {
|
||||
global.screensData = [{ id: 1 }, { id: 2 }];
|
||||
global.window.currentScreenId = 2;
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'ecran_precedent' });
|
||||
assert.deepEqual(calls, [['showScreen', 1]]);
|
||||
});
|
||||
global.window.currentScreenId = 1;
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'ecran_precedent' });
|
||||
assert.deepEqual(calls, [], 'déjà le premier écran : aucun changement');
|
||||
});
|
||||
});
|
||||
|
||||
test('runActionNode — aller_a : montre l\'écran cible si présent, ignore sans target_screen_id', () => {
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'aller_a', target_screen_id: 7 });
|
||||
assert.deepEqual(calls, [['showScreen', 7]]);
|
||||
});
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'aller_a' });
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
});
|
||||
|
||||
test('runActionNode — ouvrir_ligne : mémorise la ligne cliquée, affiche l\'écran, rebranche les écouteurs', () => {
|
||||
global.window.lastClickedRowId = 5;
|
||||
global.window.lastClickedDefinitionId = 9;
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'ouvrir_ligne', target_screen_id: 3 });
|
||||
assert.deepEqual(global.window.currentOpenRow, { definitionId: 9, rowId: 5 });
|
||||
assert.deepEqual(calls, [['showScreen', 3], ['applyOpenRowBindings'], ['bindClicks'], ['bindHoverTexts'], ['bindHoverTriggers']]);
|
||||
});
|
||||
});
|
||||
|
||||
test('runActionNode — modifier_element : applique la propriété sur l\'élément trouvé, ignore si absent', () => {
|
||||
const el = fakeElement();
|
||||
global.document = { querySelector: (sel) => (sel.includes('"5"') ? el : null) };
|
||||
runActionNode({
|
||||
action_type: 'modifier_element', target_element_id: 5, element_property: 'contenu', element_value: 'Salut',
|
||||
});
|
||||
assert.equal(el.textContent, 'Salut');
|
||||
// Aucune erreur levée quand l'élément n'existe pas (id différent -> querySelector renvoie null).
|
||||
runActionNode({
|
||||
action_type: 'modifier_element', target_element_id: 999, element_property: 'contenu', element_value: 'x',
|
||||
});
|
||||
});
|
||||
|
||||
test('runActionNode — modifier_objet_scene : applique la propriété sur l\'objet trouvé (par target_element_id ou target_object_id)', () => {
|
||||
const el = fakeElement();
|
||||
el.style.width = '10%';
|
||||
global.document = { querySelector: () => el };
|
||||
runActionNode({
|
||||
action_type: 'modifier_objet_scene', target_object_id: 8, element_property: 'contenu', element_value: 'Objet',
|
||||
});
|
||||
assert.equal(el.textContent, 'Objet');
|
||||
});
|
||||
|
||||
test('runActionNode — declencher_evenement : notifie l\'événement personnalisé ciblé', () => {
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'declencher_evenement', target_custom_event_id: 12 });
|
||||
assert.deepEqual(calls, [['dispatchGameEvent', 12]]);
|
||||
});
|
||||
});
|
||||
|
||||
test('runActionNode — activer_onglet : montre l\'élément visé, masque ses frères directs, ignore les autres', () => {
|
||||
const elTarget = fakeElement();
|
||||
const elSibling = fakeElement();
|
||||
elSibling.style.display = '';
|
||||
const els = { 1: elTarget, 2: elSibling };
|
||||
global.document = { querySelector: (sel) => els[sel.match(/"(\d+)"/)[1]] };
|
||||
withScreensData({
|
||||
id: 1,
|
||||
elements: [{ id: 1, parent_id: null }, { id: 2, parent_id: null }, { id: 3, parent_id: 99 }],
|
||||
}, () => {
|
||||
runActionNode({ action_type: 'activer_onglet', target_element_id: 1 });
|
||||
});
|
||||
assert.equal(elTarget.style.display, '');
|
||||
assert.equal(elSibling.style.display, 'none');
|
||||
});
|
||||
|
||||
test('runActionNode — attendre : résout après le délai (data_value en secondes), 0 si non numérique', async () => {
|
||||
const t0 = Date.now();
|
||||
await runActionNode({ action_type: 'attendre', data_value: '0.01' });
|
||||
assert.ok(Date.now() - t0 >= 5, 'doit avoir réellement attendu');
|
||||
await runActionNode({ action_type: 'attendre', data_value: 'pas-un-nombre' });
|
||||
});
|
||||
|
||||
test('runActionNode — modifier_donnee : hors ligne délègue à forgeApplyDataActionOffline, en ligne POST vers flowNodeRunData', async () => {
|
||||
global.window.FORGE_OFFLINE = true;
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'modifier_donnee', id: 11 });
|
||||
assert.deepEqual(calls, [['forgeApplyDataActionOffline', 11], ['refreshRuntimeData']]);
|
||||
});
|
||||
global.window.FORGE_OFFLINE = false;
|
||||
await withActionNodeGlobals(async (calls) => {
|
||||
await runActionNode({ action_type: 'modifier_donnee', id: 999999 });
|
||||
assert.equal(calls[0][0], 'fetch');
|
||||
assert.equal(calls[0][1], '/run-data/999999');
|
||||
assert.equal(calls[0][2], 'POST');
|
||||
});
|
||||
});
|
||||
|
||||
test('runActionNode — modifier_variable : hors ligne délègue à forgeApplyVariableActionOffline, en ligne POST vers flowNodeRunVariable', async () => {
|
||||
global.window.FORGE_OFFLINE = true;
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'modifier_variable', id: 22 });
|
||||
assert.deepEqual(calls, [['forgeApplyVariableActionOffline', 22], ['refreshRuntimeData']]);
|
||||
});
|
||||
global.window.FORGE_OFFLINE = false;
|
||||
await withActionNodeGlobals(async (calls) => {
|
||||
await runActionNode({ action_type: 'modifier_variable', id: 999999 });
|
||||
assert.equal(calls[0][1], '/run-variable/999999');
|
||||
});
|
||||
});
|
||||
|
||||
test('runActionNode — modifier_score : hors ligne délègue à forgeApplyScoreActionOffline, en ligne POST vers flowNodeRunScore', async () => {
|
||||
global.window.FORGE_OFFLINE = true;
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'modifier_score', id: 33 });
|
||||
assert.deepEqual(calls, [['forgeApplyScoreActionOffline', 33], ['refreshRuntimeData']]);
|
||||
});
|
||||
global.window.FORGE_OFFLINE = false;
|
||||
await withActionNodeGlobals(async (calls) => {
|
||||
await runActionNode({ action_type: 'modifier_score', id: 999999 });
|
||||
assert.equal(calls[0][1], '/run-score/999999');
|
||||
});
|
||||
});
|
||||
|
||||
test('runActionNode — definir_statut_partie : hors ligne délègue à forgeApplyStatusActionOffline, en ligne POST vers flowNodeRunStatus', async () => {
|
||||
global.window.FORGE_OFFLINE = true;
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'definir_statut_partie', id: 44 });
|
||||
assert.deepEqual(calls, [['forgeApplyStatusActionOffline', 44], ['refreshRuntimeData']]);
|
||||
});
|
||||
global.window.FORGE_OFFLINE = false;
|
||||
await withActionNodeGlobals(async (calls) => {
|
||||
await runActionNode({ action_type: 'definir_statut_partie', id: 999999 });
|
||||
assert.equal(calls[0][1], '/run-status/999999');
|
||||
});
|
||||
});
|
||||
|
||||
test('runActionNode — ajouter_ligne : hors ligne mémorise l\'id renvoyé, en ligne POST puis mémorise json.row_id', async () => {
|
||||
global.window.FORGE_OFFLINE = true;
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'ajouter_ligne', id: 55 });
|
||||
assert.equal(global.window.lastInsertedRowId, 4242);
|
||||
assert.deepEqual(calls, [['forgeApplyAddRowActionOffline', 55], ['refreshRuntimeData']]);
|
||||
});
|
||||
global.window.FORGE_OFFLINE = false;
|
||||
await withActionNodeGlobals(async (calls) => {
|
||||
await runActionNode({ action_type: 'ajouter_ligne', id: 999999 });
|
||||
assert.equal(global.window.lastInsertedRowId, 4343);
|
||||
assert.equal(calls[0][1], '/run-add-row/999999');
|
||||
});
|
||||
});
|
||||
|
||||
test('runActionNode — jouer_son : joue le fichier audio (fire-and-forget), aucun effet sans data_value', () => {
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'jouer_son', data_value: '/bip.mp3' });
|
||||
assert.deepEqual(calls, [['Audio', '/bip.mp3']]);
|
||||
});
|
||||
withActionNodeGlobals((calls) => {
|
||||
runActionNode({ action_type: 'jouer_son' });
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
});
|
||||
|
||||
test('runActionNode — action_type inconnu (ou "rien") : ne fait rien, renvoie une Promise résolue', async () => {
|
||||
const result = runActionNode({ action_type: 'rien' });
|
||||
assert.ok(result instanceof Promise);
|
||||
await result;
|
||||
});
|
||||
|
||||
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' } };
|
||||
|
||||
@@ -41,6 +41,11 @@ function fakeEl(style) {
|
||||
return undefined;
|
||||
},
|
||||
getAttribute(attr) { return attr === 'data-object-id' ? this._objectId : null; },
|
||||
// Getter (pas une valeur figée à la construction) : recalculé à
|
||||
// chaque lecture, comme le vrai `.dataset`, puisque `_objectId` est
|
||||
// posé APRÈS coup par les tests (voir plus bas) — un `dataset: {}`
|
||||
// gelé au moment de fakeEl() serait toujours vide.
|
||||
get dataset() { return { objectId: this._objectId }; },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -35,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.dataset.forObjectId === m[1]) : undefined;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -53,12 +53,24 @@ function fakeEl(parentEl) {
|
||||
};
|
||||
}
|
||||
|
||||
// Un vrai `.dataset` (DOMStringMap) convertit toujours la valeur assignée
|
||||
// en chaîne — reproduit ici via un Proxy, sinon `bubble.dataset.forObjectId
|
||||
// = objectId` avec un objectId numérique (cas réel : obj.id vient de la
|
||||
// base) resterait un number, et ne correspondrait plus jamais à la
|
||||
// capture de regex (toujours une string) dans fakeParentEl.querySelector().
|
||||
function makeDataset() {
|
||||
const store = {};
|
||||
return new Proxy(store, {
|
||||
set(target, key, value) { target[key] = String(value); return true; },
|
||||
});
|
||||
}
|
||||
|
||||
function fakeBubbleEl() {
|
||||
return {
|
||||
className: 'collisionInteractBubble',
|
||||
style: {},
|
||||
textContent: '',
|
||||
setAttribute(name, value) { if (name === 'data-for-object-id') this._forObjectId = String(value); },
|
||||
dataset: makeDataset(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,24 @@ const {
|
||||
forgeQuizResultQuit,
|
||||
} = require('../dialogue-box-controller.js');
|
||||
|
||||
// Un vrai `.dataset` (DOMStringMap) lit/écrit les MÊMES attributs qu'un
|
||||
// `getAttribute('data-foo-bar')`/`setAttribute(...)`, juste sous forme
|
||||
// camelCase (`dataset.fooBar`) — ce Proxy reflète les deux dans le MÊME
|
||||
// objet `attrs` passé en argument, pour que les mocks déjà construits par
|
||||
// clé kebab-case (ex. `{ 'data-timer-mode': 'question' }`, déjà utilisé
|
||||
// par plusieurs tests de ce fichier) continuent de marcher sans y
|
||||
// toucher, tout en exposant `.dataset` comme le vrai DOM (voir
|
||||
// dialogue-box-controller.js après le lot 4 "modernisation JS", S7761).
|
||||
function kebabDataKey(camelProp) {
|
||||
return `data-${camelProp.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}`;
|
||||
}
|
||||
function makeDatasetFromAttrs(attrs) {
|
||||
return new Proxy({}, {
|
||||
get(_, prop) { return attrs[kebabDataKey(prop)]; },
|
||||
set(_, prop, value) { attrs[kebabDataKey(prop)] = String(value); return true; },
|
||||
});
|
||||
}
|
||||
|
||||
function fakeWidget(objectId) {
|
||||
const roles = {
|
||||
header: { textContent: '' },
|
||||
@@ -27,8 +45,10 @@ function fakeWidget(objectId) {
|
||||
footer: { innerHTML: '' },
|
||||
};
|
||||
const classes = new Set();
|
||||
const attrs = { 'data-object-id': String(objectId) };
|
||||
return {
|
||||
getAttribute: (attr) => (attr === 'data-object-id' ? String(objectId) : null),
|
||||
getAttribute: (attr) => (attr in attrs ? attrs[attr] : null),
|
||||
dataset: makeDatasetFromAttrs(attrs),
|
||||
classList: {
|
||||
add: (c) => classes.add(c),
|
||||
remove: (c) => classes.delete(c),
|
||||
@@ -57,7 +77,7 @@ 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 = { dataset: { screenId: '7' } };
|
||||
const attrs = {};
|
||||
const classes = new Set();
|
||||
const bodyChildren = [];
|
||||
@@ -65,7 +85,7 @@ test('forgeEscapeCameraForFullscreen déplace le widget en enfant de <body> et r
|
||||
const widget = {
|
||||
parentNode: null,
|
||||
closest: (sel) => (sel === '.playScreen' ? screenEl : null),
|
||||
setAttribute: (k, v) => { attrs[k] = v; },
|
||||
dataset: makeDatasetFromAttrs(attrs),
|
||||
classList: { add: (c) => classes.add(c) },
|
||||
};
|
||||
const previousDoc = global.document;
|
||||
@@ -74,7 +94,7 @@ test('forgeEscapeCameraForFullscreen déplace le widget en enfant de <body> et r
|
||||
forgeEscapeCameraForFullscreen(widget);
|
||||
assert.equal(bodyChildren[0], widget);
|
||||
assert.equal(widget.parentNode, body);
|
||||
assert.equal(attrs['data-owner-screen-id'], '7');
|
||||
assert.equal(widget.dataset.ownerScreenId, '7');
|
||||
assert.equal(classes.has('quizBoxWidget--detachedForFullscreen'), true);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
@@ -288,6 +308,7 @@ function fakeQuizChoiceBtn() {
|
||||
|
||||
function fakeQuizWidget(objectId, choiceCount, attrs) {
|
||||
attrs = attrs || {};
|
||||
attrs['data-object-id'] = String(objectId);
|
||||
const roles = {
|
||||
header: { textContent: '' },
|
||||
question: { textContent: '' },
|
||||
@@ -312,13 +333,10 @@ 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) => {
|
||||
if (attr === 'data-object-id') return String(objectId);
|
||||
if (attr in attrs) return attrs[attr];
|
||||
return null;
|
||||
},
|
||||
closest: (sel) => (sel === '.playScreen' ? { dataset: { screenId: String(window.currentScreenId) } } : null),
|
||||
getAttribute: (attr) => (attr in attrs ? attrs[attr] : null),
|
||||
setAttribute: (k, v) => { attrs[k] = v; },
|
||||
dataset: makeDatasetFromAttrs(attrs),
|
||||
classList: {
|
||||
// Variadique (comme le vrai DOMTokenList.remove/add) : le moteur
|
||||
// appelle parfois .remove('is-active', 'is-fullscreen') d'un coup
|
||||
@@ -600,7 +618,6 @@ 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);
|
||||
const quizWidget = fakeQuizWidget(99, 2);
|
||||
const previousDoc = global.document;
|
||||
global.document = docWithDialogueAndQuiz(dialogueWidget, quizWidget);
|
||||
@@ -948,7 +965,7 @@ 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),
|
||||
dataset: { objectId: '99' },
|
||||
classList: { add: () => {}, remove: () => {}, toggle: () => {} },
|
||||
querySelector: (sel) => (sel.indexOf('choices') !== -1 ? { innerHTML: '' } : { textContent: '' }),
|
||||
querySelectorAll: () => [],
|
||||
@@ -979,7 +996,7 @@ 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),
|
||||
dataset: { objectId: '99' },
|
||||
classList: { add: () => {}, remove: () => {}, toggle: () => {} },
|
||||
querySelector: (sel) => (sel.indexOf('choices') !== -1 ? { innerHTML: '' } : { textContent: '' }),
|
||||
querySelectorAll: () => [],
|
||||
|
||||
@@ -34,6 +34,7 @@ function fakeMangaWidget() {
|
||||
const classes = new Set();
|
||||
return {
|
||||
getAttribute(attr) { return attr === 'data-object-id' ? '42' : null; },
|
||||
dataset: { objectId: '42' },
|
||||
querySelector(sel) {
|
||||
if (sel === '[data-quiz-role="header"]') return header;
|
||||
if (sel === '[data-manga-role="progress"]') return progress;
|
||||
@@ -139,6 +140,7 @@ function fakeClassiqueWidget() {
|
||||
const progress = { innerHTML: '' };
|
||||
return {
|
||||
getAttribute(attr) { return attr === 'data-object-id' ? '42' : null; },
|
||||
dataset: { objectId: '42' },
|
||||
querySelector(sel) {
|
||||
if (sel === '[data-quiz-role="header"]') return header;
|
||||
if (sel === '[data-classique-role="progress"]') return progress;
|
||||
@@ -182,6 +184,7 @@ function fakeClassiqueWidgetForResult() {
|
||||
const headerTitle = { textContent: 'Formation Sécurité Incendie' };
|
||||
return {
|
||||
getAttribute(attr) { return attr === 'data-object-id' ? '42' : null; },
|
||||
dataset: { objectId: '42' },
|
||||
querySelector(sel) {
|
||||
if (sel === '.classiqueContent') return content;
|
||||
if (sel === '[data-classique-role="result"]') return resultZone;
|
||||
|
||||
Reference in New Issue
Block a user