Files
TheTree_mock/tests/ui/test_main_dashboard.gd
T

483 lines
24 KiB
GDScript

extends SceneTree
var _manual_save_path := "user://tests/main_dashboard_%d/manual_ui" % OS.get_process_id()
var _checks := 0
var _failures := 0
var _main: Control
var _preview: WorldPreview
var _clock: Variant
var _state: Variant
var _runtime: Variant
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_clock = root.get_node("GameClock")
_state = root.get_node("GameState")
_runtime = root.get_node("SimulationRuntime")
_clock.reset(1, 6, 0)
_state.reset()
_runtime.initialize(_clock, _state, 424242)
_clock.set_paused(true)
root.size = Vector2i(1440, 810)
_expect(
ProjectSettings.get_setting("display/window/stretch/mode") == "canvas_items"
and ProjectSettings.get_setting("display/window/stretch/aspect") == "expand"
and bool(ProjectSettings.get_setting("display/window/dpi/allow_hidpi")),
"project uses DPI-aware canvas scaling for high-resolution displays"
)
_main = load("res://scenes/main/main.tscn").instantiate() as Control
_main.set("manual_save_path", _manual_save_path)
root.add_child(_main)
await process_frame
await process_frame
_preview = _main.find_child("WorldPreview", true, false) as WorldPreview
_expect(_preview != null, "dashboard creates the live world preview")
var visual_theme := _main.get("visual_theme") as VisualThemeProfile
_expect(
visual_theme != null
and visual_theme.world_background_for_layer(WorldPreview.LayerMode.FRONT) != null
and visual_theme.world_background_for_layer(WorldPreview.LayerMode.BACK) != null,
"replaceable visual theme supplies independent FRONT and BACK art"
)
_expect(
_preview.visual_theme == visual_theme,
"world preview consumes the scene theme instead of hard-coded asset paths"
)
_expect(
visual_theme.facility_icons.size() == 16
and visual_theme.navigation_icons.size() == 6
and visual_theme.resource_icons.size() == 4
and visual_theme.control_icons.size() == 6,
"replaceable visual theme provides complete semantic icon assignments"
)
_expect(
visual_theme.guest_actor_marker(&"mage_elf") != null
and visual_theme.guest_actor_marker(&"legacy_guest") != null
and visual_theme.staff_actor_marker(&"guard") != null
and visual_theme.staff_actor_marker(&"legacy_role") != null,
"visual theme resolves class, role, and fallback actor markers"
)
_expect(
visual_theme.ui_skin_textures.size() == 16
and visual_theme.ui_skin(&"panel_parchment") != null
and visual_theme.ui_skin(&"button_pressed") != null
and visual_theme.ui_skin(&"queue_badge") != null
and visual_theme.ui_skin(&"construction_hatch") != null,
"visual theme provides replaceable panel, button, badge, route, and construction skins"
)
for texture_path: String in [
"res://assets/themes/japanese_fantasy/world-front-v1.png.import",
"res://assets/themes/japanese_fantasy/world-back-v1.png.import",
"res://assets/themes/japanese_fantasy/facility-icons-v2.png.import",
"res://assets/themes/japanese_fantasy/system-icons-v2.png.import",
"res://assets/themes/japanese_fantasy/actor-markers-v2.png.import",
"res://assets/themes/japanese_fantasy/ui-skin-v1.png.import",
]:
var import_config := ConfigFile.new()
_expect(
import_config.load(texture_path) == OK
and bool(import_config.get_value("params", "mipmaps/generate", false)),
"high-resolution texture enables mipmaps: %s" % texture_path.get_file()
)
var operations: Dictionary = _runtime.get_operations_report()
_expect(bool(operations.get(&"enabled", false)), "dashboard enables the production Operations loop")
var summary := _main.get("dashboard_summary_label") as Label
_expect(
summary != null and "行動点" in summary.text and "未払給与" in summary.text,
"dashboard exposes action points and payroll arrears"
)
var palette := _main.find_child("BuildPalette", true, false) as GridContainer
_expect(palette.get_child_count() == 16, "all sixteen MVP facilities appear in the build palette")
var iconized_build_buttons := 0
for build_button_value: Variant in (_main.get("build_buttons") as Dictionary).values():
if (build_button_value as Button).icon != null:
iconized_build_buttons += 1
_expect(iconized_build_buttons == 16, "all facility catalog buttons consume theme-provided icons")
var palette_scroll := _main.find_child("BuildPaletteScroll", true, false) as ScrollContainer
_expect(
palette_scroll != null and palette_scroll.is_ancestor_of(palette),
"construction catalog lives in the persistent bottom palette"
)
var navigation := _main.find_child("NavigationRail", true, false) as Control
_expect(
navigation != null and (_main.get("navigation_buttons") as Dictionary).size() == 6,
"dashboard exposes the six Japanese management categories"
)
var iconized_navigation_buttons := 0
for navigation_button_value: Variant in (_main.get("navigation_buttons") as Dictionary).values():
if (navigation_button_value as Button).icon != null:
iconized_navigation_buttons += 1
_expect(iconized_navigation_buttons == 6, "all management categories consume theme-provided icons")
var resource_icon_nodes := _main.get("resource_icon_nodes") as Dictionary
_expect(
resource_icon_nodes.size() == 4
and (resource_icon_nodes.get(&"gold") as TextureRect).texture != null
and (resource_icon_nodes.get(&"mana") as TextureRect).texture != null,
"top HUD consumes replaceable resource icons"
)
var alert_icon := _main.find_child("AlertIcon", true, false) as TextureRect
_expect(alert_icon != null and alert_icon.texture != null, "top HUD consumes the theme warning icon")
var selected_icon := _main.find_child("SelectedEntityIcon", true, false) as TextureRect
_expect(selected_icon != null, "facility inspector reserves a theme-driven identity icon")
var guest_count := _main.get("guest_count_label") as Label
var lodging_count := _main.get("lodging_count_label") as Label
var alert_count := _main.get("alert_count_label") as Label
_expect(
guest_count != null and "来客" in guest_count.text
and lodging_count != null and "宿泊" in lodging_count.text
and alert_count != null and "警戒" in alert_count.text,
"top HUD keeps visitor, lodging, and alert information visible"
)
_expect((_main.get("build_buttons") as Dictionary).has(&"guest_elevator"), "connector proxy definitions are exposed for vertical-layout prototyping")
_expect((_main.get("build_buttons") as Dictionary).has(&"adult_private_room"), "the full MVP catalog is reachable from the scrolling palette")
await _check_layout(Vector2i(1440, 810))
await _check_layout(Vector2i(1280, 720))
await _check_layout(Vector2i(1920, 1080))
await _check_layout(Vector2i(2560, 1440))
await _check_layout(Vector2i(3840, 2160))
_test_staff_candidate_hiring_ui()
_test_excavation_rank_gate_and_tool_exclusion()
_test_coordinate_round_trip()
_test_build_payload_and_hit_test()
_test_excavation_preview_payload_and_start()
_test_manual_save_and_load()
_test_event_choice_ui()
if is_instance_valid(_main):
_main.queue_free()
SaveService.delete_save(_manual_save_path)
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _check_layout(window_size: Vector2i) -> void:
root.size = window_size
await process_frame
await process_frame
var side := _main.find_child("SidePanel", true, false) as Control
var map_panel := _main.find_child("MapPanel", true, false) as Control
var bottom := _main.find_child("BottomBar", true, false) as Control
var navigation := _main.find_child("NavigationRail", true, false) as Control
var excavation_button := _main.find_child("ExcavationButton", true, false) as Button
_expect(side != null and side.size.x >= 300.0, "%dx%d keeps the operations sidebar readable" % [window_size.x, window_size.y])
_expect(map_panel != null and map_panel.size.x >= 600.0 and map_panel.size.y >= 380.0, "%dx%d keeps a usable map area" % [window_size.x, window_size.y])
_expect(
navigation != null and navigation.position.x < map_panel.position.x and side.position.x > map_panel.position.x,
"%dx%d keeps navigation left and facility inspection right of the living map" % [window_size.x, window_size.y]
)
_expect(bottom != null and bottom.position.y + bottom.size.y <= _main.size.y + 1.0, "%dx%d keeps all controls inside the viewport" % [window_size.x, window_size.y])
_expect(
excavation_button != null
and excavation_button.position.x + excavation_button.size.x <= bottom.size.x + 1.0,
"%dx%d keeps the excavation control inside the bottom bar" % [window_size.x, window_size.y]
)
func _test_staff_candidate_hiring_ui() -> void:
var panel := _main.find_child("StaffCandidatePanel", true, false) as VBoxContainer
var list := _main.find_child("StaffCandidateList", true, false) as VBoxContainer
var toggle := _main.find_child("StaffCandidateToggleButton", true, false) as Button
var buttons := _main.get("staff_candidate_buttons") as Dictionary
_expect(panel != null and not panel.visible, "staff candidate screen starts collapsed")
_expect(list != null and list.get_child_count() == 20, "all twenty validated Staff candidates have UI cards")
_expect(toggle != null and "20" in toggle.text, "collapsed hiring control exposes the available candidate count")
_expect(buttons.size() == 20 and buttons.has(&"staff_akane"), "every candidate has a stable hire control")
var akane_detail := _main.find_child("CandidateDetail_staff_akane", true, false) as Label
_expect(
akane_detail != null
and "アカネ" in akane_detail.text
and "契約 G180" in akane_detail.text
and "日給 G120" in akane_detail.text,
"candidate card shows Japanese identity and both signing and payroll costs"
)
_main.call("_unhandled_key_input", _key_event(KEY_H))
_expect(panel.visible and bool(_main.get("_staff_candidate_panel_open")), "H opens the Staff candidate screen")
var cash_before: int = int(_runtime.economy_service.get_balance(&"PLAYER_CASH"))
_main.call("_hire_staff_candidate", &"staff_akane")
_expect(_runtime.get_hired_staff_candidate_ids().has(&"staff_akane"), "paused UI hire commits the selected canonical candidate immediately")
_expect(_runtime.staff_service.has_staff(&"staff_akane"), "UI hire creates the Staff member through Runtime")
_expect(
_runtime.economy_service.get_balance(&"PLAYER_CASH") == cash_before - 180,
"UI hire charges the catalog signing cost exactly once"
)
var akane_button := buttons.get(&"staff_akane") as Button
_expect(akane_button != null and akane_button.disabled and akane_button.text == "雇用済み", "hired candidate is visibly final and cannot be clicked again")
var after_first_hire: int = int(_runtime.economy_service.get_balance(&"PLAYER_CASH"))
_main.call("_hire_staff_candidate", &"staff_akane")
_expect(
_runtime.economy_service.get_balance(&"PLAYER_CASH") == after_first_hire
and _runtime.get_hired_staff_candidate_ids().count(&"staff_akane") == 1,
"repeated UI hire does not double-charge or duplicate the employee"
)
_clock.set_paused(false)
_main.call("_hire_staff_candidate", &"staff_bardo")
var pending := _runtime.command_queue.create_snapshot().get("pending", []) as Array
_expect(pending.size() == 1, "running-clock Staff hire is queued before mutation")
_main.call("_hire_staff_candidate", &"staff_bardo")
_expect(
(_runtime.command_queue.create_snapshot().get("pending", []) as Array).size() == 1,
"pending Staff hire suppresses repeated clicks"
)
_clock.advance_minutes(1)
_expect(
_runtime.get_hired_staff_candidate_ids().has(&"staff_bardo")
and (_main.get("_pending_hire_commands") as Dictionary).is_empty(),
"queued Staff hire completes and clears its UI pending state"
)
_clock.set_paused(true)
var candidate_summary := _main.find_child("StaffCandidateSummary", true, false) as Label
_expect(candidate_summary != null and "雇用済み 2人" in candidate_summary.text, "candidate screen refreshes its hired count from Runtime state")
_main.set("_hire_sequence", 1)
_main.call("_synchronize_ui_command_state")
_expect(
int(_main.get("_hire_sequence")) >= 3,
"saved Command metadata advances the UI hire-attempt sequence after reconstruction"
)
_main.call("_unhandled_key_input", _key_event(KEY_ESCAPE))
_expect(not panel.visible and not bool(_main.get("_staff_candidate_panel_open")), "Escape closes the Staff candidate screen")
func _test_excavation_rank_gate_and_tool_exclusion() -> void:
_main.call("_unhandled_key_input", _key_event(KEY_E))
var status := _main.get("build_status_label") as Label
var excavation_button := _main.get("excavation_button") as Button
_expect(bool(_main.get("_excavation_mode")), "E enables the explicit excavation mode")
_expect(_preview.excavation_mode, "dashboard and map share the excavation-mode state")
_expect(excavation_button != null and excavation_button.button_pressed, "excavation toggle visibly reflects the keyboard shortcut")
_expect(status != null and "ランク2" in status.text, "Rank 1 excavation gate is explained honestly in Japanese")
_main.call("_select_build_definition", &"reception")
_expect(not bool(_main.get("_excavation_mode")) and not _preview.excavation_mode, "choosing a facility exits excavation mode")
_main.call("_unhandled_key_input", _key_event(KEY_E))
_expect(_main.get("_selected_definition") == null, "entering excavation mode clears the facility build selection")
var reception_button := (_main.get("build_buttons") as Dictionary).get(&"reception") as Button
_expect(reception_button != null and not reception_button.button_pressed, "exclusive excavation mode clears the palette toggle")
_main.call("_unhandled_key_input", _key_event(KEY_ESCAPE))
_expect(not bool(_main.get("_excavation_mode")) and not _preview.excavation_mode, "Escape exits excavation mode")
func _test_coordinate_round_trip() -> void:
_preview.layer_mode = WorldPreview.LayerMode.FRONT
for cell: Vector3i in [Vector3i(-12, 0, 0), Vector3i(0, 3, 0), Vector3i(12, 6, 0)]:
var restored: Variant = _preview.canvas_to_cell(_preview.cell_to_canvas(cell))
_expect(restored == cell, "FRONT cell-to-canvas round trip preserves %s" % cell)
_preview.layer_mode = WorldPreview.LayerMode.BACK
var back_cell := Vector3i(4, 0, 1)
_expect(_preview.canvas_to_cell(_preview.cell_to_canvas(back_cell)) == back_cell, "BACK layer coordinate round trip preserves the layer")
_expect(_preview.canvas_to_cell(Vector2(-10, -10)) == null, "points outside the map do not produce build cells")
func _test_build_payload_and_hit_test() -> void:
_main.call("_select_build_definition", &"reception")
var origin := Vector3i(-6, 0, WorldService.LAYER_FRONT)
var preview_report: Dictionary = _preview.get_build_preview_report(origin)
_expect(bool(preview_report.get(&"can_build", false)), "starter reception shows a valid authoritative placement preview")
var jobs_before: int = _runtime.construction_service.get_jobs().size()
_main.call("_on_map_cell_clicked", origin)
var jobs: Array = _runtime.construction_service.get_jobs()
_expect(jobs.size() == jobs_before + 1, "map click submits one build_facility command")
var job: Variant = jobs.back()
_expect(job.definition_id == &"reception" and job.origin == origin and job.rotation == 0, "build command payload preserves definition, cell, and rotation")
_main.call("_on_map_cell_clicked", origin)
_expect(_runtime.construction_service.get_jobs().size() == jobs.size(), "invalid overlap preview does not submit a second command")
_clock.advance_minutes(120)
var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities()
_expect(facilities.size() == 1, "dashboard-created construction completes in simulation time")
if facilities.is_empty():
return
var instance_id := StringName(facilities[0].get(&"instance_id", &""))
var facility_point := _preview.cell_to_canvas(origin)
_expect(_preview.facility_at_canvas(facility_point) == instance_id, "facility hit test resolves the completed world occupant")
_main.call("_on_facility_selected", instance_id)
var detail := _main.get("selected_detail_label") as Label
_expect(detail != null and "待機列" in detail.text and "配置人員" in detail.text, "facility selection exposes queue, service, and staffing state")
var selected_icon := _main.get("selected_entity_icon") as TextureRect
_expect(selected_icon != null and selected_icon.texture != null, "facility selection exposes its theme icon in the inspector")
func _test_excavation_preview_payload_and_start() -> void:
_expect(_promote_rank_two(), "UI fixture reaches Rank 2 through public progression commands")
_main.call("_set_layer", WorldPreview.LayerMode.FRONT)
_main.call("_set_excavation_mode", true)
var first_cell := Vector3i(0, -1, WorldPreview.LayerMode.FRONT)
_preview.call("_set_hovered_cell", first_cell)
var report := _preview.get_excavation_preview_report(first_cell)
_expect(bool(report.get(&"ok", false)), "underground hover uses the authoritative excavation validator")
var preview_cells: Variant = report.get(&"cells", [])
_expect(
preview_cells is Array
and preview_cells.has(Vector3i(0, -1, WorldPreview.LayerMode.FRONT))
and preview_cells.has(Vector3i(0, -1, WorldPreview.LayerMode.BACK)),
"excavation preview identifies both FRONT and BACK target cells"
)
var status := _main.get("build_status_label") as Label
_expect(
status != null and "掘削可能" in status.text and "費用" in status.text and "所要" in status.text,
"valid excavation preview shows Japanese cost and duration"
)
_main.call("_set_layer", WorldPreview.LayerMode.BACK)
_expect(
(_preview.get("_last_excavation_preview_report") as Dictionary).get(&"ok", false),
"changing layer recomputes the active-layer excavation preview"
)
_main.call("_set_layer", WorldPreview.LayerMode.FRONT)
_preview.call("_set_hovered_cell", first_cell)
var jobs_before: int = _runtime.excavation_service.get_jobs().size()
_clock.set_paused(false)
_main.call("_on_map_cell_clicked", first_cell)
var pending_snapshot: Dictionary = _runtime.command_queue.create_snapshot()
var pending: Array = pending_snapshot.get("pending", [])
_expect(_runtime.excavation_service.get_jobs().size() == jobs_before, "running clock queues the excavation command before mutation")
_expect(pending.size() == 1, "map click queues exactly one start_excavation command")
if not pending.is_empty():
var command: Dictionary = pending.back()
var payload: Variant = command.get("payload", {})
_expect(
StringName(command.get("command_type", "")) == &"start_excavation"
and payload is Dictionary
and payload.get(&"cell", null) == first_cell,
"excavation command payload preserves the hovered layer cell"
)
_expect(
StringName(command.get("idempotency_key", "")) == &"ui.excavation.0.-1.00000001",
"excavation uses a stable per-attempt idempotency key"
)
_expect(status != null and "受理しました" in status.text, "queued excavation reports command acceptance")
_main.call("_on_map_cell_clicked", first_cell)
_expect(
(_runtime.command_queue.create_snapshot().get("pending", []) as Array).size() == 1,
"a repeated click cannot queue a second excavation attempt while the first is pending"
)
_clock.advance_minutes(1)
var first_jobs: Array[Dictionary] = _runtime.excavation_service.get_jobs()
_expect(first_jobs.size() == jobs_before + 1, "queued excavation starts on the next Runtime command phase")
_expect((_main.get("_pending_excavation_commands") as Dictionary).is_empty(), "queued completion clears the UI pending-command entry")
_expect(status != null and "開始しました" in status.text, "queued command completion reports the actual start")
if first_jobs.is_empty():
_clock.set_paused(true)
return
var first_job: Dictionary = first_jobs.back()
_expect(
first_job.get(&"position", Vector2i.ZERO) == Vector2i(0, -1)
and int(first_job.get(&"progress", 0)) > 0,
"started excavation is visible and progresses through simulation time"
)
_clock.set_paused(true)
_clock.advance_minutes(int(first_job.get(&"build_minutes", 0)))
var second_cell := Vector3i(0, -2, WorldPreview.LayerMode.FRONT)
_preview.call("_set_hovered_cell", second_cell)
var second_report := _preview.get_excavation_preview_report(second_cell)
_expect(bool(second_report.get(&"ok", false)), "completed excavation unlocks the next connected underground cell")
var jobs_before_immediate: int = _runtime.excavation_service.get_jobs().size()
_main.call("_on_map_cell_clicked", second_cell)
_expect(
_runtime.excavation_service.get_jobs().size() == jobs_before_immediate + 1,
"paused clock executes an accepted excavation command immediately"
)
var detail := _main.get("selected_detail_label") as Label
_expect(
detail != null and "進捗" in detail.text and "FRONT / 表層・BACK / 裏層" in detail.text,
"started excavation exposes progress and both target layers in selection detail"
)
_main.call("_unhandled_key_input", _key_event(KEY_ESCAPE))
_expect(not bool(_main.get("_excavation_mode")), "Escape still exits excavation mode after starting a job")
func _promote_rank_two() -> bool:
var reputation: Variant = _runtime.submit_command(
&"update_progression_metrics",
{&"metrics": {&"reputation": 25}},
&"player", &"", 100, -1, &"ui.excavation.rank.reputation"
)
var capital: Variant = _runtime.submit_command(
&"change_resources",
{&"deltas": {&"gold": maxi(10_000 - _runtime.economy_service.get_balance(&"PLAYER_CASH"), 0)}},
&"player", &"", 100, -1, &"ui.excavation.rank.capital"
)
var promoted: Variant = _runtime.submit_command(
&"set_world_tree_rank",
{&"rank": 2},
&"player", &"", 100, -1, &"ui.excavation.rank.promote"
)
return reputation.is_success() and capital.is_success() and promoted.is_success() \
and _state.world_tree_rank == 2
func _test_manual_save_and_load() -> void:
SaveService.delete_save(_manual_save_path)
_main.call("_save_manual")
var status := _main.get("build_status_label") as Label
_expect(status != null and "保存しました" in status.text, "manual save button writes the runtime snapshot")
var expected_gold: int = _state.get_resource(&"gold")
_state.set_resource(&"gold", expected_gold + 7)
_main.call("_load_manual")
_expect(_state.get_resource(&"gold") == expected_gold, "manual load button restores the saved simulation")
_expect(status != null and "読み込みました" in status.text, "manual load reports success in Japanese")
func _test_event_choice_ui() -> void:
var event_service: Variant = _runtime.event_service
var queued: Variant = event_service.enqueue_candidate(
&"relationship.threshold",
[],
_clock.absolute_minute,
{&"deduplication_key": &"ui-event-choice-test"}
)
_expect(queued.is_success(), "UI fixture enqueues a foreground relationship Event")
var started: Variant = event_service.start_next_event(_clock.absolute_minute, {})
_expect(
started.is_success() and bool(started.details.get(&"waiting_for_choice", false)),
"UI fixture starts an Event waiting for a Choice"
)
_clock.set_paused(true)
_main.call("_refresh_dashboard")
var panel := _main.find_child("ForegroundEventPanel", true, false) as VBoxContainer
var choices := _main.find_child("ForegroundEventChoices", true, false) as VBoxContainer
_expect(panel != null and panel.visible, "foreground Event opens the decision panel")
_expect(choices != null and choices.get_child_count() == 2, "all two canonical Event choices are visible")
var pause_button := _main.get("pause_button") as Button
_expect(
pause_button != null and pause_button.disabled and "イベント" in pause_button.text,
"unresolved foreground Event cannot be bypassed with the pause control"
)
if choices == null or choices.get_child_count() != 2:
return
var first_choice := choices.get_child(0) as Button
_expect(
first_choice != null and not first_choice.text.is_empty(),
"Event choice uses a readable Japanese button"
)
first_choice.emit_signal(&"pressed")
_expect(event_service.get_foreground_instance() == null, "choice button resolves the foreground Event exactly once")
_expect(not _clock.paused, "resolved foreground Event releases the simulation pause")
_expect(not panel.visible and choices.get_child_count() == 0, "resolved Event removes the stale choice controls")
_expect(not pause_button.disabled, "resolved Event restores the normal pause control")
_clock.set_paused(true)
func _expect(condition: bool, message: String) -> void:
_checks += 1
if condition:
return
_failures += 1
push_error(message)
func _key_event(keycode: Key) -> InputEventKey:
var event := InputEventKey.new()
event.keycode = keycode
event.pressed = true
return event