diff --git a/assets/icons/lucide/calculator.svg b/assets/icons/lucide/calculator.svg new file mode 100644 index 0000000..6cffcf7 --- /dev/null +++ b/assets/icons/lucide/calculator.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/lucide/calculator.svg.import b/assets/icons/lucide/calculator.svg.import new file mode 100644 index 0000000..74f4cf4 --- /dev/null +++ b/assets/icons/lucide/calculator.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dy8lr6snglfpm" +path="res://.godot/imported/calculator.svg-0f64f216f45259cc59d80d23ea9a79a8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/lucide/calculator.svg" +dest_files=["res://.godot/imported/calculator.svg-0f64f216f45259cc59d80d23ea9a79a8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/scripts/ui/calculator_panel.gd b/scripts/ui/calculator_panel.gd new file mode 100644 index 0000000..ece38f6 --- /dev/null +++ b/scripts/ui/calculator_panel.gd @@ -0,0 +1,448 @@ +extends PanelContainer + +const Style := preload("res://scripts/ui/ui_style.gd") +const Brand := preload("res://scripts/ui/medmind_style.gd") +const MAX_ENTRY_LENGTH := 16 + +var display_label: Label +var operation_label: Label +var keypad_buttons: Array[Button] = [] +var entry := "0" +var accumulator := 0.0 +var pending_operator := "" +var waiting_for_operand := true +var last_operator := "" +var last_operand := 0.0 +var has_last_operation := false +var error_state := false +var drag_active := false +var drag_offset := Vector2.ZERO +var has_been_positioned := false + + +func _ready() -> void: + name = "CalculatorPanel" + visible = false + top_level = true + mouse_filter = Control.MOUSE_FILTER_STOP + custom_minimum_size = Vector2(310, 390) + size = custom_minimum_size + _build_ui() + if not get_viewport().size_changed.is_connected(_on_viewport_size_changed): + get_viewport().size_changed.connect(_on_viewport_size_changed) + _on_viewport_size_changed() + refresh_theme_controls() + _refresh_display() + + +func open_panel() -> void: + visible = true + move_to_front() + if not has_been_positioned: + var viewport_size := get_viewport_rect().size + position = Vector2(maxf(12.0, viewport_size.x - size.x - 24.0), 72.0) + has_been_positioned = true + _clamp_to_viewport() + + +func close_panel() -> void: + visible = false + drag_active = false + + +func toggle_panel() -> void: + if visible: + close_panel() + else: + open_panel() + + +func reset_calculation() -> void: + entry = "0" + accumulator = 0.0 + pending_operator = "" + waiting_for_operand = true + last_operator = "" + last_operand = 0.0 + has_last_operation = false + error_state = false + _refresh_display() + + +func press_token(token: String) -> void: + if token.length() == 1 and token.is_valid_int(): + _enter_digit(token) + return + match token: + ".": + _enter_decimal() + "+", "−", "×", "÷": + _set_operator(token) + "=": + _equals() + "%": + _apply_unary("percent") + "√": + _apply_unary("sqrt") + "±": + _apply_unary("sign") + "⌫": + _backspace() + "AC": + reset_calculation() + + +func get_display_text() -> String: + return entry + + +func refresh_theme_controls() -> void: + if display_label == null: + return + var dark := UIThemeService.is_dark_mode() + add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE)) + display_label.add_theme_color_override("font_color", Brand.text_color(dark)) + operation_label.add_theme_color_override("font_color", Brand.muted_color(dark)) + for button in keypad_buttons: + var accent := Brand.ROYAL_BLUE if str(button.get_meta("calculator_token", "")) in ["+", "−", "×", "÷", "="] else Brand.SLATE + button.add_theme_stylebox_override("normal", Brand.badge_box(dark, accent)) + button.add_theme_stylebox_override("hover", Brand.badge_box(dark, Brand.TEAL)) + button.add_theme_stylebox_override("pressed", Brand.badge_box(dark, Brand.PURPLE)) + button.add_theme_color_override("font_color", Brand.text_color(dark)) + + +func _build_ui() -> void: + var margin := MarginContainer.new() + margin.add_theme_constant_override("margin_left", 10) + margin.add_theme_constant_override("margin_right", 10) + margin.add_theme_constant_override("margin_top", 12) + margin.add_theme_constant_override("margin_bottom", 14) + add_child(margin) + + var content := VBoxContainer.new() + content.add_theme_constant_override("separation", 10) + margin.add_child(content) + + var title_bar := HBoxContainer.new() + title_bar.name = "CalculatorTitleBar" + title_bar.mouse_filter = Control.MOUSE_FILTER_STOP + title_bar.custom_minimum_size.y = 34 + title_bar.gui_input.connect(_on_title_bar_gui_input) + content.add_child(title_bar) + + var title := Label.new() + title.text = "Calculator" + title.size_flags_horizontal = Control.SIZE_EXPAND_FILL + title.mouse_filter = Control.MOUSE_FILTER_IGNORE + Brand.apply_font(title) + title.add_theme_font_size_override("font_size", 18) + title_bar.add_child(title) + + var close := Button.new() + close.name = "CalculatorClose" + close.text = "×" + close.tooltip_text = "Close calculator" + close.custom_minimum_size = Vector2(36, 32) + close.focus_mode = Control.FOCUS_NONE + close.pressed.connect(close_panel) + title_bar.add_child(close) + + var display_panel := PanelContainer.new() + display_panel.custom_minimum_size.y = 78 + display_panel.add_theme_stylebox_override("panel", Style.panel_box(Color("101b31"), Style.CYAN)) + content.add_child(display_panel) + + var display_margin := MarginContainer.new() + display_margin.add_theme_constant_override("margin_left", 12) + display_margin.add_theme_constant_override("margin_right", 12) + display_margin.add_theme_constant_override("margin_top", 8) + display_margin.add_theme_constant_override("margin_bottom", 8) + display_panel.add_child(display_margin) + + var display_stack := VBoxContainer.new() + display_margin.add_child(display_stack) + operation_label = Label.new() + operation_label.name = "CalculatorOperation" + operation_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + operation_label.add_theme_font_size_override("font_size", 13) + Brand.apply_font(operation_label) + display_stack.add_child(operation_label) + display_label = Label.new() + display_label.name = "CalculatorDisplay" + display_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + display_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + display_label.add_theme_font_size_override("font_size", 28) + Brand.apply_font(display_label) + display_stack.add_child(display_label) + + var keypad := GridContainer.new() + keypad.name = "CalculatorKeypad" + keypad.columns = 4 + keypad.add_theme_constant_override("h_separation", 6) + keypad.add_theme_constant_override("v_separation", 8) + content.add_child(keypad) + + var tokens := [ + "AC", "⌫", "±", "√", + "7", "8", "9", "÷", + "4", "5", "6", "×", + "1", "2", "3", "−", + "%", "0", ".", "+", + "", "", "", "=" + ] + for token in tokens: + var button := Button.new() + button.custom_minimum_size = Vector2(44, 45) + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + button.focus_mode = Control.FOCUS_NONE + if token == "": + button.visible = false + keypad.add_child(button) + continue + button.text = token + button.set_meta("calculator_token", token) + button.tooltip_text = _token_tooltip(token) + button.add_theme_font_size_override("font_size", 18) + Brand.apply_font(button) + UISoundService.set_button_sound(button, "toggle") + button.pressed.connect(press_token.bind(token)) + keypad.add_child(button) + keypad_buttons.append(button) + + +func _input(event: InputEvent) -> void: + if not visible or not (event is InputEventKey): + return + var key_event := event as InputEventKey + if not key_event.pressed or key_event.echo: + return + var token := "" + if key_event.keycode == KEY_ESCAPE: + close_panel() + get_viewport().set_input_as_handled() + return + if key_event.keycode in [KEY_ENTER, KEY_KP_ENTER]: + token = "=" + elif key_event.keycode == KEY_BACKSPACE: + token = "⌫" + elif key_event.keycode == KEY_DELETE: + token = "AC" + elif key_event.unicode > 0: + var character := String.chr(key_event.unicode) + match character: + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "%", "=": + token = character + "+": token = "+" + "-": token = "−" + "*", "x", "X": token = "×" + "/": token = "÷" + "c", "C": token = "AC" + if token == "": + return + press_token(token) + get_viewport().set_input_as_handled() + + +func _enter_digit(digit: String) -> void: + if error_state: + reset_calculation() + if waiting_for_operand: + entry = digit + waiting_for_operand = false + elif entry == "0": + entry = digit + elif entry.length() < MAX_ENTRY_LENGTH: + entry += digit + _refresh_display() + + +func _enter_decimal() -> void: + if error_state: + reset_calculation() + if waiting_for_operand: + entry = "0." + waiting_for_operand = false + elif not "." in entry and entry.length() < MAX_ENTRY_LENGTH: + entry += "." + _refresh_display() + + +func _set_operator(operator: String) -> void: + if error_state: + return + var current := _entry_value() + if pending_operator != "" and not waiting_for_operand: + if not _perform_operation(pending_operator, current): + return + else: + accumulator = current + pending_operator = operator + waiting_for_operand = true + has_last_operation = false + _refresh_display() + + +func _equals() -> void: + if error_state: + return + if pending_operator != "": + var operand := accumulator if waiting_for_operand else _entry_value() + var operator := pending_operator + if not _perform_operation(operator, operand): + return + last_operator = operator + last_operand = operand + has_last_operation = true + pending_operator = "" + elif has_last_operation: + if not _perform_operation(last_operator, last_operand): + return + else: + return + waiting_for_operand = true + _refresh_display() + + +func _apply_unary(operation: String) -> void: + if error_state: + return + var value := _entry_value() + match operation: + "percent": + value /= 100.0 + "sqrt": + if value < 0.0: + _set_error() + return + value = sqrt(value) + "sign": + value = -value + entry = _format_number(value) + if pending_operator == "": + accumulator = value + waiting_for_operand = false + _refresh_display() + + +func _backspace() -> void: + if error_state: + reset_calculation() + return + if waiting_for_operand: + return + entry = entry.left(entry.length() - 1) + if entry in ["", "-"]: + entry = "0" + waiting_for_operand = true + _refresh_display() + + +func _perform_operation(operator: String, operand: float) -> bool: + var result := accumulator + match operator: + "+": result += operand + "−": result -= operand + "×": result *= operand + "÷": + if is_zero_approx(operand): + _set_error() + return false + result /= operand + if is_nan(result) or is_inf(result): + _set_error() + return false + accumulator = result + entry = _format_number(result) + return true + + +func _entry_value() -> float: + return entry.to_float() + + +func _format_number(value: float) -> String: + if is_zero_approx(value): + return "0" + var formatted := String.num(value, 12) + if "." in formatted: + while formatted.ends_with("0"): + formatted = formatted.left(formatted.length() - 1) + if formatted.ends_with("."): + formatted = formatted.left(formatted.length() - 1) + if formatted.length() > MAX_ENTRY_LENGTH: + formatted = String.num_scientific(value).replace("e+", "e") + return formatted + + +func _set_error() -> void: + entry = "Error" + error_state = true + pending_operator = "" + waiting_for_operand = true + has_last_operation = false + _refresh_display() + + +func _refresh_display() -> void: + if display_label == null: + return + display_label.text = entry + operation_label.text = ("%s %s" % [_format_number(accumulator), pending_operator]) if pending_operator != "" else "" + + +func _on_title_bar_gui_input(event: InputEvent) -> void: + var touch_event := event as InputEventScreenTouch + if touch_event != null: + drag_active = touch_event.pressed + if drag_active: + drag_offset = touch_event.position + get_viewport().set_input_as_handled() + return + var touch_drag := event as InputEventScreenDrag + if touch_drag != null and drag_active: + global_position += touch_drag.relative + _clamp_to_viewport() + get_viewport().set_input_as_handled() + return + var mouse_event := event as InputEventMouseButton + if mouse_event != null and mouse_event.button_index == MOUSE_BUTTON_LEFT: + drag_active = mouse_event.pressed + if drag_active: + drag_offset = mouse_event.global_position - global_position + get_viewport().set_input_as_handled() + return + var motion := event as InputEventMouseMotion + if motion != null and drag_active: + global_position = motion.global_position - drag_offset + _clamp_to_viewport() + get_viewport().set_input_as_handled() + + +func _on_viewport_size_changed() -> void: + var viewport_size := get_viewport_rect().size + custom_minimum_size.x = minf(310.0, maxf(216.0, viewport_size.x - 24.0)) + size.x = custom_minimum_size.x + _clamp_to_viewport() + + +func _clamp_to_viewport() -> void: + if not is_inside_tree(): + return + var viewport_size := get_viewport_rect().size + position.x = clampf(position.x, 12.0, maxf(12.0, viewport_size.x - size.x - 12.0)) + position.y = clampf(position.y, 12.0, maxf(12.0, viewport_size.y - size.y - 12.0)) + + +func _token_tooltip(token: String) -> String: + return { + "AC": "Clear calculation", + "⌫": "Backspace", + "±": "Change sign", + "√": "Square root", + "%": "Convert to percent", + "÷": "Divide", + "×": "Multiply", + "−": "Subtract", + "+": "Add", + "=": "Equals" + }.get(token, token) diff --git a/scripts/ui/calculator_panel.gd.uid b/scripts/ui/calculator_panel.gd.uid new file mode 100644 index 0000000..cb25d08 --- /dev/null +++ b/scripts/ui/calculator_panel.gd.uid @@ -0,0 +1 @@ +uid://dssyep5a2sime diff --git a/scripts/ui/question_player.gd b/scripts/ui/question_player.gd index b5a889f..faa274c 100644 --- a/scripts/ui/question_player.gd +++ b/scripts/ui/question_player.gd @@ -3,6 +3,7 @@ extends VBoxContainer const Style := preload("res://scripts/ui/ui_style.gd") const Brand := preload("res://scripts/ui/medmind_style.gd") const FlashcardPrefillDialog := preload("res://scripts/ui/flashcard_prefill_dialog.gd") +const CalculatorPanel := preload("res://scripts/ui/calculator_panel.gd") const CLEAR_HIGHLIGHT_DARK_ICON := preload("res://assets/icons/medmind/clear-highlight-dark.svg") const CLEAR_HIGHLIGHT_LIGHT_ICON := preload("res://assets/icons/medmind/clear-highlight-light.svg") const LABS_DARK_ICON := preload("res://assets/icons/medmind/labs-dark.svg") @@ -81,6 +82,7 @@ var skip_button: Button var text_smaller_button: Button var text_larger_button: Button var labs_button: Button +var calculator_button: Button var clear_highlight_button: Button var flag_button: Button var report_button: Button @@ -109,6 +111,7 @@ var feedback_fog: ColorRect var labs_overlay: Control var labs_panel: PanelContainer var labs_content: VBoxContainer +var calculator_panel var report_overlay: Control var report_panel: PanelContainer var report_form_content: VBoxContainer @@ -258,6 +261,19 @@ func _ready() -> void: text_larger_button.pressed.connect(func(): _adjust_question_text_scale(TEXT_SCALE_STEP)) question_toolbar.add_child(text_larger_button) + calculator_button = Button.new() + calculator_button.name = "CalculatorButton" + calculator_button.tooltip_text = "Open calculator" + calculator_button.icon = preload("res://assets/icons/lucide/calculator.svg") + calculator_button.custom_minimum_size = Vector2(44, 44) + calculator_button.add_theme_constant_override("icon_max_width", 22) + calculator_button.alignment = HORIZONTAL_ALIGNMENT_CENTER + calculator_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER + calculator_button.focus_mode = Control.FOCUS_NONE + UISoundService.set_button_sound(calculator_button, "open") + calculator_button.pressed.connect(_toggle_calculator) + question_toolbar.add_child(calculator_button) + labs_button = Button.new() labs_button.tooltip_text = "Review lab results" labs_button.custom_minimum_size = Vector2(44, 44) @@ -574,6 +590,8 @@ func _ready() -> void: add_child(feedback_fog) _build_labs_modal() + calculator_panel = CalculatorPanel.new() + add_child(calculator_panel) _build_report_modal() _build_media_viewer() resized.connect(_fit_modal_panels) @@ -909,7 +927,7 @@ func _apply_question_theme() -> void: var navigator_heading := navigator_popup.find_child("NavigatorHeading", true, false) as Label if navigator_heading != null: navigator_heading.add_theme_color_override("font_color", Brand.text_color(dark)) - for control in [text_smaller_button, text_larger_button, bookmark_button, notes_button, flashcard_button, app_feedback_button]: + for control in [text_smaller_button, text_larger_button, calculator_button, bookmark_button, notes_button, flashcard_button, app_feedback_button]: if control == null: continue var active: bool = control == bookmark_button and BookmarkService.is_bookmarked(str(BlockSessionService.get_current_question().get("id", ""))) @@ -923,6 +941,8 @@ func _apply_question_theme() -> void: _style_asset_toolbar_button(report_button, REPORT_QUESTION_DARK_ICON if dark else REPORT_QUESTION_LIGHT_ICON, dark) _style_asset_toolbar_button(flag_button, FLAG_DARK_ICON if dark else FLAG_LIGHT_ICON, dark, flagged) _style_theme_toggle_button(dark) + if calculator_panel != null: + calculator_panel.refresh_theme_controls() if skip_button != null: skip_button.add_theme_stylebox_override("normal", _toolbar_box(dark)) skip_button.add_theme_stylebox_override("hover", _toolbar_box(dark, true)) @@ -963,6 +983,9 @@ func on_hide() -> void: _hide_media_viewer() _hide_report_modal(true) _hide_labs_modal() + if calculator_panel != null: + calculator_panel.close_panel() + calculator_panel.reset_calculation() func _save_simulation_current_response() -> void: @@ -1049,6 +1072,8 @@ func _input(event: InputEvent) -> void: _hide_labs_modal() get_viewport().set_input_as_handled() return + if calculator_panel != null and calculator_panel.visible and event is InputEventKey: + return if not visible or pending_correct_advance or stem_label == null: return @@ -2038,9 +2063,16 @@ func _refresh_toolbar_state() -> void: var bookmark_value := BookmarkService.is_bookmarked(bookmark_question_id) var bookmark_pending := BookmarkService.is_pending(bookmark_question_id) bookmark_button.tooltip_text = ("Saved; waiting to sync" if bookmark_pending else "Remove bookmark") if bookmark_value else ("Removal waiting to sync" if bookmark_pending else "Save question") + if calculator_button != null: + calculator_button.visible = true _apply_question_theme() +func _toggle_calculator() -> void: + if calculator_panel != null: + calculator_panel.toggle_panel() + + func _toggle_bookmark_current_question() -> void: var question := BlockSessionService.get_current_question() if not question.is_empty(): diff --git a/tests/calculator_panel_test.gd b/tests/calculator_panel_test.gd new file mode 100644 index 0000000..1639aee --- /dev/null +++ b/tests/calculator_panel_test.gd @@ -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) diff --git a/tests/calculator_panel_test.gd.uid b/tests/calculator_panel_test.gd.uid new file mode 100644 index 0000000..65b40de --- /dev/null +++ b/tests/calculator_panel_test.gd.uid @@ -0,0 +1 @@ +uid://j08gujwjb7jx diff --git a/tests/post_answer_review_test.gd b/tests/post_answer_review_test.gd index 4574020..533c317 100644 --- a/tests/post_answer_review_test.gd +++ b/tests/post_answer_review_test.gd @@ -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"]