- Add SimBridge._last_snapshot = null to P3 before_test() (contamination risk) - Clarify mono.is_visible is custom property, not CanvasItem builtin - Camera smoothing test uses approximate equality instead of exact - Propagate inner _encode_message errors in array/dict encoding - Add explanatory comment on int_64 encoder branch - Document unfalsifiable gauntlet guard as intentional future-proof - Remove loose D-053 citation from LERP_SPEED pin - Assert FogOverlay is sibling of FogGroup under World - Add CANVAS_MODAL=30 (ModalLayer) assertion in P3-Z04 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
266 lines
9.5 KiB
GDScript
266 lines
9.5 KiB
GDScript
## 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
|