extends RefCounted ## Session shell (start menu, pause overlay, settings) and routine-traffic ## despawn. These are headless-shape tests: they instantiate the UI scripts and ## check state transitions rather than rendering. const StartMenu := preload("res://src/game/ui/start_menu.gd") const PauseOverlay := preload("res://src/game/ui/pause_overlay.gd") const SettingsPanel := preload("res://src/game/ui/settings_panel.gd") const NpcAvatar := preload("res://src/game/npc_avatar.gd") func test_start_menu_lists_all_venues() -> bool: var menu: Node = StartMenu.new() # Buttons are created in _ready; build them without a tree by calling _build. menu._build() var ids: Array[String] = [] for venue in StartMenu.VENUES: ids.append(str(venue["id"])) var a := TestUtils.check_eq(ids.size(), 3, "three venues offered") var b := TestUtils.check(ids.has("hollywood_building"), "Hollywood Blvd present") var c := TestUtils.check(ids.has("joke_shop"), "Joke Shop present") var d := TestUtils.check(ids.has("tv_studio"), "TV Studio present") menu.free() return a and b and c and d func test_start_menu_emits_selected_venue() -> bool: var menu: Node = StartMenu.new() menu._build() var got := {"id": ""} menu.venue_selected.connect(func(id): got["id"] = id) menu._on_venue_pressed(1) var a := TestUtils.check_eq(got["id"], "joke_shop", "clicking venue 2 selects the Joke Shop") var b := TestUtils.check(not menu.visible, "menu closes after a venue is picked") menu.free() return a and b func test_start_menu_clamps_focus() -> bool: var menu: Node = StartMenu.new() menu._build() menu._focus_index(-5) var low: int = menu._index menu._focus_index(99) var high: int = menu._index menu.free() return TestUtils.check_eq(low, 0, "focus clamps at the top") \ and TestUtils.check_eq(high, StartMenu.VENUES.size() - 1, "focus clamps at the bottom") func test_pause_overlay_buttons_emit() -> bool: var overlay: Node = PauseOverlay.new() overlay._build() var seen: Array[String] = [] overlay.resume_requested.connect(func(): seen.append("resume")) overlay.restart_requested.connect(func(): seen.append("restart")) overlay.settings_requested.connect(func(): seen.append("settings")) overlay.quit_to_menu_requested.connect(func(): seen.append("menu")) for node_name in ["ResumeButton", "RestartButton", "SettingsButton", "MenuButton"]: var button: Button = overlay.find_child(node_name, true, false) as Button if button: button.pressed.emit() overlay.free() var expected := ["resume", "restart", "settings", "menu"] return TestUtils.check_eq(seen, expected, "pause overlay exposes resume/restart/settings/menu") func test_settings_panel_persists_values() -> bool: var panel: Node = SettingsPanel.new() panel._build() panel.open({"look_sensitivity": 0.0022, "hud_font_scale": 1.0}) panel._on_sensitivity_changed(0.004) panel._on_font_scale_changed(1.3) var applied := {"settings": {}} panel.applied.connect(func(s): applied["settings"] = s) panel._on_apply() var s: Dictionary = applied["settings"] var a := TestUtils.check(is_equal_approx(float(s.get("look_sensitivity", 0.0)), 0.004), "sensitivity applied") var b := TestUtils.check(is_equal_approx(float(s.get("hud_font_scale", 0.0)), 1.3), "font scale applied") var c := TestUtils.check(not panel.visible, "panel closes on apply") # Restore defaults written by the apply call. if Save: Save.save_settings(Save.DEFAULT_SETTINGS) panel.free() return a and b and c func test_settings_panel_cancel_discards() -> bool: var panel: Node = SettingsPanel.new() panel._build() panel.open({"look_sensitivity": 0.0022, "hud_font_scale": 1.0}) panel._on_sensitivity_changed(0.005) var applied := {"count": 0} panel.applied.connect(func(_s): applied["count"] += 1) panel._on_cancel() var a := TestUtils.check(not panel.visible, "cancel closes the panel") var b := TestUtils.check_eq(applied["count"], 0, "cancel does not apply") panel.free() return a and b func test_routine_traffic_is_ephemeral() -> bool: var avatar: Node = NpcAvatar.new() var a := TestUtils.check(avatar.ephemeral, "avatars default to ephemeral") var b := TestUtils.check(avatar.lifetime_s > 0.0, "ephemeral traffic has a lifetime") avatar.free() return a and b