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
@@ -56,8 +56,8 @@ window.forgeCompletedDialogueIds = window.forgeCompletedDialogueIds || {};
|
||||
|
||||
function forgeEscapeHtml(str) {
|
||||
return String(str == null ? '' : str)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
|
||||
.replaceAll('"', '"');
|
||||
}
|
||||
|
||||
// ---------- Boîte de dialogue ----------
|
||||
@@ -141,7 +141,7 @@ function forgeQuizBoxWidgetById(objectId) {
|
||||
function forgeEscapeCameraForFullscreen(widget) {
|
||||
if (widget.parentNode === document.body) return;
|
||||
const screenEl = widget.closest('.playScreen');
|
||||
if (screenEl) widget.setAttribute('data-owner-screen-id', screenEl.getAttribute('data-screen-id'));
|
||||
if (screenEl) widget.dataset.ownerScreenId = screenEl.dataset.screenId;
|
||||
widget.classList.add('quizBoxWidget--detachedForFullscreen');
|
||||
document.body.appendChild(widget);
|
||||
}
|
||||
@@ -155,7 +155,7 @@ function forgeHideQuizBoxOnCurrentScreen() {
|
||||
const widget = forgeQuizBoxWidgetOnCurrentScreen();
|
||||
if (!widget) return;
|
||||
widget.classList.remove('is-active', 'is-fullscreen');
|
||||
const objectId = widget.getAttribute('data-object-id');
|
||||
const objectId = widget.dataset.objectId;
|
||||
forgeStopQuizTimer(objectId);
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ const forgeQuizTimerState = {};
|
||||
|
||||
function forgeStopQuizTimer(objectId) {
|
||||
const t = forgeQuizTimerState[objectId];
|
||||
if (t && t.intervalId) clearInterval(t.intervalId);
|
||||
if (t?.intervalId) clearInterval(t.intervalId);
|
||||
delete forgeQuizTimerState[objectId];
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ function forgeStartQuizTimer(objectId, widget, seconds, onExpire) {
|
||||
|
||||
function forgeUpdateAllScoreWidgets() {
|
||||
const values = document.querySelectorAll('.scoreWidget [data-score-role="value"]');
|
||||
for (let i = 0; i < values.length; i++) values[i].textContent = window.forgeQuizScore;
|
||||
for (const value of values) value.textContent = window.forgeQuizScore;
|
||||
forgeUpdateQuizBoxScoreDisplays();
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ function forgeUpdateAllScoreWidgets() {
|
||||
// non-plein-écran, mais tenu à jour dans tous les cas (coûte rien).
|
||||
function forgeUpdateQuizBoxScoreDisplays() {
|
||||
const values = document.querySelectorAll('.quizBoxWidget [data-quiz-role="score"]');
|
||||
for (let i = 0; i < values.length; i++) values[i].textContent = `Score : ${window.forgeQuizScore}`;
|
||||
for (const value of values) value.textContent = `Score : ${window.forgeQuizScore}`;
|
||||
}
|
||||
|
||||
// Alimente aussi gameData.scoring (voir static/js/play/offline/
|
||||
@@ -341,7 +341,7 @@ function forgeShowDialogueBox(dialogueId, objectName, lines, markCompleted, then
|
||||
// forgeShowCurrentConversationLine), même logique symétrique qu'une
|
||||
// question sans boîte à quiz posée (voir forgeShowQuizBox).
|
||||
const widget = forgeDialogueBoxWidgetOnCurrentScreen();
|
||||
const stateKey = widget ? widget.getAttribute('data-object-id') : (`_sansBoiteDialogue_${dialogueId}`);
|
||||
const stateKey = widget ? widget.dataset.objectId : (`_sansBoiteDialogue_${dialogueId}`);
|
||||
forgeSyncDialogueStartedToScorm();
|
||||
if (!lines.length) {
|
||||
// Rien à jouer : applique quand même markCompleted PUIS thenAction
|
||||
@@ -400,7 +400,7 @@ function forgeAdvanceConversation(dialogueObjectId) {
|
||||
// voir forgeShowCurrentConversationLine, mais gardé strict par sécurité).
|
||||
function forgeDialogueBoxAdvance(objectId) {
|
||||
const state = forgeDialogueBoxState[objectId];
|
||||
if (!state || (state.lines[state.index] && state.lines[state.index].type === 'question')) return;
|
||||
if (!state || state.lines[state.index]?.type === 'question') return;
|
||||
forgeAdvanceConversation(objectId);
|
||||
}
|
||||
|
||||
@@ -413,7 +413,7 @@ function forgeDialogueBoxAdvance(objectId) {
|
||||
function forgeShowQuizBox(dialogueObjectId, dialogueId, objectName, line, questionIndex) {
|
||||
const widget = forgeQuizBoxWidgetOnCurrentScreen();
|
||||
if (!widget) { forgeAdvanceConversation(dialogueObjectId); return; }
|
||||
const quizObjectId = widget.getAttribute('data-object-id');
|
||||
const quizObjectId = widget.dataset.objectId;
|
||||
const previousState = forgeQuizBoxState[quizObjectId];
|
||||
forgeQuizBoxState[quizObjectId] = {
|
||||
dialogueObjectId,
|
||||
@@ -456,14 +456,14 @@ function forgeShowQuizBox(dialogueObjectId, dialogueId, objectName, line, questi
|
||||
// static/style.css). Échappe d'abord au conteneur de scène transformé
|
||||
// (voir forgeEscapeCameraForFullscreen) — sinon position:fixed reste
|
||||
// borné à sa taille "caméra" au lieu du vrai écran du joueur.
|
||||
const isFullscreen = widget.getAttribute('data-fullscreen') === '1';
|
||||
const isFullscreen = widget.dataset.fullscreen === '1';
|
||||
if (isFullscreen) forgeEscapeCameraForFullscreen(widget);
|
||||
widget.classList.toggle('is-fullscreen', isFullscreen);
|
||||
forgeUpdateQuizBoxScoreDisplays();
|
||||
// Minuteur (optionnel — "aucun" reste le réglage par défaut, jamais
|
||||
// imposé, voir screens/rendering/quiz_box_config.py).
|
||||
const timerMode = widget.getAttribute('data-timer-mode');
|
||||
const timerSeconds = parseInt(widget.getAttribute('data-timer-seconds'), 10) || 0;
|
||||
const timerMode = widget.dataset.timerMode;
|
||||
const timerSeconds = Number.parseInt(widget.dataset.timerSeconds, 10) || 0;
|
||||
if (timerMode === 'question') {
|
||||
forgeStartQuizTimer(quizObjectId, widget, timerSeconds, () => { forgeQuizBoxTimeout(quizObjectId, false); });
|
||||
} else if (timerMode === 'quiz' && !forgeQuizBoxState[quizObjectId].quizTimerStarted) {
|
||||
@@ -477,7 +477,17 @@ function forgeShowQuizBox(dialogueObjectId, dialogueId, objectName, line, questi
|
||||
// ce retrait/reflow/réajout, une classe déjà posée ne redéclenche pas
|
||||
// son animation CSS.
|
||||
widget.classList.remove('is-active');
|
||||
void widget.offsetWidth;
|
||||
// Lecture volontairement sans effet apparent : force le navigateur à
|
||||
// calculer la mise en page AVANT le .add() ci-dessous (sans ça, les deux
|
||||
// changements de classe sont fusionnés en une seule passe de rendu et
|
||||
// l'animation ne redémarre jamais). `void` est la forme la plus claire
|
||||
// pour marquer une expression délibérément ignorée (accepté par ESLint,
|
||||
// no-unused-expressions désactivé dans ce projet) — Sonar n'a aucun
|
||||
// idiome qu'il reconnaisse pour ce cas : S3735 (void), S905 (expression
|
||||
// seule sans void) et S1481 (variable jamais lue) se sont succédé sur
|
||||
// les 3 formes essayées, chacune corrigeant la précédente en en créant
|
||||
// une autre. NOSONAR ici plutôt que de continuer, voir CODE_QUALITY.md.
|
||||
void widget.offsetWidth; // NOSONAR S3735 - reflow force, aucune forme alternative ne satisfait Sonar, CODE_QUALITY.md
|
||||
widget.classList.add('is-active');
|
||||
// Effet d'entrée propre au modèle (voir static/js/play/
|
||||
// quiz-box-templates.js) — optionnel, "defaut" n'en a aucun.
|
||||
@@ -489,42 +499,27 @@ function forgeShowQuizBox(dialogueObjectId, dialogueId, objectName, line, questi
|
||||
// templates.js, chargé AVANT ce fichier) — absent en environnement de
|
||||
// test qui ne charge pas ce script, d'où la garde `typeof`.
|
||||
function forgeQuizTemplateEffectsFor(widget) {
|
||||
const template = (widget && widget.getAttribute('data-quiz-template')) || 'defaut';
|
||||
const template = widget?.dataset.quizTemplate || 'defaut';
|
||||
return (typeof forgeQuizTemplateEffects !== 'undefined' && forgeQuizTemplateEffects[template]) || {};
|
||||
}
|
||||
|
||||
// Appelée par le clic sur un choix (les boutons sont injectés EN JEU par
|
||||
// forgeShowQuizBox ci-dessus, jamais rendus côté serveur — voir
|
||||
// render_scene_object.py pour le seul aperçu statique de l'éditeur).
|
||||
// TOUJOURS crédite le score si la réponse est bonne PUIS avance à la
|
||||
// ligne suivante, QUE la réponse soit bonne ou mauvaise (demande
|
||||
// explicite : une mauvaise réponse ne doit jamais bloquer la
|
||||
// progression) — surligne la bonne réponse en vert (et le choix cliqué
|
||||
// en rouge s'il était faux) le temps de la voir avant d'avancer. Un
|
||||
// second clic pendant cet affichage est ignoré (`resolved`).
|
||||
function forgeQuizBoxAnswer(quizObjectId, choiceIndex) {
|
||||
const state = forgeQuizBoxState[quizObjectId];
|
||||
if (!state || state.resolved) return;
|
||||
state.resolved = true;
|
||||
const widget = forgeQuizBoxWidgetById(quizObjectId);
|
||||
const correct = choiceIndex === state.correctIndex;
|
||||
// Minuteur "par question" : cette question est résolue (bonne réponse,
|
||||
// mauvaise réponse, OU minuteur expiré via forgeQuizBoxTimeout, qui
|
||||
// appelle CETTE fonction avec choiceIndex=-1) — jamais le minuteur
|
||||
// "pour tout le quiz", qui doit continuer à tourner sans interruption
|
||||
// d'une question à l'autre.
|
||||
if (widget && widget.getAttribute('data-timer-mode') === 'question') forgeStopQuizTimer(quizObjectId);
|
||||
if (widget) {
|
||||
const buttons = widget.querySelectorAll('.quizBoxChoiceBtn');
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
buttons[i].disabled = true;
|
||||
if (i === state.correctIndex) buttons[i].classList.add('is-correct');
|
||||
else if (i === choiceIndex) buttons[i].classList.add('is-wrong');
|
||||
}
|
||||
// Surligne la bonne réponse en vert (et le choix cliqué en rouge s'il
|
||||
// était faux) sur les boutons injectés par forgeShowQuizBox — extrait de
|
||||
// forgeQuizBoxAnswer (lot 4 "modernisation JS", S3776).
|
||||
function markQuizChoiceButtons(widget, state, choiceIndex) {
|
||||
if (!widget) return;
|
||||
const buttons = widget.querySelectorAll('.quizBoxChoiceBtn');
|
||||
for (const [i, button] of buttons.entries()) {
|
||||
button.disabled = true;
|
||||
if (i === state.correctIndex) button.classList.add('is-correct');
|
||||
else if (i === choiceIndex) button.classList.add('is-wrong');
|
||||
}
|
||||
window.forgeQuizTotalAnswered += 1;
|
||||
state.answeredCount += 1;
|
||||
const fx = forgeQuizTemplateEffectsFor(widget);
|
||||
}
|
||||
|
||||
// Crédite le score/compteurs SI la réponse est bonne, puis déclenche
|
||||
// l'effet visuel du modèle actif (onCorrect/onWrong, voir static/js/play/
|
||||
// quiz-box-templates.js) — extrait de forgeQuizBoxAnswer.
|
||||
function applyQuizAnswerScoreAndEffect(widget, state, correct, fx) {
|
||||
if (correct) {
|
||||
window.forgeQuizCorrectAnswered += 1;
|
||||
state.correctCount += 1;
|
||||
@@ -533,6 +528,11 @@ function forgeQuizBoxAnswer(quizObjectId, choiceIndex) {
|
||||
} else if (fx.onWrong) {
|
||||
fx.onWrong(widget);
|
||||
}
|
||||
}
|
||||
|
||||
// xAPI + SCORM 2004 (cmi.interactions.n.*, no-op en SCORM 1.2/hors LMS) —
|
||||
// extrait de forgeQuizBoxAnswer.
|
||||
function notifyQuizAnswerTelemetry(state, correct, choiceIndex) {
|
||||
if (typeof forgeXapiNotifyQuestionAnswered === 'function') {
|
||||
forgeXapiNotifyQuestionAnswered(
|
||||
state.dialogueId,
|
||||
@@ -543,8 +543,6 @@ function forgeQuizBoxAnswer(quizObjectId, choiceIndex) {
|
||||
correct ? state.rewardAmount : 0,
|
||||
);
|
||||
}
|
||||
// cmi.interactions.n.* (SCORM 2004 uniquement, voir scorm2004-api.js) :
|
||||
// no-op en SCORM 1.2/hors LMS (forgeScorm2004Api y reste null).
|
||||
if (typeof forgeScorm2004NotifyQuestionAnswered === 'function') {
|
||||
forgeScorm2004NotifyQuestionAnswered(
|
||||
state.dialogueId,
|
||||
@@ -556,19 +554,50 @@ function forgeQuizBoxAnswer(quizObjectId, choiceIndex) {
|
||||
state.correctIndex,
|
||||
);
|
||||
}
|
||||
// RGAA/WCAG 2.2.1 (délai réglable) : avancait auparavant tout seul
|
||||
// après un délai fixe (700ms/1400ms), jamais assez pour un lecteur
|
||||
// lent ou un utilisateur de lecteur d'écran — l'avancée dépend
|
||||
// maintenant uniquement d'un clic sur "Continuer →", jamais d'un
|
||||
// minuteur. Le bouton existe déjà (posé désactivé par forgeShowQuizBox
|
||||
// ci-dessus, TOUJOURS visible — demande explicite, comme la maquette
|
||||
// manga) : on se contente ici de l'ACTIVER, jamais de le recréer (ça
|
||||
// effacerait le surlignage vert/rouge qu'on vient de poser sur les
|
||||
// choix juste au-dessus).
|
||||
if (widget) {
|
||||
const continueBtnToEnable = widget.querySelector('.quizBoxContinueBtn');
|
||||
if (continueBtnToEnable) continueBtnToEnable.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// RGAA/WCAG 2.2.1 (délai réglable) : avancait auparavant tout seul après
|
||||
// un délai fixe (700ms/1400ms), jamais assez pour un lecteur lent ou un
|
||||
// utilisateur de lecteur d'écran — l'avancée dépend maintenant
|
||||
// uniquement d'un clic sur "Continuer →", jamais d'un minuteur. Le
|
||||
// bouton existe déjà (posé désactivé par forgeShowQuizBox, TOUJOURS
|
||||
// visible — demande explicite, comme la maquette manga) : on se contente
|
||||
// ici de l'ACTIVER, jamais de le recréer (ça effacerait le surlignage
|
||||
// vert/rouge qu'on vient de poser sur les choix). Extrait de
|
||||
// forgeQuizBoxAnswer.
|
||||
function enableQuizContinueButton(widget) {
|
||||
if (!widget) return;
|
||||
const continueBtnToEnable = widget.querySelector('.quizBoxContinueBtn');
|
||||
if (continueBtnToEnable) continueBtnToEnable.disabled = false;
|
||||
}
|
||||
|
||||
// Appelée par le clic sur un choix (les boutons sont injectés EN JEU par
|
||||
// forgeShowQuizBox ci-dessus, jamais rendus côté serveur — voir
|
||||
// render_scene_object.py pour le seul aperçu statique de l'éditeur).
|
||||
// TOUJOURS crédite le score si la réponse est bonne PUIS avance à la
|
||||
// ligne suivante, QUE la réponse soit bonne ou mauvaise (demande
|
||||
// explicite : une mauvaise réponse ne doit jamais bloquer la
|
||||
// progression). Un second clic pendant cet affichage est ignoré
|
||||
// (`resolved`).
|
||||
function forgeQuizBoxAnswer(quizObjectId, choiceIndex) {
|
||||
const state = forgeQuizBoxState[quizObjectId];
|
||||
if (!state || state.resolved) return;
|
||||
state.resolved = true;
|
||||
const widget = forgeQuizBoxWidgetById(quizObjectId);
|
||||
const correct = choiceIndex === state.correctIndex;
|
||||
// Minuteur "par question" : cette question est résolue (bonne réponse,
|
||||
// mauvaise réponse, OU minuteur expiré via forgeQuizBoxTimeout, qui
|
||||
// appelle CETTE fonction avec choiceIndex=-1) — jamais le minuteur
|
||||
// "pour tout le quiz", qui doit continuer à tourner sans interruption
|
||||
// d'une question à l'autre.
|
||||
if (widget?.dataset.timerMode === 'question') forgeStopQuizTimer(quizObjectId);
|
||||
markQuizChoiceButtons(widget, state, choiceIndex);
|
||||
window.forgeQuizTotalAnswered += 1;
|
||||
state.answeredCount += 1;
|
||||
const fx = forgeQuizTemplateEffectsFor(widget);
|
||||
applyQuizAnswerScoreAndEffect(widget, state, correct, fx);
|
||||
notifyQuizAnswerTelemetry(state, correct, choiceIndex);
|
||||
enableQuizContinueButton(widget);
|
||||
}
|
||||
|
||||
// Prépare l'écran de résultat de fin de quiz (voir static/js/play/
|
||||
@@ -595,7 +624,7 @@ function forgeQuizPrepareResultScreen(quizObjectId, widget, state, forceEnd) {
|
||||
const { answeredCount } = state;
|
||||
return function () {
|
||||
widget.classList.add('is-active');
|
||||
if (widget.getAttribute('data-fullscreen') === '1') widget.classList.add('is-fullscreen');
|
||||
if (widget.dataset.fullscreen === '1') widget.classList.add('is-fullscreen');
|
||||
completeFx(widget, { correctCount, totalCount: answeredCount });
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user