Redesign exam and CCS experiences
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
extends SceneTree
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
||||
|
||||
var failures := 0
|
||||
var ccs_service
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
ccs_service = root.get_node("CCSService")
|
||||
var theme_service = root.get_node("UIThemeService")
|
||||
var prior_dark_mode: bool = theme_service.is_dark_mode()
|
||||
var prior_attempt: Dictionary = ccs_service.attempt.duplicate(true)
|
||||
var prior_orders: Array = ccs_service.orders.duplicate(true)
|
||||
var prior_scale: float = ccs_service.text_scale
|
||||
ccs_service.text_scale = 1.0
|
||||
ccs_service.orders = []
|
||||
ccs_service.attempt = _sample_attempt()
|
||||
|
||||
var host := Control.new()
|
||||
host.size = Vector2(1440, 900)
|
||||
root.add_child(host)
|
||||
var player = load("res://scripts/ui/ccs_player.gd").new()
|
||||
player.main = host
|
||||
player.size = host.size
|
||||
host.add_child(player)
|
||||
await process_frame
|
||||
player._render()
|
||||
await process_frame
|
||||
|
||||
_assert(player.heading.text == "CCS Case | Emergency Department", "CCS header should show the readable location without a case id")
|
||||
_assert(player._ordered_vital_keys(ccs_service.attempt.vitals) == ["temperature", "bloodPressure", "pulse", "respirations", "oxygenSaturation"], "vital chips should use the clinical priority order")
|
||||
_assert(player.vitals_flow.get_child_count() == 5, "every supplied vital should render as a chip")
|
||||
_assert(player.search_button.icon != null, "Order Clerk search should use a bundled icon rather than a font glyph")
|
||||
_assert(player.summary.get_meta("ccs_region", "") == "summary", "summary highlighting should use its own region")
|
||||
_assert(player.history.get_meta("ccs_region", "") == "history", "history highlighting should use its own region")
|
||||
_assert(player.appearance.get_meta("ccs_region", "") == "appearance", "appearance highlighting should use its own region")
|
||||
_assert(player.chart.get_meta("ccs_region", "") == "chart", "chart highlighting should use its own region")
|
||||
_assert(not player.workspace.vertical, "wide CCS screens should use chart and Order Clerk columns")
|
||||
_assert(player.orders.get_child_count() == 1, "active orders should render in the Order Clerk")
|
||||
var active_order_row := player.orders.get_child(0) as HBoxContainer
|
||||
var active_order_label := active_order_row.get_child(0) as Label
|
||||
_assert(active_order_label.text == "Normal Saline Bolus", "internal order codes should be human readable")
|
||||
_assert(player.actions.get_child_count() == 2, "available physical examinations should render as actions")
|
||||
_assert(player.locations.get_child_count() == 2, "allowed locations should render as move actions")
|
||||
_assert(not player.clock.visible, "the normal active-case countdown should remain hidden")
|
||||
|
||||
var clerk_font_size: int = player.search.get_theme_font_size("font_size")
|
||||
var chart_font_size: int = player.summary.get_theme_font_size("normal_font_size")
|
||||
player._zoom(ccs_service.TEXT_SCALE_STEP)
|
||||
_assert(player.summary.get_theme_font_size("normal_font_size") > chart_font_size, "text zoom should enlarge clinical chart text")
|
||||
_assert(player.search.get_theme_font_size("font_size") == clerk_font_size, "text zoom should not resize Order Clerk controls")
|
||||
|
||||
player.size = Vector2(800, 900)
|
||||
player._update_responsive_layout()
|
||||
_assert(player.workspace.vertical, "narrow CCS screens should stack the Order Clerk below the chart")
|
||||
ccs_service.attempt.state = "final_orders"
|
||||
ccs_service.attempt.finalOrdersDeadlineUtc = Time.get_datetime_string_from_unix_time(Time.get_unix_time_from_system() + 90.0, true)
|
||||
player._render()
|
||||
_assert(player.final_orders_notice.visible and player.clock.visible, "final-orders mode should show instructions and its deadline")
|
||||
_assert(not player.time_row.visible, "time advancement should be unavailable during final orders")
|
||||
|
||||
theme_service.set_dark_mode(true)
|
||||
await process_frame
|
||||
theme_service.set_dark_mode(false)
|
||||
Style.refresh_tree(player, true)
|
||||
await process_frame
|
||||
var light_panel := player.chart_panel.get_theme_stylebox("panel") as StyleBoxFlat
|
||||
_assert(light_panel.bg_color.is_equal_approx(Brand.content_card_box(false, false).bg_color), "CCS light styles should win after the shell palette translation")
|
||||
_assert(player.summary.get_theme_color("default_color").is_equal_approx(Brand.text_color(false)), "CCS light chart text should remain readable")
|
||||
theme_service.set_dark_mode(true)
|
||||
Style.refresh_tree(player, false)
|
||||
await process_frame
|
||||
var dark_panel := player.chart_panel.get_theme_stylebox("panel") as StyleBoxFlat
|
||||
_assert(dark_panel.bg_color.is_equal_approx(Brand.content_card_box(true, false).bg_color), "CCS dark styles should win after the shell palette translation")
|
||||
_assert(player.summary.get_theme_color("default_color").is_equal_approx(Brand.text_color(true)), "CCS dark chart text should remain readable")
|
||||
|
||||
theme_service.set_dark_mode(prior_dark_mode)
|
||||
await process_frame
|
||||
host.queue_free()
|
||||
await process_frame
|
||||
ccs_service.attempt = prior_attempt
|
||||
ccs_service.orders = prior_orders
|
||||
ccs_service.text_scale = prior_scale
|
||||
quit(1 if failures > 0 else 0)
|
||||
|
||||
|
||||
func _sample_attempt() -> Dictionary:
|
||||
return {
|
||||
"id": "attempt-1",
|
||||
"state": "active",
|
||||
"location": "emergency_department",
|
||||
"patientSummary": "24-year-old woman with type 1 diabetes mellitus",
|
||||
"reasonForEncounter": "Two days of vomiting and progressive lethargy.",
|
||||
"history": "She stopped insulin when oral intake declined.",
|
||||
"appearance": "She is lethargic but arousable.",
|
||||
"vitals": {
|
||||
"oxygenSaturation": "98% on room air",
|
||||
"pulse": "118/min",
|
||||
"temperature": "37.3 C (99.1 F)",
|
||||
"respirations": "28/min",
|
||||
"bloodPressure": "88/56 mm Hg"
|
||||
},
|
||||
"chart": [{"simulatedMinute": 0, "category": "progress", "text": "Initial evaluation is in progress."}],
|
||||
"activeOrderCodes": ["normal_saline_bolus"],
|
||||
"availableExams": [
|
||||
{"code": "general", "name": "General and hydration examination"},
|
||||
{"code": "neurologic", "name": "Neurologic examination"}
|
||||
],
|
||||
"allowedLocations": ["emergency_department", "icu"],
|
||||
"deadlineUtc": Time.get_datetime_string_from_unix_time(Time.get_unix_time_from_system() + 600.0, true)
|
||||
}
|
||||
|
||||
|
||||
func _assert(condition: bool, message: String) -> void:
|
||||
if condition:
|
||||
return
|
||||
failures += 1
|
||||
push_error(message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://sc7j07qkb1ci
|
||||
@@ -13,6 +13,12 @@ func _initialize() -> void:
|
||||
_assert(history.has_active_simulation({}, {"state": "block_active"}), "a loaded active block should be resumable")
|
||||
_assert(history.has_active_simulation({}, {"state": "between_blocks"}), "a loaded between-block simulation should be resumable")
|
||||
_assert(not history.has_active_simulation({}, {"state": "completed"}), "completed simulations should not appear active")
|
||||
_assert(history.completed_blocks({"state": "block_active", "currentBlock": 3, "totalBlocks": 12}) == 3, "active block progress should count only finished blocks")
|
||||
_assert(history.completed_blocks({"state": "between_blocks", "currentBlock": 3, "totalBlocks": 12}) == 4, "between-block progress should include the submitted block")
|
||||
_assert(history.completed_blocks({"state": "completed", "currentBlock": 11, "totalBlocks": 12}) == 12, "completed simulations should show every block")
|
||||
_assert(history.progress_percent(3, 12) == 25, "progress percentage should use floating-point division")
|
||||
_assert(history.remaining_time_text({"state": "between_blocks", "currentBlock": 2, "totalBlocks": 12}) == "~4h 30m remaining", "remaining time should reflect unstarted blocks")
|
||||
_assert(history.format_datetime_parts({"year": 2026, "month": 7, "day": 16, "hour": 22, "minute": 4}) == "Jul 16, 2026\n10:04 PM", "history dates should use the compact mockup format")
|
||||
quit(1 if failures > 0 else 0)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var service = root.get_node("ExamSimulationService")
|
||||
var theme_service = root.get_node("UIThemeService")
|
||||
var prior_dark_mode: bool = theme_service.is_dark_mode()
|
||||
service.readiness = {"ready": true, "hasActiveSimulation": true, "availableQuestions": 300, "requiredQuestions": 232}
|
||||
service.simulation = {"id": "active", "state": "between_blocks", "currentBlock": 2, "totalBlocks": 12, "createdUtc": "2026-07-16T22:04:00Z"}
|
||||
service.history = [{"id": "completed", "state": "completed", "currentBlock": 11, "totalBlocks": 12, "createdUtc": "2026-07-15T20:35:00Z", "predictedScore": 236, "rangeLow": 221, "rangeHigh": 248, "passProbability": 0.82}]
|
||||
service.history_next_cursor = "next-page"
|
||||
var host := Control.new()
|
||||
host.size = Vector2(1400, 800)
|
||||
root.add_child(host)
|
||||
var screen = load("res://scripts/ui/simulation_history.gd").new()
|
||||
screen.size = host.size
|
||||
host.add_child(screen)
|
||||
await process_frame
|
||||
screen._update_responsive_layout()
|
||||
screen._render_active_section()
|
||||
screen._render_history()
|
||||
await process_frame
|
||||
_assert(screen.history_list.get_child_count() == 1, "history should render completed attempts without duplicating the active simulation")
|
||||
_assert(screen.load_more_button.visible, "history should expose pagination when the API returns a cursor")
|
||||
var active_layout: BoxContainer = screen.find_child("ActiveCardLayout", true, false)
|
||||
_assert(active_layout != null and not active_layout.vertical, "wide exam screens should use the table-oriented layout")
|
||||
theme_service.set_dark_mode(false)
|
||||
await process_frame
|
||||
var light_active_box := screen.active_section.get_theme_stylebox("panel") as StyleBoxFlat
|
||||
_assert(light_active_box.bg_color.is_equal_approx(screen._section_box(false).bg_color), "the Exams page should apply its light surface after a live theme change")
|
||||
theme_service.set_dark_mode(true)
|
||||
await process_frame
|
||||
var dark_active_box := screen.active_section.get_theme_stylebox("panel") as StyleBoxFlat
|
||||
_assert(dark_active_box.bg_color.is_equal_approx(screen._section_box(true).bg_color), "the Exams page should restore its dark surface after a live theme change")
|
||||
screen.size = Vector2(760, 800)
|
||||
screen._update_responsive_layout()
|
||||
await process_frame
|
||||
active_layout = screen.find_child("ActiveCardLayout", true, false)
|
||||
_assert(active_layout != null and active_layout.vertical, "narrow exam screens should stack the active simulation card")
|
||||
_assert(not screen.table_header.visible, "narrow exam screens should replace the table header with history cards")
|
||||
host.queue_free()
|
||||
await process_frame
|
||||
theme_service.set_dark_mode(prior_dark_mode)
|
||||
quit(1 if failures > 0 else 0)
|
||||
|
||||
|
||||
func _assert(condition: bool, message: String) -> void:
|
||||
if condition:
|
||||
return
|
||||
failures += 1
|
||||
push_error(message)
|
||||
@@ -28,6 +28,24 @@ func _initialize() -> void:
|
||||
_assert(light_card.border_color.is_equal_approx(Brand.ROYAL_BLUE), "active light cards should use the primary blue border")
|
||||
var light_field := Brand.field_box(false)
|
||||
_assert(light_field.border_color.is_equal_approx(Brand.LIGHT_BORDER), "light fields should use the semantic cool-gray border")
|
||||
var theme_toggle := Button.new()
|
||||
Brand.set_theme_toggle_icon(theme_toggle, true)
|
||||
_assert(theme_toggle.text.is_empty() and theme_toggle.icon == Brand.SUN_ICON, "dark mode should use the bundled sun icon")
|
||||
Brand.set_theme_toggle_icon(theme_toggle, false)
|
||||
_assert(theme_toggle.text.is_empty() and theme_toggle.icon == Brand.MOON_ICON, "light mode should use the bundled moon icon")
|
||||
var login = load("res://scripts/ui/login_screen.gd").new()
|
||||
var password := LineEdit.new()
|
||||
var password_reveal := Button.new()
|
||||
password.secret = true
|
||||
login._refresh_password_reveal(password, password_reveal)
|
||||
var show_icon := password_reveal.icon
|
||||
password.secret = false
|
||||
login._refresh_password_reveal(password, password_reveal)
|
||||
_assert(show_icon != null and password_reveal.icon != null and show_icon != password_reveal.icon, "password visibility should switch bundled eye icons")
|
||||
password_reveal.free()
|
||||
password.free()
|
||||
login.free()
|
||||
theme_toggle.free()
|
||||
|
||||
Style.set_dark_mode(true)
|
||||
Style.refresh_tree(root, false)
|
||||
|
||||
Reference in New Issue
Block a user