from pathlib import Path ROOT = Path(__file__).resolve().parents[1] INDEX = (ROOT / "frontend/static/index.html").read_text() MAIN_JS = (ROOT / "frontend/static/src/main.js").read_text() API_JS = (ROOT / "frontend/static/src/api.js").read_text() WORLD_JS = (ROOT / "frontend/static/src/world.js").read_text() STYLES = (ROOT / "frontend/static/src/styles.css").read_text() def test_auth_is_the_only_initially_visible_game_screen(): assert 'data-screen="auth"' in INDEX for screen in ("onboarding", "world", "school", "home", "hq", "quiz", "boss"): assert f'data-screen="{screen}" hidden' in INDEX def test_space_auth_uses_bearer_token_when_iframe_cookies_are_blocked(): assert "sessionStorage.setItem" in API_JS assert "headers.Authorization = `Bearer ${token}`" in API_JS assert "setSessionToken(response.session_token)" in MAIN_JS assert "setSessionToken(null)" in MAIN_JS def test_world_is_created_lazily_after_authentication(): assert "let world = null" in MAIN_JS assert "function ensureWorld()" in MAIN_JS assert 'if (next === "world")' in MAIN_JS def test_game_assets_use_deployment_cache_busting(): assert 'src="/game-static/src/main.js?v=20260615-2"' in INDEX assert 'href="/game-static/src/styles.css?v=20260615-2"' in INDEX assert './world.js?v=20260615-2' in MAIN_JS assert './character.js?v=20260615-2' in WORLD_JS assert 'world-map-agent.png?v=20260615-2' in WORLD_JS def test_world_keyboard_is_disabled_off_screen_and_ignores_editable_focus(): assert 'if (!active || isEditableTarget(event.target)' in WORLD_JS assert 'window.addEventListener("keydown", handleKeyDown)' in WORLD_JS assert "pressedKeys.clear()" in WORLD_JS assert "isEditableTarget(document.activeElement)" in WORLD_JS assert 'target.matches("input, textarea, select' in WORLD_JS def test_world_uses_fixed_internal_resolution_to_avoid_hidden_canvas_collapse(): assert "Phaser.Scale.FIT" in WORLD_JS assert "mode: Phaser.Scale.RESIZE" not in WORLD_JS assert "type: Phaser.CANVAS" in WORLD_JS def test_world_does_not_use_phaser_global_keyboard_capture(): assert "createCursorKeys" not in WORLD_JS assert "addKeys" not in WORLD_JS def test_world_repaints_after_becoming_visible_again(): assert "getPlayerPosition()" in WORLD_JS assert "initialPlayerPosition" in WORLD_JS assert "world.destroy()" in MAIN_JS assert "world = null" in MAIN_JS assert '$("#worldMount").replaceChildren()' in MAIN_JS def test_final_lesson_step_leads_directly_to_quiz(): assert 'ui.nextStep.textContent = finalStep ? "Start Quiz" : "Next"' in MAIN_JS assert "function advanceLesson()" in MAIN_JS assert "if (finalStep) beginQuiz()" in MAIN_JS def test_teacher_chat_supports_close_scroll_and_chained_questions(): assert '$("#closeTeacherChat")' in MAIN_JS assert 'ui.teacherChat.hidden = true' in MAIN_JS assert "teacherChatHistory" in MAIN_JS assert "history: priorHistory" in MAIN_JS assert "scrollTop = ui.teacherChatMessages.scrollHeight" in MAIN_JS assert "scrollElementToTop(ui.teacherChatMessages, latestTeacher)" in MAIN_JS assert 'querySelector(".chat-message.teacher:last-of-type")' in MAIN_JS def test_lesson_navigation_scrolls_reader_to_top_with_reduced_motion_support(): assert "scrollToStart(ui.lessonReader)" in MAIN_JS assert 'window.matchMedia?.("(prefers-reduced-motion: reduce)")' in MAIN_JS assert 'top: 0, behavior: prefersReducedMotion() ? "auto" : "smooth"' in MAIN_JS def test_character_customization_is_registered_and_used_by_world(): assert 'id="appearanceFields"' in INDEX assert 'name="shirtColor"' in INDEX assert 'name="pantsColor"' in INDEX assert 'name="hairColor"' in INDEX assert "payload.character_appearance = selectedAppearance()" in MAIN_JS assert "getPlayerAppearance" in WORLD_JS assert 'from "./character.js?v=20260615-2"' in MAIN_JS assert 'from "./character.js?v=20260615-2"' in WORLD_JS assert "createCharacter" in WORLD_JS assert "updateCharacter" in WORLD_JS assert "drawCharacterPreview" in MAIN_JS def test_layered_character_uses_shared_sprite_contract(): character = (ROOT / "frontend/static/src/character.js").read_text() assert 'width: 32, height: 48' in character assert '["gear-back", "body", "pants", "shirt", "hair", "gear-front"]' in character assert 'const DIRECTIONS = { down: 0, left: 1, right: 2, up: 3 }' in character assert 'Math.floor(time / 125)' in character assert 'resolveCharacterFacing' in character assert 'characterAssetUrl' in character assert 'previewDrawIds' in character for color in ("red", "blue", "green", "yellow", "purple", "teal"): assert f"{color}:" in character for color in ("navy", "charcoal", "brown", "olive", "blue", "plum"): assert f"{color}:" in character for color in ("black", "dark_brown", "brown", "blond", "auburn"): assert f"{color}:" in character for layer in ("gear-back", "body", "gear-front"): assert (ROOT / f"frontend/static/assets/player/student-{layer}.png").is_file() for layer, colors in { "shirt": ("red", "blue", "green", "yellow", "purple", "teal"), "pants": ("navy", "charcoal", "brown", "olive", "blue", "plum"), "hair": ("black", "dark_brown", "brown", "blond", "auburn"), }.items(): for color in colors: assert (ROOT / f"frontend/static/assets/player/student-{layer}-{color}.png").is_file() def test_quiz_and_boss_wait_for_explicit_progression(): assert 'id="nextQuizQuestionButton"' in INDEX assert 'id="nextBossQuestionButton"' in INDEX assert 'id="quizProgressBar"' in INDEX assert 'id="bossProgressBar"' in INDEX assert "advanceQuizAfterFeedback" in MAIN_JS assert "advanceBossAfterFeedback" in MAIN_JS assert "setTimeout(renderQuizQuestion" not in MAIN_JS assert "setTimeout(renderBossQuestion" not in MAIN_JS def test_quiz_and_boss_use_forms_and_typed_answers_take_precedence(): assert 'id="quizAnswerForm"' in INDEX assert 'id="bossAnswerForm"' in INDEX assert 'id="submitQuizAnswerButton" type="submit"' in INDEX assert 'id="submitBossAnswerButton" type="submit"' in INDEX assert 'ui.quizForm.addEventListener("submit", submitQuizAnswer)' in MAIN_JS assert 'ui.bossForm.addEventListener("submit", submitBossAnswer)' in MAIN_JS assert "if (text) return { ...base, answer_text: text }" in MAIN_JS assert "return { ...base, image_data_url: image }" in MAIN_JS assert "event?.preventDefault()" in MAIN_JS def test_answer_feedback_has_visual_states_and_resets(): assert 'setFeedback("quiz", response.result.correct ? "correct" : "incorrect"' in MAIN_JS assert 'setFeedback("boss", response.result.correct ? "correct" : "incorrect"' in MAIN_JS assert 'setFeedback("quiz", "neutral", "")' in MAIN_JS assert 'setFeedback("boss", "neutral", "")' in MAIN_JS assert '.feedback-line[data-status="correct"]' in STYLES assert '.feedback-line[data-status="incorrect"]' in STYLES assert '.battle-console[data-feedback="correct"]' in STYLES assert '.notebook-page[data-feedback="incorrect"]' in STYLES def test_touch_navigation_supports_press_and_hold_cleanup(): assert "const heldDirections = new Set()" in WORLD_JS assert "startMoving(direction)" in WORLD_JS assert "stopMoving(direction)" in WORLD_JS assert "heldDirections.clear()" in WORLD_JS assert 'window.addEventListener("blur", clearMovement)' in WORLD_JS assert 'window.removeEventListener("blur", clearMovement)' in WORLD_JS assert 'button.addEventListener("pointerdown"' in MAIN_JS assert 'button.addEventListener("pointerup", stop)' in MAIN_JS assert 'button.addEventListener("pointercancel", stop)' in MAIN_JS assert 'button.addEventListener("lostpointercapture"' in MAIN_JS assert "touch-action: none" in STYLES def test_world_movement_uses_reduced_consistent_speed(): assert "const PLAYER_SPEED = 1.8" in WORLD_JS assert "const TOUCH_MOVE_STEP = 15" in WORLD_JS assert "dx -= PLAYER_SPEED" in WORLD_JS assert "const amount = TOUCH_MOVE_STEP" in WORLD_JS def test_generated_room_and_character_art_are_wired_with_fallbacks(): assert "/game-static/assets/bedroom-agent.png" in (ROOT / "frontend/static/src/styles.css").read_text() assert "/game-static/assets/world-map-agent.png" in (ROOT / "frontend/static/src/world.js").read_text() assert "/game-static/assets/codename-grandma.png" in INDEX assert "/game-static/assets/villain-default.png" in INDEX assert "safeVillainUrl" in MAIN_JS assert 'drawBuildingHotspot(scene, buildings.hq, "GRANDMA", "hq")' in WORLD_JS