Files
step3/tests/ccs_cases_2_ui_test.gd
hz 030bcf6d7a
Deploy STEP3 UAT / deploy (push) Canceled after 0s
Fix graphical final orders workflow
2026-08-14 19:31:31 -05:00

499 lines
34 KiB
GDScript

extends SceneTree
var failures := 0
class ScreenHost extends Control:
var shown_screen := ""
func show_screen(screen_name: String, _payload: Variant = null) -> void:
shown_screen = screen_name
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var prior_environment: String = ProjectSettings.get_setting("step3/deployment_environment", "development")
ProjectSettings.set_setting("step3/deployment_environment", "uat")
var main_screen = load("res://scripts/ui/main.gd").new()
root.add_child(main_screen)
await process_frame
var nav_button := main_screen.sidebar.find_child("NavCcsCases2", true, false) as Button
_assert(nav_button != null and nav_button.visible, "UAT should expose a CCS Cases 2 navigation item")
_assert(main_screen.screens.has("ccs_cases_2"), "UAT should register the graphical CCS screen")
_assert(not main_screen._screen_uses_shell("ccs_cases_2"), "CCS Cases 2 should hide the application shell")
main_screen._apply_screen_margins("ccs_cases_2")
_assert(main_screen.screens_margin.get_theme_constant("margin_left") == 0, "fullscreen CCS should remove horizontal screen margins")
_assert(main_screen.screens_margin.get_theme_constant("margin_top") == 0, "fullscreen CCS should remove vertical screen margins")
main_screen.current_screen_name = "ccs_cases_2"
_assert(main_screen._screen_uses_pause_menu("ccs_cases_2"), "CCS Cases 2 should route Escape to the pause menu")
main_screen._toggle_app_menu()
_assert(main_screen.app_menu.visible, "the pause menu should open over CCS Cases 2")
_assert(main_screen.menu_exit_button.visible and main_screen.menu_exit_button.text == "Exit CCS Interview", "the CCS pause menu should expose a clear exit action")
main_screen._close_app_menu()
main_screen.queue_free()
await process_frame
ProjectSettings.set_setting("step3/deployment_environment", prior_environment)
var service = root.get_node("CCSService")
var prior_attempt: Dictionary = service.attempt.duplicate(true)
var prior_cases: Array = service.cases.duplicate(true)
var prior_origin: String = service.launch_origin
var host := ScreenHost.new()
host.size = Vector2(1200.0, 700.0)
root.add_child(host)
var screen = load("res://scripts/ui/ccs_cases_2_screen.gd").new()
screen.main = host
host.add_child(screen)
await process_frame
service.cases = [
{
"externalId": "interactive", "title": "Interactive case", "system": "Endocrine", "difficulty": 2,
"realTimeMinutes": 20, "interactiveInterviewAvailable": true, "graphicalPresentationAvailable": true,
"graphicalPresentation": {"sceneKey": "doctor_office", "patientAssetKey": "patient_44_white_male"}
},
{"externalId": "legacy", "title": "Legacy case", "interactiveInterviewAvailable": false}
]
service.attempt = {}
screen.cases_loaded = true
screen.active_loaded = true
screen._render_state()
await process_frame
_assert(screen.current_view == "case_selection", "CCS Cases 2 should begin with compatible case selection")
_assert(screen.case_list.get_child_count() == 1, "legacy cases should be excluded from graphical selection")
var retired_attempt := {
"caseExternalId": "ccs-gastro-duodenal-ulcer-office-001",
"graphicalPresentation": {"interviewRevision": 1}
}
_assert(screen._is_retired_interview_revision(retired_attempt, {
"id": "fatigue_course", "patientText": "I feel tired.", "choices": []
}), "an older authored interview revision should trigger a restart")
retired_attempt.graphicalPresentation.interviewRevision = 4
_assert(not screen._is_retired_interview_revision(retired_attempt, {}),
"the current authored interview revision should remain active")
service.attempt = _sample_attempt(false)
screen._render_state()
await process_frame
_assert(screen.current_view == "doctor_office", "an active interactive attempt should open the doctor office")
_assert(not screen.active_scene.is_patient_dialogue_visible(), "the opening should wait for a clinician dialogue selection")
_assert(screen.active_scene.choice_container.get_child_count() == 1, "the scene should render server-provided dialogue choices")
_assert(screen.active_scene.examination_launcher.visible, "Examination should be available while questions are visible")
_assert(screen.active_scene.computer_launcher.visible, "the doctor office should expose the simulated computer launcher")
_assert(screen.active_scene.computer_launcher.icon.resource_path.ends_with("monitor.svg"), "the computer launcher should use the monitor icon")
screen.active_scene.computer_launcher.pressed.emit()
_assert(screen.active_scene.is_computer_open(), "the computer launcher should open the simulated desktop")
_assert(not screen.active_scene.computer_launcher.visible, "the launcher should hide while the computer is open")
_assert(screen.active_scene.computer_overlay.get_node("Screen").texture.resource_path.ends_with("computer-base.png"), "the overlay should render the supplied computer base")
_assert(screen.active_scene.computer_overlay.get_node("Screen").stretch_mode == TextureRect.STRETCH_SCALE, "the computer should fill the viewport without side bars")
_assert(screen.active_scene.vitals_launcher.visible, "the computer desktop should expose a Vitals app")
_assert(screen.active_scene.medical_history_launcher.visible, "the computer desktop should expose a Medical History app")
_assert(screen.active_scene.orders_launcher.visible, "the computer desktop should expose an Orders app")
_assert(screen.active_scene.vitals_launcher.text.is_empty() and screen.active_scene.medical_history_launcher.text.is_empty(), "desktop shortcuts should place labels below centered icons")
_assert(screen.active_scene.vitals_launcher.get_node("Label").text == "Vitals" and screen.active_scene.medical_history_launcher.get_node("Label").text == "Medical History", "desktop shortcut labels should remain readable")
_assert(screen.active_scene.vitals_launcher.get_theme_stylebox("normal") is StyleBoxEmpty and screen.active_scene.medical_history_launcher.get_theme_stylebox("normal") is StyleBoxEmpty, "desktop shortcuts should not render permanent button tiles")
_assert(screen.active_scene.medical_history_launcher.position.y > screen.active_scene.vitals_launcher.get_rect().end.y, "desktop shortcuts should form a vertical icon column")
_assert(screen.active_scene.vitals_launcher.icon.get_width() >= 58 and screen.active_scene.medical_history_launcher.icon.get_width() >= 58, "desktop shortcuts should use high-resolution vector imports")
screen.active_scene.orders_launcher.pressed.emit()
_assert(screen.active_scene.orders_window.visible and screen.active_scene.orders_tabs.get_tab_count() == 3, "Orders should open a three-tab clinical workspace")
screen.active_scene._queue_live_order_search("bl")
_assert(not screen.active_scene.order_search_timer.is_stopped(), "order typing should schedule a debounced live search")
screen.active_scene.order_search_timer.stop()
screen.active_scene.current_order_location = "office"
screen.active_scene.render_order_search([
{"code": "cbc", "canonicalName": "Complete blood count", "category": "laboratory", "availableLocations": []},
{"code": "venous_blood_gas", "canonicalName": "Venous blood gas", "category": "laboratory", "availableLocations": ["emergency_department", "icu"]}
])
_assert(screen.active_scene.orders_search_results.get_child_count() == 2, "order search should keep unavailable catalog entries visible")
_assert(not (screen.active_scene.orders_search_results.get_child(0) as Button).disabled, "case-authored orders should remain selectable across performance locations")
_assert((screen.active_scene.orders_search_results.get_child(1) as Button).disabled, "orders not authored for this case should explain their unavailability")
(screen.active_scene.orders_search_results.get_child(0) as Button).pressed.emit()
_assert(screen.active_scene.staged_order_codes == ["cbc"] and not screen.active_scene.orders_confirm_button.disabled, "catalog results should stage for confirmation")
screen.active_scene.configure_orders({
"state": "active", "activeOrderCodes": ["cbc"],
"orderOptions": [{"code": "cbc", "performanceLocation": "outpatient_laboratory", "turnaroundMinutes": 20, "requiresPatientVisit": true}],
"activeOrders": [{"code": "cbc", "name": "Complete blood count", "status": "completed", "performanceLocation": "outpatient_laboratory"}],
"chart": [{"category": "lab", "code": "cbc", "title": "Complete blood count", "text": "Hemoglobin 10.4 g/dL."}]
})
_assert(screen.active_scene.staged_order_codes.is_empty(), "confirmed active orders should leave the staged list")
_assert(screen.active_scene.orders_results.text.contains("Hemoglobin 10.4"), "returned results should render in the Orders results tab")
screen.active_scene.configure_orders({
"state": "active", "activeOrderCodes": ["cbc"], "orderOptions": [], "activeOrders": [],
"chart": [
{"category": "progress", "code": "cbc_completed", "simulatedMinute": 20, "text": "The patient completed Complete blood count at Outpatient Laboratory and returned."},
{"category": "lab", "code": "cbc", "title": "Complete blood count", "text": "Hemoglobin 10.4 g/dL."}
]
})
_assert(screen.active_scene.patient_update_panel.visible and screen.active_scene.patient_update_text.text.contains("returned"), "completed off-site work should produce an in-scene patient update")
screen.active_scene.patient_update_panel.visible = false
screen.active_scene.show_order_action_complete("orders")
_assert(screen.active_scene.orders_tabs.current_tab == 1 and screen.active_scene.orders_status.text.begins_with("Orders confirmed"), "successful submission should explain completion and open Active Orders")
screen.active_scene.show_order_action_complete("advance")
_assert(screen.active_scene.orders_tabs.current_tab == 2, "obtaining results should open the Results tab")
screen.active_scene.close_orders()
screen.active_scene.configure_orders({"state": "final_orders", "activeOrderCodes": [], "activeOrders": [], "chart": [], "finalOrdersDeadlineUtc": "2026-08-14T12:00:00Z"})
_assert(screen.active_scene.is_computer_open() and screen.active_scene.orders_window.visible, "final orders should automatically open the Orders app")
_assert(screen.active_scene.orders_submit_final_button.visible and not screen.active_scene.orders_submit_final_button.disabled \
and not screen.active_scene.orders_advance_button.visible, "final orders should expose an enabled submission while locking time advancement")
_assert(screen.active_scene.orders_status.text.contains("Pending results do not need to return"), "final review should clearly explain pending orders")
_assert(screen.active_scene.close_orders(), "the Orders app should remain closable during final review")
_assert(screen.active_scene.close_computer(), "the computer should remain closable during final review")
screen.active_scene.open_computer()
screen.active_scene.open_orders()
screen.active_scene.final_orders_mode = false
screen.active_scene.close_orders()
screen.active_scene.close_computer()
screen.active_scene.open_computer()
screen.active_scene.vitals_launcher.pressed.emit()
_assert(screen.active_scene.is_vitals_open(), "the Vitals app should open a nurse vital-sign window")
_assert(screen.active_scene.vitals_header.get_node("Title").text == "Vitals", "the vital window should use the concise Vitals heading")
_assert(_vital_values(screen.active_scene) == ["36.8 C (98.2 F)", "76/min", "14/min", "122 mm Hg", "76 mm Hg", "99% on room air"], "the Vitals app should render the active attempt data in clinical order")
_assert(_vital_values_fit(screen.active_scene), "vital values should retain a readable column without character-by-character wrapping")
screen.active_scene.medical_history_launcher.pressed.emit()
_assert(screen.active_scene.is_vitals_open() and screen.active_scene.is_medical_history_open(), "both chart windows should be able to remain open")
_assert(screen.active_scene.medical_history_text.text == "Hyperlipidemia", "Medical History should render the case-authored past medical history")
_assert(screen.active_scene.medical_history_window.size.y <= 350.0, "Medical History should size closely to its tabbed content (size %s, minimum %s)" % [screen.active_scene.medical_history_window.size, screen.active_scene.medical_history_window.get_combined_minimum_size()])
_assert(screen.active_scene.physical_exam_tab_button.text == "Physical Exam", "Medical History should include a Physical Exam tab")
_assert(screen.active_scene.medical_history_exam_text.text == "No physical examinations have been completed.", "Physical Exam should begin with an empty state")
_assert(screen.active_scene.medical_history_exam_text.bbcode_enabled, "Physical Exam headings should render as rich text")
_assert(screen.active_scene.medical_history_exam_text.get_parent() is ScrollContainer, "Physical Exam overflow should use an internal scroller")
_assert(screen.active_scene.vitals_window.size.y <= 410.0, "Vitals should size to its rendered measurement rows")
_assert(screen.active_scene.medical_history_window.z_index > screen.active_scene.vitals_window.z_index, "the most recently opened chart window should come to the front")
var original_history_position: Vector2 = screen.active_scene.medical_history_window.position
screen.active_scene._set_medical_history_window_position(Vector2(90.0, 70.0))
var moved_history_position: Vector2 = screen.active_scene.medical_history_window.position
_assert(not moved_history_position.is_equal_approx(original_history_position) \
and moved_history_position.x >= 0.0 and moved_history_position.y >= 0.0,
"Medical History should use the shared draggable-window behavior")
_assert(screen.close_transient_overlay(), "the first Escape should close the topmost Medical History app")
_assert(not screen.active_scene.is_medical_history_open() and screen.active_scene.is_vitals_open(), "closing Medical History should leave Vitals open")
screen.active_scene._set_vitals_window_position(Vector2(75.0, 55.0))
_assert(screen.active_scene.vitals_window.position.is_equal_approx(Vector2(75.0, 55.0)), "the Vitals window should support repositioning by its header")
screen.active_scene._set_vitals_window_position(Vector2(-500.0, 5000.0))
var max_vitals_position: Vector2 = screen.active_scene.size - screen.active_scene.vitals_window.size
_assert(screen.active_scene.vitals_window.position.is_equal_approx(Vector2(0.0, max_vitals_position.y)), "dragging should keep the Vitals window inside the computer viewport")
_assert(screen.close_transient_overlay(), "the second Escape should close the Vitals app")
_assert(not screen.active_scene.is_vitals_open() and screen.active_scene.is_computer_open(), "closing Vitals should return to the computer desktop")
_assert(screen.close_transient_overlay(), "the third Escape should close the computer")
_assert(not screen.active_scene.is_computer_open(), "closing the transient overlay should restore the doctor office")
_assert(not screen.close_transient_overlay(), "the fourth Escape should be available to open the pause menu")
screen.active_scene.open_computer()
screen.active_scene.open_vitals()
screen.active_scene.open_medical_history()
screen.active_scene.computer_close_button.pressed.emit()
_assert(not screen.active_scene.is_computer_open() and not screen.active_scene.is_vitals_open() \
and not screen.active_scene.is_medical_history_open(),
"the computer close button should close every child chart window")
_assert(is_equal_approx(screen.DESIGN_SIZE.x / screen.DESIGN_SIZE.y, 16.0 / 9.0), "the graphical CCS stage should use a true 16:9 design canvas")
_assert(screen.stage_root.size.is_equal_approx(screen.size), "the graphical stage should fill the available viewport")
_assert(screen.stage_root.position.is_equal_approx(Vector2.ZERO), "the graphical stage should not introduce letterbox margins")
var stale_graphical_attempt := _sample_attempt(false)
stale_graphical_attempt.experienceMode = "graphical_interview"
stale_graphical_attempt.openingCompleted = true
service.attempt = stale_graphical_attempt
screen._render_state()
await process_frame
_assert(screen.current_view == "doctor_office", "legacy completion flags should not block a graphical interview")
service.attempt = _sample_branching_attempt()
screen._render_state()
await process_frame
_assert(screen.active_scene.is_patient_dialogue_visible(), "a selected topic should display the patient's response")
_assert(not screen.active_scene.examination_launcher.visible, "Examination should be unavailable while the patient is speaking")
_assert(not screen.active_scene.choice_container.visible, "the next choices should wait until the patient finishes speaking")
_assert(screen.active_scene.dialogue_pages.size() > 1, "long patient responses should split into readable dialogue pages")
_assert(_label_content_fits(screen.active_scene.dialogue_text), "the first patient response page should fit inside its speech bubble")
_assert(is_equal_approx(screen.active_scene.dialogue_text.offset_left, 96.0), "patient dialogue should retain the increased left inset")
_assert(_bubble_matches_content(screen.active_scene), "the speech bubble should size itself to the wrapped response page")
_assert(screen.active_scene.dialogue_continue_hint.visible, "the continuation prompt should remain visible while text is typing")
_assert(screen.active_scene.dialogue_continue_hint.text == "Click to continue", "every dialogue page should use supported continuation wording")
_assert(screen.active_scene.dialogue_text.visible_characters_behavior == TextServer.VC_CHARS_AFTER_SHAPING, "typewriter text should wrap before glyphs are revealed")
var wrapped_line_count: int = screen.active_scene.dialogue_text.get_line_count()
screen.active_scene.open_computer()
var computer_blocked_page: int = screen.active_scene.dialogue_page_index
var computer_click := _left_click(screen.active_scene.get_global_rect().get_center())
screen.active_scene._input(computer_click)
_assert(screen.active_scene.dialogue_page_index == computer_blocked_page and not screen.active_scene.dialogue_reveal_complete, "the open computer should block scene-wide dialogue clicks")
screen.active_scene.computer_close_button.pressed.emit()
_assert(not screen.active_scene.is_computer_open(), "the computer close button should return to the interview")
var outside_click := _left_click(screen.active_scene.get_global_rect().end + Vector2(50.0, 50.0))
screen.active_scene._input(outside_click)
_assert(not screen.active_scene.dialogue_reveal_complete, "clicks outside the doctor scene should not affect dialogue")
var scene_click := _left_click(screen.active_scene.get_global_rect().get_center())
screen.set_pause_menu_open(true)
screen.active_scene._input(scene_click)
_assert(not screen.active_scene.dialogue_reveal_complete, "dialogue should ignore scene clicks while the Escape menu is open")
screen.set_pause_menu_open(false)
screen.active_scene._input(scene_click)
_assert(screen.active_scene.dialogue_page_index == 0, "clicking during typing should complete rather than skip the current page")
_assert(screen.active_scene.dialogue_text.get_line_count() == wrapped_line_count, "revealing typewriter text should not shift words between lines")
_assert(screen.active_scene.dialogue_continue_hint.visible, "completed dialogue pages should invite the learner to continue")
await create_timer(1.35).timeout
_assert(screen.active_scene.is_patient_dialogue_visible(), "patient dialogue should remain until the learner advances it")
_assert(not screen.active_scene.choice_container.visible, "questions must not appear on a timer")
while screen.active_scene.dialogue_page_index < screen.active_scene.dialogue_pages.size() - 1:
screen.active_scene._input(scene_click)
await process_frame
_assert(_label_content_fits(screen.active_scene.dialogue_text), "every patient response page should fit inside its speech bubble")
screen.active_scene.complete_dialogue_reveal()
screen.active_scene._input(scene_click)
await process_frame
_assert(not screen.active_scene.is_patient_dialogue_visible(), "questions should replace patient dialogue after the final learner click")
_assert(screen.active_scene.choice_container.get_child_count() == 4, "the first question page should contain four choices")
screen.active_scene._input(scene_click)
_assert(screen.active_scene.choice_container.get_child_count() == 4, "scene clicks should not affect explicit question controls")
_assert(screen.active_scene.choice_pager.visible, "eight choices should display paging controls")
_assert(screen.active_scene.choice_page == 0, "new question sets should begin on the first page")
_assert(_visible_choice_ids(screen.active_scene) == ["topic_0", "topic_1", "topic_2", "topic_3"], "the first page should preserve choice order and IDs")
_assert(_choice_labels_fit(screen.active_scene), "first-page question text should fit within every button")
screen.active_scene._change_choice_page(1)
await process_frame
_assert(screen.active_scene.choice_page == 1, "Next should advance to the second question page")
_assert(screen.active_scene.choice_container.get_child_count() == 4, "the second question page should contain the remaining choices")
_assert(_visible_choice_ids(screen.active_scene) == ["topic_4", "topic_5", "topic_6", "topic_7"], "the second page should preserve choice order and IDs")
_assert(_choice_labels_fit(screen.active_scene), "second-page question text should fit within every button")
screen.active_scene._change_choice_page(-1)
_assert(screen.active_scene.choice_page == 0, "Previous should return to the first question page")
service.attempt = _sample_attempt(true)
screen._on_attempt_changed()
await process_frame
_assert(screen.active_scene.dialogue_text.text == "Okay, doctor.", "attempt updates should advance the rendered patient response")
_assert(not screen.active_scene.choice_container.visible, "the terminal action should wait for the closing response")
screen.active_scene.complete_dialogue_reveal()
screen.active_scene.advance_dialogue()
_assert(screen.active_scene.choice_container.get_child_count() == 1, "terminal dialogue should expose the management handoff")
_assert(not screen.active_scene.choice_pager.visible, "a single terminal action should not show paging controls")
var terminal_choice := screen.active_scene.choice_container.get_child(0) as Control
_assert(terminal_choice.size.x <= 360.0, "a single terminal choice should remain a compact centered card")
_assert((terminal_choice.get_child(0) as Label).text == "Review Examinations",
"terminal dialogue should lead into the graphical physical examination")
terminal_choice.pressed.emit()
await process_frame
_assert(screen.current_view == "doctor_office", "the examination should remain inside the active interview")
_assert(screen.active_scene.is_physical_exam_visible(), "the physical examination panel should be visible")
_assert(screen.active_scene.physical_exam_grid.columns == 2, "exam components should use two responsive columns")
_assert(screen.active_scene.physical_exam_grid.get_child_count() == 12, "all twelve examination components should be available")
_assert(not screen.active_scene.physical_exam_finish_button.disabled, "Finish Case should remain available before every exam is complete")
screen.active_scene.set_physical_exam_pending("abdomen")
screen.pending_action = "physical_exam"
screen.pending_exam_code = "abdomen"
_assert(screen.active_scene.physical_exam_finish_button.disabled, "case completion should pause while an exam command is pending")
_assert(screen.active_scene.physical_exam_buttons.values().all(func(button): return button.disabled),
"exam controls should disable while one component is submitted")
var examined_attempt := _sample_attempt(true)
examined_attempt.chart = [{"code": "abdomen", "title": "Abdomen", "text": "Mild epigastric tenderness."}]
service.attempt = examined_attempt
screen._on_attempt_changed()
await process_frame
_assert(screen.current_view == "doctor_office", "attempt refreshes should preserve the interview phase")
_assert(not screen.active_scene.is_patient_dialogue_visible(), "an examination result should not replay the current patient dialogue")
_assert(screen.active_scene.physical_exam_finding_text.text == "Mild epigastric tenderness.", "the returned chart finding should be displayed")
_assert(screen.active_scene.physical_exam_result_button.visible, "a completed exam should present a focused result card")
var result_dialogue_page: int = screen.active_scene.dialogue_page_index
screen.active_scene._input(_left_click(screen.active_scene.physical_exam_result_button.get_global_rect().get_center()))
_assert(screen.active_scene.physical_exam_result_button.visible \
and screen.active_scene.dialogue_page_index == result_dialogue_page,
"scene-wide dialogue input should not consume clicks while an examination result is open")
screen.active_scene.physical_exam_result_button.pressed.emit()
_assert(screen.active_scene.is_physical_exam_visible(), "terminal exam results should return to the exam selector")
screen.active_scene.open_computer()
screen.active_scene.open_medical_history()
screen.active_scene.physical_exam_tab_button.pressed.emit()
_assert(screen.active_scene.medical_history_exam_text.text.contains("Mild epigastric tenderness."), "completed findings should be stored in the Physical Exam tab")
_assert(screen.active_scene.medical_history_window.size.y <= 650.0, "the Physical Exam chart should remain bounded while findings scroll")
service.attempt = {"id": "classic-attempt", "state": "active", "openingCompleted": false, "interview": {}}
host.shown_screen = ""
screen._on_attempt_changed()
await process_frame
_assert(screen.current_view == "case_selection", "unsupported active attempts should remain outside the classic player")
_assert(host.shown_screen.is_empty(), "CCS Cases 2 should not trigger classic opening overlays")
service.attempt = _sample_graphical_debrief()
screen._on_attempt_changed()
await process_frame
_assert(screen.current_view == "interview_debrief", "a completed graphical interview should remain in CCS Cases 2")
_assert(screen.active_scene.find_child("InterviewDebrief", true, false) != null, "the graphical case should render its debrief")
_assert(not screen.active_scene.computer_launcher.visible, "the computer launcher should stay hidden during the interview debrief")
var sizing_scene = load("res://scenes/ccs_cases_2/doctor_office.tscn").instantiate()
host.add_child(sizing_scene)
await process_frame
sizing_scene.show_patient_dialogue("Short response.")
await process_frame
var short_height: float = sizing_scene.dialogue_bubble.size.y
_assert(is_equal_approx(short_height, sizing_scene.DIALOGUE_BUBBLE_MIN_HEIGHT), "short dialogue should use the minimum bubble height")
_assert(sizing_scene.dialogue_bubble.get_global_rect().encloses(sizing_scene.dialogue_continue_hint.get_global_rect()), "the continuation hint should fit inside a minimum-height bubble")
_assert(sizing_scene.dialogue_continue_hint.position.y - sizing_scene.dialogue_text.get_rect().end.y >= 8.0, "the continuation hint should have visible padding above it")
sizing_scene.show_patient_dialogue("Line one explains the symptom.\nLine two adds timing.\nLine three adds context.\nLine four concludes.")
await process_frame
_assert(sizing_scene.dialogue_bubble.size.y > short_height, "multi-line dialogue should increase the bubble height")
_assert(sizing_scene.dialogue_bubble.size.y <= sizing_scene.DIALOGUE_BUBBLE_MAX_HEIGHT, "dialogue bubble growth should remain bounded")
_assert(_bubble_matches_content(sizing_scene), "the expanded bubble should contain all wrapped text")
sizing_scene.complete_dialogue_reveal()
_assert(sizing_scene.dialogue_bubble.get_global_rect().encloses(sizing_scene.dialogue_continue_hint.get_global_rect()), "the continuation hint should remain inside the resized bubble")
sizing_scene.queue_free()
screen.on_hide()
await process_frame
_assert(screen.active_scene == null, "hiding CCS Cases 2 should release the doctor scene")
host.queue_free()
service.attempt = prior_attempt
service.cases = prior_cases
service.launch_origin = prior_origin
await process_frame
quit(1 if failures > 0 else 0)
func _sample_attempt(completed: bool) -> Dictionary:
var node := {
"id": "done" if completed else "greeting",
"patientText": "Okay, doctor." if completed else "Hello, doctor.",
"isTerminal": completed,
"suppressPatientText": not completed,
"choices": [] if completed else [{"id": "ask_reason", "text": "What brings you in?", "nextNodeId": "done"}]
}
return {
"id": "attempt-1", "state": "active", "revision": 1,
"simulatedStartUtc": "2026-08-05T09:00:00Z", "simulatedMinute": 0,
"graphicalPresentation": {"sceneKey": "doctor_office", "patientAssetKey": "patient_44_white_male", "interviewRevision": 4},
"medicalHistory": "Hyperlipidemia",
"vitals": {
"temperature": "36.8 C (98.2 F)", "bloodPressure": "122/76 mm Hg",
"pulse": "76/min", "respirations": "14/min", "oxygenSaturation": "99% on room air"
},
"openingCompleted": completed,
"experienceMode": "graphical_interview",
"availableExams": _sample_exams(), "chart": [],
"orderOptions": [{"code": "cbc", "performanceLocation": "outpatient_laboratory", "turnaroundMinutes": 20, "requiresPatientVisit": true}],
"interview": {"currentNodeId": node.id, "currentNode": node, "visitedChoiceIds": [], "completed": completed}
}
func _sample_exams() -> Array:
var definitions := [
["physical_appearance", "Physical Appearance"], ["skin", "Skin"], ["breasts", "Breasts"],
["lymph_nodes", "Lymph Nodes"], ["heent_neck", "HEENT / Neck"], ["chest_lungs", "Chest / Lungs"],
["heart_cardiovascular", "Heart / Cardiovascular"], ["abdomen", "Abdomen"], ["genitalia", "Genitalia"],
["rectal", "Rectal"], ["extremities_spine", "Extremities / Spine"], ["neuro_psych", "Neuro / Psych"]
]
var exams: Array = []
for definition in definitions:
exams.append({"code": definition[0], "name": definition[1], "durationMinutes": 1})
return exams
func _sample_graphical_debrief() -> Dictionary:
return {
"id": "attempt-2", "caseExternalId": "interactive", "state": "completed", "revision": 7,
"experienceMode": "graphical_interview", "simulatedStartUtc": "2026-08-05T09:00:00Z", "simulatedMinute": 0,
"graphicalPresentation": {"sceneKey": "doctor_office", "patientAssetKey": "patient_44_white_male"},
"interviewDebrief": {
"likelyDiagnosis": "Probable duodenal ulcer", "summary": "A focused history supports the assessment.",
"teachingPoints": ["Characterize dizziness.", "Screen for bleeding."]
},
"score": {"percent": 85.0}
}
func _sample_branching_attempt() -> Dictionary:
var option_texts := [
"Tell me more about the dizziness. Does the room spin, and have you fainted?",
"How has the fatigue affected you, and how have you been sleeping and eating?",
"You mentioned your stomach. Please describe the discomfort and when it occurs.",
"Have you had black stools, blood in vomit or stool, trouble swallowing, persistent vomiting, weight loss, or severe sudden pain?",
"What medicines do you take, including aspirin, ibuprofen, naproxen, or other over-the-counter products?",
"Do you have prior stomach problems, other medical conditions, surgeries, or medication allergies?",
"Do you smoke, drink alcohol, or use recreational drugs?",
"Any fever, shortness of breath, headaches, bowel changes, or urinary symptoms?"
]
var choices: Array = []
for index in 8:
choices.append({"id": "topic_%d" % index, "text": option_texts[index], "nextNodeId": "topic_%d" % index})
return {
"id": "attempt-1", "state": "active", "revision": 2,
"simulatedStartUtc": "2026-08-05T09:00:00Z", "simulatedMinute": 0,
"graphicalPresentation": {"sceneKey": "doctor_office", "patientAssetKey": "patient_44_white_male"},
"openingCompleted": false,
"interview": {
"currentNodeId": "chief_concern",
"currentNode": {
"id": "chief_concern",
"patientText": "For the past few weeks, I have felt worn out, and every so often I get lightheaded—especially when I stand up quickly.",
"isTerminal": false,
"choices": choices
},
"visitedChoiceIds": ["chief_concern"], "completed": false
}
}
func _visible_choice_ids(scene: Control) -> Array:
var ids: Array = []
for child in scene.choice_container.get_children():
ids.append(str(child.get_meta("choice_id", "")))
return ids
func _vital_values(scene: Control) -> Array:
var values: Array = []
for index in range(1, scene.vitals_rows.get_child_count(), 2):
values.append(scene.vitals_rows.get_child(index).text)
return values
func _vital_values_fit(scene: Control) -> bool:
for index in range(1, scene.vitals_rows.get_child_count(), 2):
var label := scene.vitals_rows.get_child(index) as Label
if label == null or label.size.x < 220.0 or label.get_line_count() != 1:
return false
return true
func _choice_labels_fit(scene: Control) -> bool:
for button in scene.choice_container.get_children():
var label := button.get_child(0) as Label
if label == null or not _label_content_fits(label):
return false
if not scene.choice_area.get_global_rect().encloses(button.get_global_rect()):
return false
return true
func _label_content_fits(label: Control) -> bool:
if label is RichTextLabel:
return label.get_content_height() <= label.size.y + 0.5
var plain_label := label as Label
if plain_label == null:
return false
var font := plain_label.get_theme_font("font")
var font_size := plain_label.get_theme_font_size("font_size")
var content_height := float(plain_label.get_line_count()) * font.get_height(font_size)
return content_height <= plain_label.size.y + 0.5
func _bubble_matches_content(scene: Control) -> bool:
var required_height: float = scene.dialogue_text.get_content_height() + scene.DIALOGUE_BUBBLE_VERTICAL_CHROME
var expected_height := clampf(
required_height, scene.DIALOGUE_BUBBLE_MIN_HEIGHT, scene.DIALOGUE_BUBBLE_MAX_HEIGHT)
return is_equal_approx(scene.dialogue_bubble.size.y, expected_height) \
and _label_content_fits(scene.dialogue_text)
func _left_click(position: Vector2) -> InputEventMouseButton:
var event := InputEventMouseButton.new()
event.button_index = MOUSE_BUTTON_LEFT
event.pressed = true
event.position = position
return event
func _assert(condition: bool, message: String) -> void:
if condition:
return
failures += 1
push_error(message)