| """Unit tests for Challenge mode event handlers in app.py. |
| |
| This module verifies that on_challenge_answer and on_next_challenge |
| correctly parse and handle string-typed time values from Gradio components. |
| """ |
|
|
| import json |
| import sys |
| from pathlib import Path |
|
|
| |
| sys.path.insert(0, str(Path(__file__).parent.parent)) |
|
|
| from app import build_ui, GameOverResult, ChallengeAnswerResult |
|
|
|
|
| def test_challenge_handlers() -> None: |
| """Verify that challenge mode handlers handle string-typed time inputs correctly. |
| |
| This test extracts the registered event handlers from the built Gradio |
| UI Blocks object, executes them with both string and integer time inputs, |
| and asserts that they calculate scoring, streaks, and elapsed times |
| without throwing any TypeError. It also validates that the returned |
| values are instances of our custom class results and their properties. |
| """ |
| demo = build_ui() |
|
|
| on_challenge_answer_fn = None |
| on_next_challenge_fn = None |
| on_game_over_fn = None |
|
|
| for bf in demo.fns.values(): |
| fn = getattr(bf, "fn", None) |
| if fn: |
| if fn.__name__ == "on_challenge_answer": |
| on_challenge_answer_fn = fn |
| elif fn.__name__ == "on_next_challenge": |
| on_next_challenge_fn = fn |
| elif fn.__name__ == "on_game_over": |
| on_game_over_fn = fn |
|
|
| assert on_challenge_answer_fn is not None, "on_challenge_answer not found" |
| assert on_next_challenge_fn is not None, "on_next_challenge not found" |
| assert on_game_over_fn is not None, "on_game_over not found" |
|
|
| |
| initial_state = { |
| "score": 10, |
| "streak": 2, |
| "time_left": 120, |
| "correct_index": 0, |
| "theme": "All", |
| "riddle_start_time": 120, |
| "active": True, |
| } |
| state_str = json.dumps(initial_state) |
|
|
| |
| |
| res_ans = on_challenge_answer_fn("A) Option A", state_str, "115") |
| assert isinstance(res_ans, ChallengeAnswerResult) |
| assert "Correct!" in res_ans.feedback |
| assert res_ans.correct_update.get("visible") is False |
| st = json.loads(res_ans.updated_state) |
| assert st["time_left"] == 115 |
| assert st["streak"] == 3 |
| assert st["score"] > 10 |
| assert res_ans.interactive_update is not None |
| assert res_ans.interactive_update.get("interactive") is False |
| assert "value" not in res_ans.interactive_update |
|
|
| |
| fb, correct_update, updated_state, interactive_update, score_upd, streak_upd = res_ans |
| assert fb == res_ans.feedback |
|
|
| |
| |
| res_go_correct = on_game_over_fn(res_ans.updated_state) |
| assert isinstance(res_go_correct, GameOverResult) |
| st_go_correct = json.loads(res_go_correct.state_json) |
| assert st_go_correct["score"] == st["score"] |
|
|
| |
| res_ans_wrong = on_challenge_answer_fn("B) Option B", state_str, 115) |
| assert isinstance(res_ans_wrong, ChallengeAnswerResult) |
| assert "Wrong!" in res_ans_wrong.feedback |
| st_int = json.loads(res_ans_wrong.updated_state) |
| assert st_int["time_left"] == 115 |
| assert st_int["streak"] == 0 |
| assert res_ans_wrong.interactive_update is not None |
| assert res_ans_wrong.interactive_update.get("interactive") is False |
| assert "value" not in res_ans_wrong.interactive_update |
|
|
| |
| res_nc = on_next_challenge_fn(state_str, "100") |
| assert isinstance(res_nc, dict) |
| |
| |
| assert len(res_nc) == 5 |
| |
| options_update = None |
| for _comp, val in res_nc.items(): |
| if isinstance(val, dict) and val.get("__type__") == "update" and "choices" in val: |
| options_update = val |
| break |
| assert options_update is not None |
| assert options_update.get("interactive") is True |
| |
| state_value = None |
| for _comp, val in res_nc.items(): |
| if isinstance(val, str) and val.startswith("{"): |
| state_value = val |
| break |
| assert state_value is not None |
| st_nc = json.loads(state_value) |
| assert st_nc["time_left"] == 100 |
| assert st_nc["riddle_start_time"] == 100 |
|
|
| |
| res_go = on_game_over_fn(state_str) |
| assert isinstance(res_go, GameOverResult) |
| assert res_go.timer_display == "00:00" |
| st_go = json.loads(res_go.state_json) |
| assert st_go["active"] is False |
| assert st_go["time_left"] == 0 |
| st_go = json.loads(res_go.state_json) |
| assert st_go["score"] == 10 |
|
|
| |
| timer_display, state_json_val, game_row_val, game_over_row_val, final_score_val = res_go |
| assert timer_display == "00:00" |
|
|
| |
| game_over_targets = [] |
| for bf in demo.fns.values(): |
| fn = getattr(bf, "fn", None) |
| if fn and fn.__name__ == "on_game_over": |
| for target_id in bf.targets: |
| if target_id[0] in demo.blocks: |
| block = demo.blocks[target_id[0]] |
| game_over_targets.append(getattr(block, "value", None)) |
|
|
| assert "Game Over" in game_over_targets |
| assert "> END GAME" in game_over_targets |
|
|