Add exam-style question calculator

This commit is contained in:
hz
2026-07-23 05:19:12 -05:00
parent ba155fd38a
commit 50f07a77a5
8 changed files with 673 additions and 4 deletions
+126
View File
@@ -0,0 +1,126 @@
extends SceneTree
var failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_test_basic_arithmetic()
_test_decimal_and_chained_operations()
_test_percent_and_sign()
_test_square_root()
_test_repeated_equals()
_test_backspace_and_clear()
_test_error_recovery()
await _test_runtime_panel_behavior()
quit(1 if failures > 0 else 0)
func _calculator():
return load("res://scripts/ui/calculator_panel.gd").new()
func _press(calculator, tokens: Array) -> void:
for token in tokens:
calculator.press_token(str(token))
func _test_basic_arithmetic() -> void:
var calculator = _calculator()
_press(calculator, ["9", "+", "6", "="])
_assert(calculator.get_display_text() == "15", "calculator should add operands")
calculator.reset_calculation()
_press(calculator, ["9", "−", "4", "="])
_assert(calculator.get_display_text() == "5", "calculator should subtract operands")
calculator.reset_calculation()
_press(calculator, ["7", "×", "8", "="])
_assert(calculator.get_display_text() == "56", "calculator should multiply operands")
calculator.reset_calculation()
_press(calculator, ["8", "÷", "4", "="])
_assert(calculator.get_display_text() == "2", "calculator should divide operands")
calculator.free()
func _test_decimal_and_chained_operations() -> void:
var calculator = _calculator()
_press(calculator, ["1", ".", "5", "+", "2", ".", "5", "×", "2", "="])
_assert(calculator.get_display_text() == "8", "calculator should evaluate chained handheld operations left to right")
calculator.free()
func _test_percent_and_sign() -> void:
var calculator = _calculator()
_press(calculator, ["5", "%"])
_assert(calculator.get_display_text() == "0.05", "percent should convert the displayed operand to a fraction")
_press(calculator, ["±"])
_assert(calculator.get_display_text() == "-0.05", "sign toggle should negate the displayed operand")
calculator.free()
func _test_square_root() -> void:
var calculator = _calculator()
_press(calculator, ["8", "1", "√"])
_assert(calculator.get_display_text() == "9", "square root should replace the displayed operand")
calculator.free()
func _test_repeated_equals() -> void:
var calculator = _calculator()
_press(calculator, ["5", "+", "2", "=", "="])
_assert(calculator.get_display_text() == "9", "repeated equals should replay the last binary operation")
calculator.free()
func _test_backspace_and_clear() -> void:
var calculator = _calculator()
_press(calculator, ["1", "2", "3", "⌫"])
_assert(calculator.get_display_text() == "12", "backspace should remove the final entry digit")
_press(calculator, ["AC"])
_assert(calculator.get_display_text() == "0", "clear should reset the display")
calculator.free()
func _test_error_recovery() -> void:
var calculator = _calculator()
_press(calculator, ["8", "÷", "0", "="])
_assert(calculator.get_display_text() == "Error", "division by zero should display an error")
_press(calculator, ["4"])
_assert(calculator.get_display_text() == "4", "digit entry should recover from an error")
_press(calculator, ["±", "√"])
_assert(calculator.get_display_text() == "Error", "square root of a negative operand should display an error")
calculator.free()
func _test_runtime_panel_behavior() -> void:
var host := Control.new()
host.size = Vector2(360, 640)
root.add_child(host)
var calculator = _calculator()
host.add_child(calculator)
await process_frame
calculator.open_panel()
await process_frame
_assert(calculator.visible, "calculator should open as a floating panel")
_assert(calculator.size.x <= 336.0, "calculator should fit a narrow web viewport")
_press(calculator, ["2", "+", "3", "="])
calculator.close_panel()
calculator.open_panel()
_assert(calculator.get_display_text() == "5", "closing and reopening should preserve the active-session value")
calculator.position = Vector2(1000, 1000)
calculator._clamp_to_viewport()
_assert(calculator.position.x + calculator.size.x <= calculator.get_viewport_rect().size.x, "calculator dragging should remain horizontally bounded")
_assert(calculator.position.y + calculator.size.y <= calculator.get_viewport_rect().size.y, "calculator dragging should remain vertically bounded")
calculator.reset_calculation()
_assert(calculator.get_display_text() == "0", "session reset should clear calculator state")
host.queue_free()
await process_frame
func _assert(condition: bool, message: String) -> void:
if condition:
return
failures += 1
push_error(message)
+1
View File
@@ -0,0 +1 @@
uid://j08gujwjb7jx
+9 -3
View File
@@ -361,6 +361,8 @@ func _test_player_runtime_construction() -> void:
_assert(player.clear_highlight_button.icon != null, "clear highlights should use the supplied toolbar artwork")
_assert(player.report_button.icon != null, "report question should use the supplied toolbar artwork")
_assert(player.labs_button.icon != null, "lab results should use the supplied toolbar artwork")
_assert(player.calculator_button != null and player.calculator_button.icon != null, "every question mode should expose the calculator tool")
_assert(player.calculator_panel != null, "question player should construct the floating calculator once")
_assert(player.notes_button != null and player.notes_button.icon != null, "practice questions should expose the notes tool")
for icon_button in [player.clear_highlight_button, player.report_button, player.labs_button]:
_assert(icon_button.alignment == HORIZONTAL_ALIGNMENT_CENTER and icon_button.icon_alignment == HORIZONTAL_ALIGNMENT_CENTER, "question toolbar artwork should be horizontally centered")
@@ -377,12 +379,16 @@ func _test_player_runtime_construction() -> void:
_assert(player.choice_badges.size() == 5, "question player should construct an answer badge for every visible choice slot")
_assert(player.choice_followup_panels[2].get_parent() == player.choice_rows[2], "distractor remediation should remain nested inside its answer choice")
var prior_session_kind: String = root.get_node("BlockSessionService").session_kind
root.get_node("BlockSessionService").session_kind = "simulation"
player._refresh_toolbar_state()
_assert(not player.notes_button.visible, "timed simulations should hide the notes tool")
for session_kind in ["block", "core", "bookmark", "review", "simulation"]:
root.get_node("BlockSessionService").session_kind = session_kind
player._refresh_toolbar_state()
_assert(player.calculator_button.visible, "%s question sessions should expose the calculator tool" % session_kind)
root.get_node("BlockSessionService").session_kind = "core"
player._refresh_toolbar_state()
_assert(player.notes_button.visible, "practice question runs should show the notes tool")
root.get_node("BlockSessionService").session_kind = "simulation"
player._refresh_toolbar_state()
_assert(not player.notes_button.visible, "timed simulations should hide the notes tool")
root.get_node("BlockSessionService").session_kind = prior_session_kind
player.choice_followup_panels[2].visible = true
player.locked_wrong_choice_ids = ["C"]