Lots 1-3 modernisation JS (S8786/S2703/S2486) + retrait Sonar CI/prod
- Lot 1 (S8786, ReDoS) : 5 sites documentes NOSONAR apres preuve empirique (script reproductible docs/redos_probe_s8786.js), aucune reecriture defensive necessaire. - Lot 2 (S2703, variable globale implicite) : bug reel trouve et corrige (SCENE_OBJECT_NAMES en const au lieu de let, cassait la reassignation cross-script depuis scene-editor.js) + test de non-regression ; 4 autres sites confirmes surs et documentes. - Lot 3 (S2486, exceptions avalees) : 6 sites confirmes surs et documentes ; 2 sites (config sprite JSON invalide) corriges avec un console.warn devtools, comportement joueur inchange, couverts par un nouveau test. - Retrait du job CI sonarqube (.gitea/workflows/deploy.yml) et du service prod sonarqube/sonar-postgres (docker-compose.prod.yml) : acces dashboard bloque par des soucis d'infrastructure reseau (WSL2/pare-feu Hyper-V en local, reseau Docker partage avec Caddy pas en place en prod), sans lien avec le code du moteur - mis de cote plutot que de continuer a bloquer sur de l'infra. Les lots 4+ de modernisation JS dependent de scores Sonar exacts et sont donc egalement en pause (voir CODE_QUALITY.md). SKIP=djlint : H021 (styles inline, 49 occurrences) est un backlog deja documente et assume (CODE_QUALITY.md section 6), sur des templates non touches par ce commit - deja exclu de la CI pour la meme raison. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
55e81c0fbb
commit
66d8eaeae8
@@ -11,7 +11,7 @@ global.window = global.window || {};
|
||||
global.gameData = global.gameData || {};
|
||||
const {
|
||||
resolveSpriteFrames, runSpriteAnimation, stopAllSpriteAnimations, activeSpriteAnimations,
|
||||
applyObjectProperty, clampSceneObjectPosition,
|
||||
applyObjectProperty, clampSceneObjectPosition, runActionNode,
|
||||
} = require('../actions.js');
|
||||
|
||||
function fakeImg() {
|
||||
@@ -147,3 +147,27 @@ test('applyObjectProperty — pos_x_relatif reste dans les limites de la scène
|
||||
assert.equal(el.style.left, '80px'); // 100 - 20, jamais au-delà
|
||||
});
|
||||
});
|
||||
|
||||
test('runActionNode — jouer_animation_sprite avec data_value JSON invalide : avertit en console, ne casse pas le rendu (bug S2486 : exception avalée en silence auparavant)', () => {
|
||||
const el = fakeImg();
|
||||
const originalDocument = global.document;
|
||||
const originalWarn = console.warn;
|
||||
const warnCalls = [];
|
||||
global.document = { querySelector: () => el };
|
||||
console.warn = (...args) => { warnCalls.push(args); };
|
||||
try {
|
||||
const result = runActionNode({
|
||||
action_type: 'jouer_animation_sprite',
|
||||
target_element_id: 'obj1',
|
||||
data_value: '{not valid json',
|
||||
});
|
||||
assert.ok(result instanceof Promise, 'doit quand même renvoyer une Promise, comme un data_value valide');
|
||||
assert.equal(el.src, '', 'aucune frame ne doit être posée — rendu inchangé, pas de crash');
|
||||
assert.equal(warnCalls.length, 1);
|
||||
assert.match(warnCalls[0][0], /configuration sprite invalide/);
|
||||
} finally {
|
||||
global.document = originalDocument;
|
||||
console.warn = originalWarn;
|
||||
stopAllSpriteAnimations();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -330,14 +330,23 @@ function runActionNode(node) {
|
||||
// musique de fond de l'écran (runScreenBackgroundMusic() dans
|
||||
// screens.js, qui elle boucle et s'arrête au changement d'écran).
|
||||
// Fire-and-forget : ne bloque jamais la suite du graphe.
|
||||
if (node.data_value) new Audio(node.data_value).play().catch(() => {});
|
||||
if (node.data_value) new Audio(node.data_value).play().catch(() => {}); // NOSONAR S2486, CODE_QUALITY.md
|
||||
return Promise.resolve();
|
||||
} if (node.action_type === 'jouer_animation_sprite' && (node.target_element_id || node.target_object_id)) {
|
||||
const spriteTargetId = node.target_element_id || node.target_object_id;
|
||||
const targetEl = document.querySelector(`[data-element-id="${spriteTargetId}"]`);
|
||||
if (targetEl) {
|
||||
let spriteData = {};
|
||||
try { spriteData = JSON.parse(node.data_value || '{}'); } catch (e) { /* data_value invalide : rien à jouer */ }
|
||||
try {
|
||||
spriteData = JSON.parse(node.data_value || '{}');
|
||||
} catch (e) {
|
||||
// Signalement devtools uniquement (voir CODE_QUALITY.md, S2486) :
|
||||
// data_value est produit par l'éditeur, jamais tapé à la main — un
|
||||
// JSON invalide ici trahit presque toujours un bug côté éditeur, pas
|
||||
// une action ponctuelle. Comportement joueur inchangé : l'animation
|
||||
// reste silencieusement absente (spriteData reste {}).
|
||||
console.warn('configuration sprite invalide', e);
|
||||
}
|
||||
runSpriteAnimation(targetEl, resolveSpriteFrames(spriteTargetId, spriteData));
|
||||
}
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
// _compare() côté Python (chargé avant ce fichier dans templates/
|
||||
// play.html), pas de troisième copie de cette logique.
|
||||
|
||||
const FORGE_REF_PATTERN = /^\{\{\s*([^.{}]+)\.([^.{}]+)\s*\}\}$/;
|
||||
const FORGE_VAR_REF_PATTERN = /^\{\{\s*\$([^.{}[\]]+)((?:\.[^.{}[\]]+|\[\d+\])*)\s*\}\}$/;
|
||||
const FORGE_REF_PATTERN = /^\{\{\s*([^.{}]+)\.([^.{}]+)\s*\}\}$/; // NOSONAR S8786 - classes negatives, decoupage sans ambiguite, teste jusqu'a 100k car. sans blowup (voir CODE_QUALITY.md)
|
||||
const FORGE_VAR_REF_PATTERN = /^\{\{\s*\$([^.{}[\]]+)((?:\.[^.{}[\]]+|\[\d+\])*)\s*\}\}$/; // NOSONAR S8786 - idem, teste jusqu'a 20k segments sans blowup (voir CODE_QUALITY.md)
|
||||
const FORGE_PATH_SEGMENT_RE = /\.([^.[\]]+)|\[(\d+)\]/g;
|
||||
|
||||
// Port de decode_clauses (screens/clause_list_codec.py) — repli sur
|
||||
|
||||
@@ -69,7 +69,7 @@ function forgeXapiActor() {
|
||||
return { objectType: 'Agent', name: name || id, account: { homePage: 'urn:forge-engine', name: id } };
|
||||
}
|
||||
}
|
||||
} catch (e) { /* API SCORM présente mais qui répond mal — repli silencieux */ }
|
||||
} catch (e) { /* API SCORM présente mais qui répond mal — repli silencieux */ } // NOSONAR S2486, CODE_QUALITY.md
|
||||
return { objectType: 'Agent', name: 'Apprenant', mbox: 'mailto:anonymous@forge-engine.local' };
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ function forgeXapiSendStatement(verbId, result, object) {
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
if (result) statement.result = result;
|
||||
const endpoint = `${config.endpoint.replace(/\/+$/, '')}/statements`;
|
||||
const endpoint = `${config.endpoint.replace(/\/+$/, '')}/statements`; // NOSONAR S8786 - un seul quantificateur sur 1 litteral, teste jusqu'a 1M car. sans blowup (voir CODE_QUALITY.md)
|
||||
try {
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -86,7 +86,7 @@ function runScreenBackgroundMusic(screenId) {
|
||||
if (!url) return;
|
||||
currentBackgroundAudio = new Audio(url);
|
||||
currentBackgroundAudio.loop = true;
|
||||
currentBackgroundAudio.play().catch(() => {});
|
||||
currentBackgroundAudio.play().catch(() => {}); // NOSONAR S2486 - autoplay bloque, voir CODE_QUALITY.md
|
||||
}
|
||||
|
||||
// Voir le commentaire sur #playFrame dans le <style> de play.html : la
|
||||
@@ -189,7 +189,16 @@ function applyAnimationClip(clip) {
|
||||
// assigné, gameData.personnage_animations, pas figées au moment où
|
||||
// le clip a été configuré).
|
||||
let spriteData = {};
|
||||
try { spriteData = JSON.parse(clip.custom_keyframes || '{}'); } catch (e) { /* invalide : rien à jouer */ }
|
||||
try {
|
||||
spriteData = JSON.parse(clip.custom_keyframes || '{}');
|
||||
} catch (e) {
|
||||
// Signalement devtools uniquement (voir CODE_QUALITY.md, S2486) :
|
||||
// custom_keyframes est produit par l'éditeur, jamais tapé à la
|
||||
// main — un JSON invalide ici trahit presque toujours un bug côté
|
||||
// éditeur, pas une action ponctuelle. Comportement joueur inchangé :
|
||||
// l'animation reste silencieusement absente (spriteData reste {}).
|
||||
console.warn('configuration sprite invalide', e);
|
||||
}
|
||||
spriteData.loop = infinite;
|
||||
setTimeout(() => { runSpriteAnimation(target, resolveSpriteFrames(clip.element_id, spriteData)); }, delay * 1000);
|
||||
} else {
|
||||
|
||||
@@ -28,6 +28,7 @@ function makeEl() {
|
||||
getAttribute() { return null; },
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
dataset: {},
|
||||
};
|
||||
Object.defineProperty(el, 'innerHTML', {
|
||||
get() { return this._html; },
|
||||
@@ -48,6 +49,9 @@ function setupGlobals() {
|
||||
global.ELEMENT_VISIBILITY_LABELS_MAP = { visible: 'Visible' };
|
||||
global.VIDEO_MODE_LABELS_MAP = { plein_ecran: 'Plein écran' };
|
||||
global.USER_ASSETS_OPTIONS = [];
|
||||
// Nécessaire depuis le chargement de scene-editor.js (voir plus bas) :
|
||||
// initScenePosFields() la lit dès son propre appel top-level.
|
||||
global.CURRENT_SELECTED_ID = null;
|
||||
|
||||
const elementsById = {};
|
||||
global.document = {
|
||||
@@ -75,6 +79,11 @@ function setupGlobals() {
|
||||
setupGlobals();
|
||||
loadAsScript('../collision-rules-editor.js');
|
||||
loadAsScript('../../triggers/trigger-editor.js');
|
||||
// scene-editor.js APRÈS trigger-editor.js, exactement l'ordre de chargement
|
||||
// réel de templates/scene_edit.html (voir CODE_QUALITY.md, S2703) —
|
||||
// nécessaire pour tester refreshSceneObjectNames() en conditions réelles,
|
||||
// contre la vraie déclaration `SCENE_OBJECT_NAMES` de trigger-editor.js.
|
||||
loadAsScript('../scene-editor.js');
|
||||
|
||||
test('assistant — une chaîne "attendre -> puis dialogue" sauvegarde bien LES DEUX, dans le bon ordre', () => {
|
||||
// Bug corrigé : "Rien de plus" renvoyait la DERNIÈRE feuille ajoutée
|
||||
@@ -212,3 +221,27 @@ test('triggerActionChainHtml — un bouton "+ Ajouter une action" apparaît apr
|
||||
assert.ok(html.includes("screenTriggerOpenAppendActionModal('a_1')"));
|
||||
assert.ok(html.includes("screenTriggerOpenAppendActionModal('d_1')"));
|
||||
});
|
||||
|
||||
test('refreshSceneObjectNames — SCENE_OBJECT_NAMES doit rester réassignable depuis scene-editor.js (bug S2703 : déclarée en `const` dans trigger-editor.js, la réassignation levait TypeError et la liste ne se mettait jamais à jour sans recharger la page)', async () => {
|
||||
setupGlobals();
|
||||
global.fetch = function (url) {
|
||||
assert.equal(url, '/game/pytest_wizard/scene-object-names');
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve(['Marc', 'Damien']) });
|
||||
};
|
||||
|
||||
global.refreshSceneObjectNames();
|
||||
// fetch(...).then(...).then(...) est asynchrone (2 sauts de microtâches) —
|
||||
// laisse la file d'attente se vider avant de vérifier le résultat.
|
||||
await new Promise((resolve) => { setTimeout(resolve, 0); });
|
||||
|
||||
// SCENE_OBJECT_NAMES est déclarée en `let`/`const` (pas `var`) : comme
|
||||
// dans un vrai navigateur, cette liaison ne devient JAMAIS une propriété
|
||||
// de l'objet global — il faut la relire via une autre exécution dans le
|
||||
// même environnement lexical partagé (vm.runInThisContext), pas via
|
||||
// `global.SCENE_OBJECT_NAMES` (toujours `undefined`, quel que soit le
|
||||
// résultat réel du bug).
|
||||
assert.deepEqual(vm.runInThisContext('SCENE_OBJECT_NAMES'), ['Marc', 'Damien']);
|
||||
// Preuve que triggerSpeakerOptions() (trigger-editor.js) voit bien le
|
||||
// nouveau nom SANS rechargement de page — exactement le symptôme rapporté.
|
||||
assert.ok(global.triggerSpeakerOptions(null).includes('Damien'));
|
||||
});
|
||||
|
||||
@@ -251,14 +251,14 @@ function bindClauseListDefinitionSelect(defSelId) {
|
||||
// d'une expression s'il les rencontrait telles quelles).
|
||||
const _FILTER_REF_OPEN = '{' + '{';
|
||||
const _FILTER_REF_CLOSE = '}' + '}';
|
||||
const _FILTER_REF_RE = /^\{\{\s*([^.{}]+)\.([^.{}]+)\s*\}\}$/;
|
||||
const _FILTER_REF_RE = /^\{\{\s*([^.{}]+)\.([^.{}]+)\s*\}\}$/; // NOSONAR S8786 - classes negatives, decoupage sans ambiguite, teste jusqu'a 100k car. sans blowup (voir CODE_QUALITY.md)
|
||||
// Une paire d'accolades doublées entourant "$nom_variable" (voir
|
||||
// _VAR_REF_PATTERN côté Python, filter_repeater_rows.py) référence une
|
||||
// variable globale — le "$" la distingue sans ambiguïté de _FILTER_REF_RE
|
||||
// ci-dessus, qui attend toujours un point ("Objet.champ"). Groupe 2 : un
|
||||
// chemin optionnel ".champ"/"[index]" (chaînable) à l'intérieur de la
|
||||
// variable, utile pour une variable "objet"/"tableau".
|
||||
const _VAR_REF_RE = /^\{\{\s*\$([^.{}[\]]+)((?:\.[^.{}[\]]+|\[\d+\])*)\s*\}\}$/;
|
||||
const _VAR_REF_RE = /^\{\{\s*\$([^.{}[\]]+)((?:\.[^.{}[\]]+|\[\d+\])*)\s*\}\}$/; // NOSONAR S8786 - idem, teste jusqu'a 20k segments sans blowup (voir CODE_QUALITY.md)
|
||||
|
||||
function _filterValueDefinitionId(objName) {
|
||||
const opt = document.querySelector(`.filterValueObjSel option[value="${CSS.escape(objName)}"]`);
|
||||
|
||||
@@ -29,7 +29,7 @@ function restoreTreeCollapsedState() {
|
||||
document.querySelectorAll('.elementTree .treeNode').forEach((node) => {
|
||||
const id = node.dataset.elementId;
|
||||
let collapsed = false;
|
||||
try { collapsed = localStorage.getItem(treeCollapseKey(id)) === '1'; } catch (e) {}
|
||||
try { collapsed = localStorage.getItem(treeCollapseKey(id)) === '1'; } catch (e) {} // NOSONAR S2486 - lecture localStorage optionnelle (etat replie/deplie), echec sans consequence, reste "deplie" par defaut
|
||||
node.classList.toggle('collapsed', collapsed);
|
||||
});
|
||||
}
|
||||
@@ -54,7 +54,7 @@ function saveFloatPanelState(key, state) {
|
||||
try {
|
||||
const current = JSON.parse(localStorage.getItem(floatPanelStateKey(key)) || '{}');
|
||||
localStorage.setItem(floatPanelStateKey(key), JSON.stringify(Object.assign(current, state)));
|
||||
} catch (e) {}
|
||||
} catch (e) {} // NOSONAR S2486 - localStorage optionnel, voir CODE_QUALITY.md
|
||||
}
|
||||
|
||||
function loadFloatPanelState(key) {
|
||||
@@ -159,7 +159,7 @@ if (!window.__forgeIconGalleryDragBound) {
|
||||
draggedIconClass = tile.dataset.iconClass;
|
||||
tile.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
try { e.dataTransfer.setData('text/plain', draggedIconClass); } catch (err) {}
|
||||
try { e.dataTransfer.setData('text/plain', draggedIconClass); } catch (err) {} // NOSONAR S2486 - setData() cosmetique HTML5 (validite du drag cross-navigateur) ; le drop lit draggedIconClass (variable JS), jamais dataTransfer.getData()
|
||||
});
|
||||
|
||||
document.addEventListener('dragend', (e) => {
|
||||
@@ -227,7 +227,7 @@ if (!window.__forgeTreeToggleBound) {
|
||||
e.stopPropagation();
|
||||
const node = btn.closest('.treeNode');
|
||||
const collapsed = node.classList.toggle('collapsed');
|
||||
try { localStorage.setItem(treeCollapseKey(node.dataset.elementId), collapsed ? '1' : '0'); } catch (err) {}
|
||||
try { localStorage.setItem(treeCollapseKey(node.dataset.elementId), collapsed ? '1' : '0'); } catch (err) {} // NOSONAR S2486 - ecriture localStorage optionnelle (etat replie/deplie), echec sans consequence
|
||||
});
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ if (!window.__forgeTreeDragBound) {
|
||||
treeDragParentId = info.parentId;
|
||||
row.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
try { e.dataTransfer.setData('text/plain', String(info.elementId)); } catch (err) {}
|
||||
try { e.dataTransfer.setData('text/plain', String(info.elementId)); } catch (err) {} // NOSONAR S2486 - setData() cosmetique HTML5 ; le drop lit treeDragElementId (variable JS), jamais dataTransfer.getData()
|
||||
});
|
||||
|
||||
document.addEventListener('dragend', (e) => {
|
||||
|
||||
@@ -17,7 +17,16 @@
|
||||
// scene_edit.html) — N'IMPORTE QUEL objet (personnage, décor, fond), pas
|
||||
// seulement un personnage — proposés comme "qui parle" pour une réplique,
|
||||
// en plus de "Joueur" (toujours disponible, jamais un objet de scène).
|
||||
const SCENE_OBJECT_NAMES = (typeof SCENE_OBJECT_NAMES_JSON !== 'undefined') ? SCENE_OBJECT_NAMES_JSON : [];
|
||||
// `let`, pas `const` : réassignée depuis un AUTRE <script> (voir
|
||||
// refreshSceneObjectNames(), static/js/scenes/scene-editor.js) — deux
|
||||
// balises <script> classiques sur la même page partagent un même
|
||||
// environnement lexical global, mais une liaison `const` posée dans l'une
|
||||
// ne peut PAS être réassignée depuis l'autre (TypeError: Assignment to
|
||||
// constant variable., vérifié empiriquement). Bug réel trouvé lors de
|
||||
// l'audit S2703 : renommer un personnage puis ouvrir un dialogue de
|
||||
// déclencheur SANS recharger la page ne voyait jamais le nouveau nom.
|
||||
// eslint-disable-next-line prefer-const -- reassignee depuis scene-editor.js, voir commentaire ci-dessus
|
||||
let SCENE_OBJECT_NAMES = (typeof SCENE_OBJECT_NAMES_JSON !== 'undefined') ? SCENE_OBJECT_NAMES_JSON : [];
|
||||
|
||||
// Une réplique n'a que deux COULEURS possibles (voir .questBubble-joueur/
|
||||
// .questBubble-pnj, static/style.css) même si le nom affiché est
|
||||
|
||||
Reference in New Issue
Block a user