Merge pull request 'Main' (#16) from main into production

Reviewed-on: admin/step3#16
This commit is contained in:
hz
2026-07-30 15:04:21 -05:00
47 changed files with 2421 additions and 38 deletions
+47 -11
View File
@@ -13,7 +13,7 @@ func _initialize() -> void:
_test_eliminated_choice_uses_rich_text_strikeout()
_test_eliminated_choice_that_is_also_locked_wrong_still_strikes_through()
_test_explanation_highlighting_has_no_status_heading()
_test_resolved_followup_has_no_header_or_verdict_prefix()
_test_resolved_followup_stays_expanded_and_toggles()
_test_review_content_normalization()
_test_followup_attempt_order_is_stable()
_test_review_followup_is_optional_and_single_attempt()
@@ -22,6 +22,7 @@ func _initialize() -> void:
_test_feedback_context_excludes_study_content()
_test_feedback_form_validation()
_test_revisitable_navigation_prefers_unanswered_questions()
_test_timer_text_avoids_redundant_frame_updates()
_test_submit_styles_keep_stable_geometry()
_test_core_timer_counts_up_and_remains_visible()
await _test_player_runtime_construction()
@@ -177,7 +178,7 @@ func _test_explanation_highlighting_has_no_status_heading() -> void:
player.free()
func _test_resolved_followup_has_no_header_or_verdict_prefix() -> void:
func _test_resolved_followup_stays_expanded_and_toggles() -> void:
var player = load("res://scripts/ui/question_player.gd").new()
var panel := PanelContainer.new()
var content := VBoxContainer.new()
@@ -191,17 +192,38 @@ func _test_resolved_followup_has_no_header_or_verdict_prefix() -> void:
player.presented_choices = [{"id": "B", "text": "Wrong answer"}]
player.followup_states["B"] = {
"choice_id": "B", "choice_text": "Wrong answer", "prompt": "Why?",
"selected_option_text": "Finding", "was_correct": false,
"teaching_point": "Incorrect. Review the finding.", "resolved": true
"choices": [{"id": "A", "text": "Selected finding"}, {"id": "B", "text": "Correct finding"}],
"correct_choice_id": "B", "selected_option_id": "A",
"selected_option_text": "A. Selected finding", "was_correct": false,
"teaching_point": "Incorrect. Review the finding.", "resolved": true, "collapsed": false,
"has_authored_choices": true
}
player._render_followup_state("B")
_assert(content.find_child("FollowupToggle", true, false) == null, "a resolved follow-up should not render a collapse toggle")
var explanation_text := ""
for child in content.get_children():
if child is Label:
explanation_text += child.text
_assert(not explanation_text.contains("Why this is"), "a resolved follow-up should not repeat a 'Why this is...' heading")
_assert(not explanation_text.begins_with("Incorrect."), "a resolved follow-up explanation should not repeat the verdict prefix")
var toggle := content.find_child("FollowupToggle", true, false) as Button
_assert(toggle != null, "a resolved follow-up should render a minimize toggle")
_assert(toggle != null and toggle.text == "Minimize", "a newly resolved follow-up should remain expanded")
_assert(content.find_child("FollowupPrompt", true, false) != null, "the resolved follow-up should retain its prompt")
_assert(content.find_child("FollowupOption_A", true, false) != null, "the resolved follow-up should retain its selected option")
_assert(content.find_child("FollowupOption_B", true, false) != null, "the resolved follow-up should retain its correct option")
var verdict := content.find_child("FollowupVerdict", true, false) as Label
_assert(verdict != null and verdict.text == "Incorrect", "the resolved follow-up should show whether the answer was correct")
if toggle != null:
toggle.pressed.emit()
_assert(bool(player.followup_states["B"].get("collapsed", false)), "the minimize toggle should collapse the follow-up")
_assert(content.find_child("FollowupPrompt", true, false) == null, "a collapsed follow-up should hide its prompt")
toggle = content.find_child("FollowupToggle", true, false) as Button
_assert(toggle != null and toggle.text == "Expand", "a collapsed follow-up should render an expand toggle")
if toggle != null:
toggle.pressed.emit()
_assert(not bool(player.followup_states["B"].get("collapsed", true)), "the expand toggle should restore the follow-up")
_assert(content.find_child("FollowupPrompt", true, false) != null, "expanding should restore the follow-up prompt")
var correct_state: Dictionary = player.followup_states["B"]
correct_state["selected_option_id"] = "B"
correct_state["was_correct"] = true
player.followup_states["B"] = correct_state
player._render_followup_state("B")
verdict = content.find_child("FollowupVerdict", true, false) as Label
_assert(verdict != null and verdict.text == "Correct", "a correct follow-up response should show a correct verdict")
player.choice_buttons[0].free()
player.choice_percent_labels[0].free()
player.choice_top_labels[0].free()
@@ -355,6 +377,20 @@ func _test_revisitable_navigation_prefers_unanswered_questions() -> void:
session.current_index = prior_index
func _test_timer_text_avoids_redundant_frame_updates() -> void:
var player = load("res://scripts/ui/question_player.gd").new()
player.timer_label = Label.new()
player._set_timer_display(65)
_assert(player.timer_label.text == "01:05", "timer should render a changed second")
player.timer_label.text = "unchanged"
player._set_timer_display(65)
_assert(player.timer_label.text == "unchanged", "timer should not rewrite its label every frame")
player._set_timer_display(66)
_assert(player.timer_label.text == "01:06", "timer should render the next changed second")
player.timer_label.free()
player.free()
func _test_player_runtime_construction() -> void:
var host := Control.new()
root.add_child(host)