Enrichit le reporting SCORM/xAPI et met en conformité RGAA le player
SCORM/xAPI : - Ajoute l'export SCORM 2004 (3rd/4th edition) au choix, en plus du 1.2 par défaut : sépare completion_status/success_status (un échec reste "completed" au lieu de retomber à tort en "incomplete" comme force la 1.2), remonte aussi cmi.interactions.n.* par question répondue. - Fournit cmi.core.score.min/max (1.2 et 2004), calculé depuis les récompenses de quiz du jeu, pour que le LMS affiche un vrai pourcentage au lieu du score brut à tort étiqueté "%". - Libellés de verbes xAPI en français en plus de l'anglais. Accessibilité (RGAA/WCAG 2.1 AA) sur le player : - Navigation clavier des objets de scène "au clic"/"au survol" (tabindex, role, Entrée/Espace, focus/blur). - alt sur les images (nom auteur ou décoratif), aria-hidden sur les icônes seules, role="dialog"/aria-live sur les boîtes de dialogue/quiz. - Landmark <main> + titre de page, respect de prefers-reduced-motion. - Le quiz n'avance plus automatiquement après un délai fixe : bouton "Continuer →" explicite (RGAA 2.2.1). - Avertissement de contraste dans l'éditeur de style de dialogue. - Déclaration d'accessibilité téléchargeable depuis la modale d'export.
This commit is contained in:
@@ -11,8 +11,9 @@ global.gameData = {};
|
||||
const {
|
||||
forgeQuestById, forgeShowDialogueBox, forgeDialogueBoxAdvance,
|
||||
forgeDialogueBoxAcceptQuest, forgeDialogueBoxDeclineQuest,
|
||||
forgeQuizBoxAnswer, forgeUpdateAllScoreWidgets,
|
||||
forgeQuizBoxAnswer, forgeQuizBoxContinue, forgeUpdateAllScoreWidgets,
|
||||
forgeAddQuizScore, forgeSyncAllQuestsCompletionToScorm,
|
||||
forgeComputeFinalScormStatus,
|
||||
} = require('../dialogue-box-controller.js');
|
||||
|
||||
function fakeWidget(objectId) {
|
||||
@@ -37,6 +38,11 @@ function fakeWidget(objectId) {
|
||||
|
||||
test.beforeEach(() => {
|
||||
global.window.currentScreenId = 1;
|
||||
// Compteurs de réussite (voir forgeComputeFinalScormStatus) : remis à
|
||||
// zéro entre chaque test, sinon les réponses au quiz d'un test
|
||||
// fuiraient sur le calcul de statut final du test suivant.
|
||||
global.window.forgeQuizTotalAnswered = 0;
|
||||
global.window.forgeQuizCorrectAnswered = 0;
|
||||
global.gameData = {
|
||||
quests: [
|
||||
{ id: 5, title: 'La quête du sorcier', status: 'nouvelle', objectif: 'Parler au sorcier', dialogues: { nouvelle: [{ speaker: 'Le Sorcier', text: 'Bonjour !' }, { speaker: 'Joueur', text: 'Salut.' }], en_cours: [], terminee: [] } },
|
||||
@@ -236,12 +242,10 @@ test('une ligne "question" est affichée dans la boîte à QUIZ (header = quête
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('mauvaise réponse au quiz : aucun point, MAIS avance quand même (ne bloque jamais la progression), la bonne réponse est surlignée', () => {
|
||||
test('mauvaise réponse au quiz : aucun point, MAIS avance quand même sur "Continuer →" (ne bloque jamais la progression), la bonne réponse est surlignée', () => {
|
||||
const dialogueWidget = fakeWidget(42);
|
||||
const quizWidget = fakeQuizWidget(99, 2);
|
||||
const previousDoc = global.document;
|
||||
const previousTimeout = global.setTimeout;
|
||||
global.setTimeout = fn => fn(); // exécute la révélation immédiatement pour le test
|
||||
global.document = docWithDialogueAndQuiz(dialogueWidget, quizWidget);
|
||||
try {
|
||||
forgeShowDialogueBox(10);
|
||||
@@ -250,20 +254,21 @@ test('mauvaise réponse au quiz : aucun point, MAIS avance quand même (ne bloqu
|
||||
assert.equal(window.forgeQuizScore, 0); // aucun point
|
||||
assert.equal(quizWidget._buttons[0]._classes.has('is-wrong'), true); // le choix cliqué (faux) surligné en rouge
|
||||
assert.equal(quizWidget._buttons[1]._classes.has('is-correct'), true); // la BONNE réponse indiquée quand même
|
||||
// Avance quand même à la ligne suivante — jamais bloqué sur une mauvaise réponse.
|
||||
// RGAA/WCAG 2.2.1 : plus d'avance automatique — jamais bloqué sur une
|
||||
// mauvaise réponse, mais seulement au clic explicite sur "Continuer →".
|
||||
assert.match(quizWidget._roles.choices.innerHTML, /forgeQuizBoxContinue\(99\)/);
|
||||
assert.equal(quizWidget._classes.has('is-active'), true); // toujours affichée tant que non cliqué
|
||||
forgeQuizBoxContinue(99);
|
||||
assert.equal(quizWidget._classes.has('is-active'), false);
|
||||
assert.equal(dialogueWidget._classes.has('is-active'), true);
|
||||
assert.equal(dialogueWidget._roles.body.textContent, 'Bien joué !');
|
||||
} finally { global.document = previousDoc; global.setTimeout = previousTimeout; }
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('un second clic pendant la révélation (avant l\'avance automatique) est ignoré', () => {
|
||||
test('un second clic pendant la révélation (avant "Continuer →") est ignoré', () => {
|
||||
const dialogueWidget = fakeWidget(42);
|
||||
const quizWidget = fakeQuizWidget(99, 2);
|
||||
const previousDoc = global.document;
|
||||
const previousTimeout = global.setTimeout;
|
||||
const pending = [];
|
||||
global.setTimeout = fn => pending.push(fn); // ne s'exécute PAS tout de suite -> la question reste affichée
|
||||
global.document = docWithDialogueAndQuiz(dialogueWidget, quizWidget);
|
||||
try {
|
||||
forgeShowDialogueBox(10);
|
||||
@@ -271,17 +276,15 @@ test('un second clic pendant la révélation (avant l\'avance automatique) est i
|
||||
forgeQuizBoxAnswer(99, 0); // 1er clic (faux)
|
||||
forgeQuizBoxAnswer(99, 1); // 2nd clic pendant la révélation -> ignoré
|
||||
assert.equal(window.forgeQuizScore, 0); // le 2nd clic (bon) n'a pas compté
|
||||
pending.forEach(fn => fn());
|
||||
assert.equal(window.forgeQuizScore, 0); // toujours 0 après l'avance différée
|
||||
} finally { global.document = previousDoc; global.setTimeout = previousTimeout; }
|
||||
forgeQuizBoxContinue(99);
|
||||
assert.equal(window.forgeQuizScore, 0); // toujours 0 après avoir continué
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('bonne réponse au quiz : crédite le score, la surligne en vert, masque la boîte, avance à la ligne suivante', () => {
|
||||
test('bonne réponse au quiz : crédite le score, la surligne en vert, "Continuer →" masque la boîte et avance à la ligne suivante', () => {
|
||||
const dialogueWidget = fakeWidget(42);
|
||||
const quizWidget = fakeQuizWidget(99, 2);
|
||||
const previousDoc = global.document;
|
||||
const previousTimeout = global.setTimeout;
|
||||
global.setTimeout = fn => fn();
|
||||
global.document = docWithDialogueAndQuiz(dialogueWidget, quizWidget);
|
||||
try {
|
||||
forgeShowDialogueBox(10);
|
||||
@@ -289,6 +292,8 @@ test('bonne réponse au quiz : crédite le score, la surligne en vert, masque la
|
||||
forgeQuizBoxAnswer(99, 1); // bonne réponse
|
||||
assert.equal(window.forgeQuizScore, 5);
|
||||
assert.equal(quizWidget._buttons[1]._classes.has('is-correct'), true);
|
||||
assert.equal(quizWidget._classes.has('is-active'), true); // pas encore avancé
|
||||
forgeQuizBoxContinue(99);
|
||||
assert.equal(quizWidget._classes.has('is-active'), false);
|
||||
assert.equal(dialogueWidget._classes.has('is-active'), true);
|
||||
assert.equal(dialogueWidget._roles.body.textContent, 'Bien joué !');
|
||||
@@ -296,7 +301,11 @@ test('bonne réponse au quiz : crédite le score, la surligne en vert, masque la
|
||||
forgeDialogueBoxAdvance(42); // dernière ligne -> dialogue "en_cours" épuisé -> quête "terminee"
|
||||
assert.equal(forgeQuestById(10).status, 'terminee');
|
||||
assert.equal(dialogueWidget._classes.has('is-active'), false);
|
||||
} finally { global.document = previousDoc; global.setTimeout = previousTimeout; }
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('forgeQuizBoxContinue ne fait rien si déjà appelé/aucune question en attente (état déjà nettoyé)', () => {
|
||||
assert.doesNotThrow(() => forgeQuizBoxContinue(99999));
|
||||
});
|
||||
|
||||
test('aucune boîte à quiz posée sur la scène : la question est ignorée, la conversation avance seule', () => {
|
||||
@@ -394,3 +403,118 @@ test('la dernière ligne d\'un dialogue "en_cours" propage la complétion à gam
|
||||
assert.equal(gameData.scoring.status, 'termine');
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
// ---------- Seuil de réussite (voir db/games/get_success_threshold.py) ----------
|
||||
|
||||
test('forgeComputeFinalScormStatus — "termine" si aucune question posée (rien à évaluer)', () => {
|
||||
global.window.forgeQuizTotalAnswered = 0;
|
||||
global.window.forgeQuizCorrectAnswered = 0;
|
||||
assert.equal(forgeComputeFinalScormStatus(), 'termine');
|
||||
});
|
||||
|
||||
test('forgeComputeFinalScormStatus — "reussi" si le taux de bonnes réponses atteint le seuil configuré', () => {
|
||||
global.window.forgeQuizTotalAnswered = 10;
|
||||
global.window.forgeQuizCorrectAnswered = 7;
|
||||
global.window.FORGE_SUCCESS_THRESHOLD_PERCENT = 70;
|
||||
assert.equal(forgeComputeFinalScormStatus(), 'reussi');
|
||||
});
|
||||
|
||||
test('forgeComputeFinalScormStatus — "echoue" si le taux de bonnes réponses est sous le seuil configuré', () => {
|
||||
global.window.forgeQuizTotalAnswered = 10;
|
||||
global.window.forgeQuizCorrectAnswered = 6;
|
||||
global.window.FORGE_SUCCESS_THRESHOLD_PERCENT = 70;
|
||||
assert.equal(forgeComputeFinalScormStatus(), 'echoue');
|
||||
});
|
||||
|
||||
test('forgeComputeFinalScormStatus — seuil par défaut (70%) si FORGE_SUCCESS_THRESHOLD_PERCENT absent (hors export SCORM)', () => {
|
||||
global.window.forgeQuizTotalAnswered = 10;
|
||||
global.window.forgeQuizCorrectAnswered = 8;
|
||||
delete global.window.FORGE_SUCCESS_THRESHOLD_PERCENT;
|
||||
assert.equal(forgeComputeFinalScormStatus(), 'reussi');
|
||||
});
|
||||
|
||||
test('forgeSyncAllQuestsCompletionToScorm propage reussi/echoue (pas juste "termine") une fois toutes les quêtes finies', () => {
|
||||
global.gameData.quests = [{ id: 1, status: 'terminee' }];
|
||||
global.gameData.scoring = { score: 0, status: 'en_cours' };
|
||||
global.window.forgeQuizTotalAnswered = 4;
|
||||
global.window.forgeQuizCorrectAnswered = 1; // 25% < seuil par défaut (70%)
|
||||
delete global.window.FORGE_SUCCESS_THRESHOLD_PERCENT;
|
||||
forgeSyncAllQuestsCompletionToScorm();
|
||||
assert.equal(gameData.scoring.status, 'echoue');
|
||||
});
|
||||
|
||||
// ---------- Granularité xAPI (quête acceptée/terminée, question répondue) ----------
|
||||
|
||||
test('accepter une quête notifie xAPI (verbe "attempted" ciblant CETTE quête)', () => {
|
||||
const widget = fakeWidget(42);
|
||||
global.gameData.scoring = undefined;
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => widget };
|
||||
const calls = [];
|
||||
global.forgeXapiNotifyQuestAccepted = quest => calls.push(quest);
|
||||
try {
|
||||
forgeDialogueBoxAcceptQuest(5, 42);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].id, 5);
|
||||
} finally { global.document = previousDoc; delete global.forgeXapiNotifyQuestAccepted; }
|
||||
});
|
||||
|
||||
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),
|
||||
classList: { add: () => {}, remove: () => {} },
|
||||
querySelector: sel => (sel.indexOf('choices') !== -1 ? { innerHTML: '' } : { textContent: '' }),
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
global.gameData.quests = [
|
||||
{ id: 5, title: 'La quête du sorcier', status: 'en_cours', dialogues: { nouvelle: [], en_cours: [{ type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 5 }], terminee: [] } },
|
||||
];
|
||||
const previousDoc = global.document;
|
||||
global.document = {
|
||||
querySelector: sel => (sel.indexOf('quizBoxWidget') !== -1 ? quizWidget : dialogueWidget),
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
const calls = [];
|
||||
global.forgeXapiNotifyQuestionAnswered = (quest, index, text, correct, score) => calls.push({ quest, index, text, correct, score });
|
||||
try {
|
||||
forgeShowDialogueBox(5);
|
||||
forgeQuizBoxAnswer(99, 0); // bonne réponse (correct_index=0)
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].quest.id, 5);
|
||||
assert.equal(calls[0].correct, true);
|
||||
assert.equal(calls[0].score, 5);
|
||||
forgeQuizBoxContinue(99); // clic explicite (RGAA 2.2.1, plus d'avance automatique)
|
||||
} finally { global.document = previousDoc; delete global.forgeXapiNotifyQuestionAnswered; }
|
||||
});
|
||||
|
||||
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),
|
||||
classList: { add: () => {}, remove: () => {} },
|
||||
querySelector: sel => (sel.indexOf('choices') !== -1 ? { innerHTML: '' } : { textContent: '' }),
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
global.gameData.quests = [
|
||||
{ id: 5, title: 'La quête du sorcier', status: 'en_cours', dialogues: { nouvelle: [], en_cours: [{ type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 5 }], terminee: [] } },
|
||||
];
|
||||
const previousDoc = global.document;
|
||||
global.document = {
|
||||
querySelector: sel => (sel.indexOf('quizBoxWidget') !== -1 ? quizWidget : dialogueWidget),
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
const calls = [];
|
||||
global.forgeScorm2004NotifyQuestionAnswered = (quest, index, text, correct, choiceIndex, correctIndex) =>
|
||||
calls.push({ quest, index, text, correct, choiceIndex, correctIndex });
|
||||
try {
|
||||
forgeShowDialogueBox(5);
|
||||
forgeQuizBoxAnswer(99, 1); // mauvaise réponse (correct_index=0, répond 1)
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].quest.id, 5);
|
||||
assert.equal(calls[0].correct, false);
|
||||
assert.equal(calls[0].choiceIndex, 1);
|
||||
assert.equal(calls[0].correctIndex, 0);
|
||||
forgeQuizBoxContinue(99); // clic explicite (RGAA 2.2.1, plus d'avance automatique)
|
||||
} finally { global.document = previousDoc; delete global.forgeScorm2004NotifyQuestionAnswered; }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user