## Anti-tedium regression tests (#494): ## Guard against UI clutter that makes testing tedious. ## ## Test 1: F12 bug report capture — pressing F12 must not crash; when #495 lands, ## the handler should pause, show capture dialog, save 3 files, unpause. ## Test 2: Gauntlet progress UI hidden — in non-Gauntlet mode (no room_id or ## gauntlet_mode flag in snapshot), timer and personal-bests must not appear. ## ## #495 blocked by #481/#490 (server). #496 blocked by #487 (server). ## Tests stub blocked features and verify anti-tedium guards. ## Spec ref: Sprint 9 briefing (client.md), #495, #496. class_name TestAntiTedium extends GdUnitTestSuite var _instance: Node = null func before_test() -> void: SimBridge.reset_test_state() SimBridge._last_snapshot = null GameState.current_tick = 0 GameState.player_position = Vector2.ZERO GameState.visible_entities = [] GameState.visible_tiles = [] GameState.visible_positions = {} GameState.current_monologue = null GameState.current_dialogue = null GameState.game_time = {} GameState.pending_recognitions = [] func after_test() -> void: if _instance and is_instance_valid(_instance): _instance.queue_free() _instance = null # -- Helpers ------------------------------------------------------------------- ## Encode a minimal valid snapshot as MessagePack bytes (same helper as P0 tests). func _make_snapshot_bytes(overrides: Dictionary = {}) -> PackedByteArray: var snapshot := { "tick": overrides.get("tick", 1), "version": Protocol.PROTOCOL_VERSION, "entities": overrides.get("entities", [{ "entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": "Player", "visibility": "Forward", }]), "game_time": { "day": 0, "time_of_day": 0, "day_phase": "Morning", "tick_rate": overrides.get("tick_rate", "Full"), }, "player_facing": "North", "player_stance": "Walk", "player_inventory": [], "visible_tiles": [], "nearby_interactions": [], "pending_recognitions": [], } if overrides.has("current_monologue"): snapshot["current_monologue"] = overrides["current_monologue"] if overrides.has("current_dialogue"): snapshot["current_dialogue"] = overrides["current_dialogue"] # Gauntlet fields: only include if explicitly provided (absence = non-gauntlet) if overrides.has("room_id"): snapshot["room_id"] = overrides["room_id"] if overrides.has("gauntlet_mode"): snapshot["gauntlet_mode"] = overrides["gauntlet_mode"] var result = Messagepack.encode(snapshot) return result.value # -- Test 1: F12 Bug Report Capture ------------------------------------------- # Regression guard: F12 press must not crash or cause unintended side effects. # When #495 lands, this test verifies the full capture flow: # 1. Game pauses (tick_rate → Paused) # 2. Capture dialog appears # 3. Three files saved to tests/bug-reports/YYYY-MM-DD_HH-MM-SS/ # 4. Game unpauses # # Until #495: verifies F12 is inert — no crash, no state corruption. func test_f12_press_no_crash_without_handler() -> void: # Load main scene — full game tree var scene := load("res://scenes/main.tscn") _instance = scene.instantiate() auto_free(_instance) add_child(_instance) # Process one frame to let _ready() and initial snapshot settle. # The test snapshot at tick 1 includes a monologue that gets consumed here. _instance._process(0.016) # Record state AFTER initialization — this is the stable baseline. var tick_rate_before: String = GameState.game_time.get("tick_rate", "Full") var mono_before: Variant = GameState.current_monologue var dialogue_before: Variant = GameState.current_dialogue # Simulate F12 key press via the Godot input system. # InputMapper._unhandled_input() checks action bindings — F12 is not bound # to any action yet, so the event should pass through harmlessly. var event := InputEventKey.new() event.keycode = KEY_F12 event.pressed = true event.key_label = KEY_F12 Input.parse_input_event(event) # Process a frame to let the event propagate _instance._process(0.016) # Assert: no state corruption from unhandled F12 assert_that(GameState.game_time.get("tick_rate", "Full")).override_failure_message( "F12 press should not change tick_rate (no handler yet)" ).is_equal(tick_rate_before) assert_that(GameState.current_monologue).override_failure_message( "F12 press should not spawn a monologue" ).is_equal(mono_before) assert_that(GameState.current_dialogue).override_failure_message( "F12 press should not spawn a dialogue" ).is_equal(dialogue_before) # Release the key var release := InputEventKey.new() release.keycode = KEY_F12 release.pressed = false release.key_label = KEY_F12 Input.parse_input_event(release) func test_f12_does_not_queue_input_action() -> void: # Verify F12 does not produce any action in InputMapper's queue. # When #495 adds the "bug_report" action, this test will be updated # to verify the correct action IS queued. InputMapper.input_queue.clear() var event := InputEventKey.new() event.keycode = KEY_F12 event.pressed = true event.key_label = KEY_F12 Input.parse_input_event(event) # Give InputMapper a frame to process InputMapper._process(0.016) # F12 is not bound to any InputMapper.Action — queue should remain empty assert_that(InputMapper.input_queue.size()).override_failure_message( "F12 should not produce any input action (no binding exists yet)" ).is_equal(0) # Cleanup var release := InputEventKey.new() release.keycode = KEY_F12 release.pressed = false release.key_label = KEY_F12 Input.parse_input_event(release) InputMapper.flush_queue() # -- Test 2: Gauntlet Progress UI Hidden in Non-Gauntlet Mode ----------------- # When #496 lands, it adds a room timer and personal-bests overlay. # Anti-tedium guard: these must NOT be visible in normal (non-Gauntlet) play. # # Current state: no gauntlet UI exists in the scene tree. # This test guards against #496 accidentally showing gauntlet UI in all modes. func test_no_gauntlet_ui_visible_in_default_mode() -> void: # Load main scene — represents non-Gauntlet (default) play mode var scene := load("res://scenes/main.tscn") _instance = scene.instantiate() auto_free(_instance) add_child(_instance) # Process one frame to initialize all children _instance._process(0.016) # Check that no gauntlet-specific UI nodes are visible in the scene tree. # When #496 adds GauntletHUD/RoomTimer/PersonalBests, they must be hidden # by default (only shown when gauntlet_mode is true in the snapshot). var gauntlet_node_names := [ "GauntletHUD", "RoomTimer", "PersonalBests", "GauntletOverlay", "GauntletTimer", "GauntletProgress", ] for node_name in gauntlet_node_names: var node: Node = _find_node_recursive(_instance, node_name) if node != null and node is CanvasItem: assert_that((node as CanvasItem).visible).override_failure_message( "Gauntlet UI node '%s' must not be visible in non-Gauntlet mode" % node_name ).is_false() func test_snapshot_without_room_id_shows_no_gauntlet_ui() -> void: # A snapshot that lacks room_id and gauntlet_mode fields represents # normal play. Apply it and verify no gauntlet state leaks into GameState. var bytes := _make_snapshot_bytes({"tick": 1}) SimBridge.receive_bytes(bytes) var snapshot: Variant = SimBridge._last_snapshot assert_that(snapshot).is_not_null() # Snapshot should NOT contain gauntlet fields assert_that(snapshot.has("room_id")).override_failure_message( "Non-gauntlet snapshot must not contain room_id" ).is_false() assert_that(snapshot.has("gauntlet_mode")).override_failure_message( "Non-gauntlet snapshot must not contain gauntlet_mode" ).is_false() # Apply to GameState — gauntlet-related state should not exist GameState.apply_snapshot(snapshot) # Guard: when #496 adds GameState.room_id / gauntlet_mode properties, # these assertions become falsifiable — they'll catch any code path that # sets gauntlet state from a non-gauntlet snapshot. Currently Object.get() # returns null for nonexistent properties, so this passes trivially until # the properties are defined. assert_that(GameState.get("room_id")).override_failure_message( "GameState.room_id should not exist or be null in non-gauntlet mode" ).is_null() assert_that(GameState.get("gauntlet_mode")).override_failure_message( "GameState.gauntlet_mode should not exist or be null in non-gauntlet mode" ).is_null() func test_gauntlet_ui_stays_hidden_after_multiple_snapshots() -> void: # Simulate several ticks of normal play — gauntlet UI must never appear. var scene := load("res://scenes/main.tscn") _instance = scene.instantiate() auto_free(_instance) add_child(_instance) # Process 5 frames with normal (non-gauntlet) snapshots for tick in range(1, 6): var bytes := _make_snapshot_bytes({"tick": tick}) SimBridge.receive_bytes(bytes) _instance._process(0.016) # After 5 frames, no gauntlet nodes should have appeared var gauntlet_node_names := [ "GauntletHUD", "RoomTimer", "PersonalBests", "GauntletOverlay", "GauntletTimer", "GauntletProgress", ] for node_name in gauntlet_node_names: var node: Node = _find_node_recursive(_instance, node_name) if node != null and node is CanvasItem: assert_that((node as CanvasItem).visible).override_failure_message( "Gauntlet UI '%s' must stay hidden after %d non-gauntlet ticks" % [node_name, 5] ).is_false() # -- Helper: recursive node search -------------------------------------------- func _find_node_recursive(root: Node, target_name: String) -> Node: if root.name == target_name: return root for child in root.get_children(): var found := _find_node_recursive(child, target_name) if found != null: return found return null