Dev #11
@@ -69,11 +69,12 @@ def render_scene_object(obj):
|
||||
# Widget "❓ Boîte à quiz" (voir "🖥️ Interface") — réutilise le
|
||||
# MÊME style que "💬 Boîte de dialogue" (resolve_dialogue_box_style/
|
||||
# set_scene_object_dialogue_box_style, une seule "🎨 Style" pour
|
||||
# les deux) : header = la question, corps = les choix de réponse
|
||||
# (boutons remplis EN JEU par static/js/play/dialogue-box-controller.js,
|
||||
# data-quiz-role sert de sélecteur) — pas de pied (une question se
|
||||
# résout au clic sur un choix, pas de "Suivant"). Masqué par
|
||||
# défaut au jeu, comme la boîte de dialogue.
|
||||
# les deux) : header = "Quête : <titre>" (comme demandé), corps =
|
||||
# la question ET ses choix de réponse ensemble (remplis EN JEU par
|
||||
# static/js/play/dialogue-box-controller.js, data-quiz-role sert de
|
||||
# sélecteur) — pas de pied (une question se résout au clic sur un
|
||||
# choix, pas de "Suivant"). Masqué par défaut au jeu, comme la
|
||||
# boîte de dialogue.
|
||||
style = resolve_dialogue_box_style(obj)
|
||||
box_style = (
|
||||
f"position:absolute; left:{obj['x']}px; top:{obj['y']}px; "
|
||||
@@ -83,11 +84,13 @@ def render_scene_object(obj):
|
||||
)
|
||||
return (
|
||||
f'<div class="quizBoxWidget" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" style="{box_style}">'
|
||||
f'<div class="quizBoxHeader" data-quiz-role="header" style="background:{html_lib.escape(style["header_bg"])}">La question s\'affiche ici.</div>'
|
||||
f'<div class="quizBoxChoices" data-quiz-role="choices" style="background:{html_lib.escape(style["body_bg"])}">'
|
||||
f'<div class="quizBoxHeader" data-quiz-role="header" style="background:{html_lib.escape(style["header_bg"])}">Quête : Titre de la quête</div>'
|
||||
f'<div class="quizBoxBody" data-quiz-role="body" style="background:{html_lib.escape(style["body_bg"])}">'
|
||||
f'<p class="quizBoxQuestionText" data-quiz-role="question">La question s\'affiche ici.</p>'
|
||||
f'<div class="quizBoxChoices" data-quiz-role="choices">'
|
||||
f'<button type="button" class="quizBoxChoiceBtn" disabled>Choix A</button>'
|
||||
f'<button type="button" class="quizBoxChoiceBtn" disabled>Choix B</button>'
|
||||
f'</div></div>'
|
||||
f'</div></div></div>'
|
||||
)
|
||||
if obj["kind"] == "score_widget":
|
||||
# Widget "🏆 Score" (voir "🖥️ Interface") — toujours VISIBLE une
|
||||
|
||||
@@ -158,18 +158,31 @@ test('forgeDialogueBoxAdvance — objet inconnu (jamais affiché) : aucun effet'
|
||||
|
||||
// ---------- Enchaînement dialogue + question ("❓ Question") ----------
|
||||
|
||||
function fakeQuizWidget(objectId) {
|
||||
const roles = { header: { textContent: '' }, choices: { innerHTML: '' } };
|
||||
function fakeQuizChoiceBtn() {
|
||||
const classes = new Set();
|
||||
return { disabled: false, classList: { add: c => classes.add(c), contains: c => classes.has(c) }, _classes: classes };
|
||||
}
|
||||
|
||||
function fakeQuizWidget(objectId, choiceCount) {
|
||||
const roles = { header: { textContent: '' }, question: { textContent: '' }, choices: { innerHTML: '' } };
|
||||
const classes = new Set();
|
||||
// querySelectorAll('.quizBoxChoiceBtn') n'a pas besoin de refléter le
|
||||
// HTML réellement injecté dans roles.choices.innerHTML (voir
|
||||
// forgeShowQuizBox) pour ces tests — juste le bon NOMBRE de boutons,
|
||||
// marqués correct/wrong par forgeQuizBoxAnswer via ces mêmes objets.
|
||||
const buttons = Array.from({ length: choiceCount }, () => fakeQuizChoiceBtn());
|
||||
return {
|
||||
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) },
|
||||
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;
|
||||
return null;
|
||||
},
|
||||
_roles: roles, _classes: classes,
|
||||
querySelectorAll: sel => (sel === '.quizBoxChoiceBtn' ? buttons : []),
|
||||
offsetWidth: 100,
|
||||
_roles: roles, _classes: classes, _buttons: buttons,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -188,7 +201,7 @@ function docWithDialogueAndQuiz(dialogueWidget, quizWidget) {
|
||||
|
||||
test.beforeEach(() => {
|
||||
global.gameData.quests.push({
|
||||
id: 10, status: 'en_cours',
|
||||
id: 10, title: 'Le quiz du sorcier', status: 'en_cours',
|
||||
dialogues: {
|
||||
nouvelle: [],
|
||||
en_cours: [
|
||||
@@ -202,10 +215,10 @@ test.beforeEach(() => {
|
||||
window.forgeQuizScore = 0;
|
||||
});
|
||||
|
||||
test('une ligne "question" est affichée dans la boîte à QUIZ, pas la boîte de dialogue', () => {
|
||||
test('une ligne "question" est affichée dans la boîte à QUIZ (header = quête, 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);
|
||||
const quizWidget = fakeQuizWidget(99, 2);
|
||||
const previousDoc = global.document;
|
||||
global.document = docWithDialogueAndQuiz(dialogueWidget, quizWidget);
|
||||
try {
|
||||
@@ -215,36 +228,66 @@ test('une ligne "question" est affichée dans la boîte à QUIZ, pas la boîte d
|
||||
forgeDialogueBoxAdvance(42); // -> ligne "question"
|
||||
assert.equal(dialogueWidget._classes.has('is-active'), false); // masquée pendant la question
|
||||
assert.equal(quizWidget._classes.has('is-active'), true);
|
||||
assert.equal(quizWidget._roles.header.textContent, '2+2 ?');
|
||||
assert.equal(quizWidget._roles.header.textContent, 'Quête : Le quiz du sorcier');
|
||||
assert.equal(quizWidget._roles.question.textContent, '2+2 ?');
|
||||
assert.match(quizWidget._roles.choices.innerHTML, /forgeQuizBoxAnswer\(99, 0\)/);
|
||||
assert.match(quizWidget._roles.choices.innerHTML, /forgeQuizBoxAnswer\(99, 1\)/);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('mauvaise réponse au quiz : aucun effet, la question reste affichée', () => {
|
||||
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', () => {
|
||||
const dialogueWidget = fakeWidget(42);
|
||||
const quizWidget = fakeQuizWidget(99);
|
||||
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);
|
||||
forgeDialogueBoxAdvance(42); // -> question
|
||||
forgeQuizBoxAnswer(99, 0); // mauvaise réponse (correct_index=1)
|
||||
assert.equal(quizWidget._classes.has('is-active'), true); // reste affichée
|
||||
assert.equal(window.forgeQuizScore, 0); // aucun point
|
||||
} finally { global.document = previousDoc; }
|
||||
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.
|
||||
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; }
|
||||
});
|
||||
|
||||
test('bonne réponse au quiz : crédite le score, masque la boîte, avance à la ligne suivante', () => {
|
||||
test('un second clic pendant la révélation (avant l\'avance automatique) est ignoré', () => {
|
||||
const dialogueWidget = fakeWidget(42);
|
||||
const quizWidget = fakeQuizWidget(99);
|
||||
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);
|
||||
forgeDialogueBoxAdvance(42);
|
||||
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; }
|
||||
});
|
||||
|
||||
test('bonne réponse au quiz : crédite le score, la surligne en vert, masque la boîte, 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);
|
||||
forgeDialogueBoxAdvance(42); // -> question
|
||||
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'), false);
|
||||
assert.equal(dialogueWidget._classes.has('is-active'), true);
|
||||
assert.equal(dialogueWidget._roles.body.textContent, 'Bien joué !');
|
||||
@@ -252,7 +295,7 @@ test('bonne réponse au quiz : crédite le score, masque la boîte, avance à 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; }
|
||||
} finally { global.document = previousDoc; global.setTimeout = previousTimeout; }
|
||||
});
|
||||
|
||||
test('aucune boîte à quiz posée sur la scène : la question est ignorée, la conversation avance seule', () => {
|
||||
|
||||
@@ -121,7 +121,7 @@ function forgeShowCurrentConversationLine(dialogueObjectId) {
|
||||
var widget = forgeDialogueBoxWidgetById(dialogueObjectId);
|
||||
if (line.type === 'question') {
|
||||
if (widget) widget.classList.remove('is-active');
|
||||
forgeShowQuizBox(dialogueObjectId, line);
|
||||
forgeShowQuizBox(dialogueObjectId, forgeQuestById(state.questId), line);
|
||||
return;
|
||||
}
|
||||
forgeHideQuizBoxOnCurrentScreen();
|
||||
@@ -189,43 +189,76 @@ function forgeDialogueBoxAdvance(objectId) {
|
||||
forgeAdvanceConversation(objectId);
|
||||
}
|
||||
|
||||
// Combien de temps la bonne/mauvaise réponse reste affichée (surlignée,
|
||||
// voir forgeQuizBoxAnswer) avant d'avancer automatiquement à la ligne
|
||||
// suivante — assez long pour LIRE la bonne réponse en cas d'erreur, plus
|
||||
// court si la réponse était déjà correcte (voir static/style.css,
|
||||
// @keyframes quizPopIn/quizWrongShake pour l'animation elle-même).
|
||||
var FORGE_QUIZ_REVEAL_DELAY_CORRECT = 700;
|
||||
var FORGE_QUIZ_REVEAL_DELAY_WRONG = 1400;
|
||||
|
||||
// Affiche une question ("❓ Question") dans la boîte à quiz posée sur
|
||||
// l'écran — si l'auteur n'en a posé aucune, la question est ignorée
|
||||
// (avance directement) plutôt que de bloquer la conversation pour
|
||||
// toujours en attendant une réponse qu'aucun widget ne peut recueillir.
|
||||
function forgeShowQuizBox(dialogueObjectId, line) {
|
||||
// l'écran — header = "Quête : <titre>", corps = la question + ses choix
|
||||
// (voir demande utilisateur) — si l'auteur n'a posé aucune boîte à quiz,
|
||||
// la question est ignorée (avance directement) plutôt que de bloquer la
|
||||
// conversation pour toujours en attendant une réponse qu'aucun widget ne
|
||||
// peut recueillir.
|
||||
function forgeShowQuizBox(dialogueObjectId, quest, line) {
|
||||
var widget = forgeQuizBoxWidgetOnCurrentScreen();
|
||||
if (!widget) { forgeAdvanceConversation(dialogueObjectId); return; }
|
||||
var quizObjectId = widget.getAttribute('data-object-id');
|
||||
forgeQuizBoxState[quizObjectId] = {
|
||||
dialogueObjectId: dialogueObjectId, correctIndex: line.correct_index, rewardAmount: line.reward_amount,
|
||||
dialogueObjectId: dialogueObjectId, correctIndex: line.correct_index, rewardAmount: line.reward_amount, resolved: false,
|
||||
};
|
||||
var header = widget.querySelector('[data-quiz-role="header"]');
|
||||
var questionEl = widget.querySelector('[data-quiz-role="question"]');
|
||||
var choicesEl = widget.querySelector('[data-quiz-role="choices"]');
|
||||
if (header) header.textContent = line.text;
|
||||
if (header) header.textContent = 'Quête : ' + (quest ? quest.title : '');
|
||||
if (questionEl) questionEl.textContent = line.text;
|
||||
if (choicesEl) {
|
||||
choicesEl.innerHTML = line.choices.map(function (choice, i) {
|
||||
return '<button type="button" class="quizBoxChoiceBtn" onclick="forgeQuizBoxAnswer(' + quizObjectId + ', ' + i + ')">' + forgeEscapeHtml(choice) + '</button>';
|
||||
}).join('');
|
||||
}
|
||||
// Rejoue l'animation d'entrée (@keyframes quizPopIn) même si la boîte
|
||||
// était déjà "is-active" juste avant (2 questions à la suite) : sans
|
||||
// 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;
|
||||
widget.classList.add('is-active');
|
||||
}
|
||||
|
||||
// Appelée par le clic sur un choix (voir render_scene_object.py côté
|
||||
// éditeur — les vrais choix sont injectés EN JEU par forgeShowQuizBox
|
||||
// ci-dessus, jamais rendus côté serveur). Mauvaise réponse : rien, la
|
||||
// question reste affichée. Bonne réponse : crédite le score, masque la
|
||||
// boîte, avance la conversation.
|
||||
// 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) {
|
||||
var state = forgeQuizBoxState[quizObjectId];
|
||||
if (!state) return;
|
||||
if (choiceIndex !== state.correctIndex) return;
|
||||
forgeAddQuizScore(state.rewardAmount);
|
||||
if (!state || state.resolved) return;
|
||||
state.resolved = true;
|
||||
var widget = forgeQuizBoxWidgetById(quizObjectId);
|
||||
if (widget) widget.classList.remove('is-active');
|
||||
var correct = choiceIndex === state.correctIndex;
|
||||
if (widget) {
|
||||
var buttons = widget.querySelectorAll('.quizBoxChoiceBtn');
|
||||
for (var 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');
|
||||
}
|
||||
}
|
||||
if (correct) forgeAddQuizScore(state.rewardAmount);
|
||||
var dialogueObjectId = state.dialogueObjectId;
|
||||
delete forgeQuizBoxState[quizObjectId];
|
||||
forgeAdvanceConversation(dialogueObjectId);
|
||||
setTimeout(function () {
|
||||
if (widget) widget.classList.remove('is-active');
|
||||
delete forgeQuizBoxState[quizObjectId];
|
||||
forgeAdvanceConversation(dialogueObjectId);
|
||||
}, correct ? FORGE_QUIZ_REVEAL_DELAY_CORRECT : FORGE_QUIZ_REVEAL_DELAY_WRONG);
|
||||
}
|
||||
|
||||
// ---------- Offre de quête (Accepter/Refuser) ----------
|
||||
|
||||
+33
-6
@@ -467,7 +467,17 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
|
||||
(.flowCanvasWrap/.animTimelineWrap ont déjà leur propre défilement),
|
||||
jamais sur cette zone entière — sans quoi la barre d'onglets et la barre
|
||||
d'outils défileraient avec le contenu au lieu de rester fixes en haut. */
|
||||
.builderCanvasArea{ flex:1 1 auto; min-width:0; height:100%; overflow:hidden; display:flex; flex-direction:column; }
|
||||
.builderCanvasArea{ flex:1 1 auto; min-width:0; height:100%; overflow:hidden; display:flex; flex-direction:column; transition:margin .2s ease; }
|
||||
/* Bug corrigé : les panneaux flottants (voir .floatPanel plus bas) sont
|
||||
en position:fixed, DONC hors du flux flex de .builder3 — ils flottent
|
||||
PAR-DESSUS le canevas sans jamais réduire sa largeur, rendant sa
|
||||
partie droite (sous "⚙️ Propriétés") ou gauche (sous "🧩 Objets")
|
||||
inaccessible au clic/glisser tant que le panneau correspondant reste
|
||||
ouvert. Réserve maintenant leur largeur explicitement dès qu'un
|
||||
panneau est ouvert (voir :not(.hidden), la classe posée/retirée par
|
||||
openFloatPanel()/closeFloatPanel(), static/js/screen_edit/tree-panels.js). */
|
||||
#builder3:has(#panelLeftFloat:not(.hidden)) .builderCanvasArea{ margin-left:310px; }
|
||||
#builder3:has(#propsPanelFloat:not(.hidden)) .builderCanvasArea{ margin-right:370px; }
|
||||
|
||||
/* ---------- Panneaux gauche/droite flottants ---------- */
|
||||
/* Les deux panneaux (éléments de l'écran / propriétés) sont sortis du flux
|
||||
@@ -670,23 +680,40 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
|
||||
|
||||
/* Widget "❓ Boîte à quiz" (voir screens/rendering/dialogue_box_style.py,
|
||||
static/js/play/dialogue-box-controller.js) : même squelette que
|
||||
"💬 Boîte de dialogue" (header/corps, même "🎨 Style") mais pas de pied
|
||||
— la question se résout au clic sur un choix. Masquée par défaut au
|
||||
jeu, comme la boîte de dialogue. */
|
||||
"💬 Boîte de dialogue" (header "Quête : <titre>"/corps, même "🎨 Style")
|
||||
mais pas de pied — la question se résout au clic sur un choix. Masquée
|
||||
par défaut au jeu, comme la boîte de dialogue ; rejoue son animation
|
||||
d'entrée à CHAQUE nouvelle question (voir forgeShowQuizBox). */
|
||||
@keyframes quizPopIn{ from{ opacity:0; transform:scale(.92) translateY(12px); } to{ opacity:1; transform:none; } }
|
||||
@keyframes quizWrongShake{ 0%, 100%{ transform:translateX(0); } 20%{ transform:translateX(-6px); } 40%{ transform:translateX(6px); } 60%{ transform:translateX(-4px); } 80%{ transform:translateX(4px); } }
|
||||
@keyframes quizCorrectPop{ 0%{ transform:scale(1); } 40%{ transform:scale(1.04); } 100%{ transform:scale(1); } }
|
||||
.quizBoxWidget{
|
||||
display:flex; flex-direction:column; border-radius:10px; overflow:hidden;
|
||||
box-shadow:0 6px 18px rgba(0,0,0,.35); pointer-events:auto;
|
||||
}
|
||||
.playScreen .quizBoxWidget{ display:none; }
|
||||
.playScreen .quizBoxWidget.is-active{ display:flex; }
|
||||
.playScreen .quizBoxWidget.is-active{ display:flex; animation:quizPopIn .3s ease; }
|
||||
.quizBoxHeader{ padding:8px 12px; font-weight:700; }
|
||||
.quizBoxChoices{ padding:10px 12px; flex:1; overflow-y:auto; display:flex; flex-direction:column; gap:8px; }
|
||||
.quizBoxBody{ padding:10px 12px; flex:1; overflow-y:auto; display:flex; flex-direction:column; gap:10px; }
|
||||
.quizBoxQuestionText{ margin:0; font-weight:600; }
|
||||
.quizBoxChoices{ display:flex; flex-direction:column; gap:8px; }
|
||||
.quizBoxChoiceBtn{
|
||||
background:rgba(255,255,255,.12); color:inherit; border:1px solid rgba(255,255,255,.3);
|
||||
border-radius:6px; padding:8px 10px; font:inherit; font-size:13px; text-align:left; cursor:pointer;
|
||||
transition:background .15s ease, transform .1s ease;
|
||||
}
|
||||
.quizBoxChoiceBtn:hover{ background:rgba(255,255,255,.25); transform:translateX(2px); }
|
||||
.quizBoxChoiceBtn:disabled{ cursor:default; }
|
||||
.quizBoxChoiceBtn:disabled:hover{ transform:none; }
|
||||
/* Révélation de la bonne/mauvaise réponse (voir forgeQuizBoxAnswer) —
|
||||
toujours la bonne en vert (même si le joueur s'est trompé, demande
|
||||
explicite : "indiquer la bonne réponse"), le choix cliqué en rouge s'il
|
||||
ne l'était pas. */
|
||||
.quizBoxChoiceBtn.is-correct{
|
||||
background:#2ecc71 !important; border-color:#2ecc71 !important; color:#08210f; font-weight:700;
|
||||
animation:quizCorrectPop .3s ease;
|
||||
}
|
||||
.quizBoxChoiceBtn.is-wrong{ background:#e0524d !important; border-color:#e0524d !important; animation:quizWrongShake .4s ease; }
|
||||
|
||||
/* Widget "🏆 Score" — TOUJOURS visible une fois posé (pas de collision/
|
||||
déclencheur, contrairement aux deux widgets ci-dessus), voir
|
||||
|
||||
@@ -179,7 +179,8 @@ def test_render_quiz_box_has_header_and_choices(tmp_game_slug_cleanup):
|
||||
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
|
||||
obj = screens.get_scene_object(slug, obj_id)
|
||||
html = screens.render_scene_object(obj)
|
||||
assert 'data-quiz-role="header"' in html
|
||||
assert 'data-quiz-role="header"' in html # "Quête : <titre>", rempli en jeu
|
||||
assert 'data-quiz-role="question"' in html # le texte de la question, dans le corps
|
||||
assert 'data-quiz-role="choices"' in html
|
||||
assert f'data-object-id="{obj_id}"' in html
|
||||
assert "quizBoxFooter" not in html # pas de pied, une question se résout au clic
|
||||
|
||||
Reference in New Issue
Block a user