diff --git a/client/tests/test_monologue_display.gd b/client/tests/test_monologue_display.gd index 2370e449a..8b1c7a07c 100644 --- a/client/tests/test_monologue_display.gd +++ b/client/tests/test_monologue_display.gd @@ -27,6 +27,22 @@ func _label_text(d: Node) -> String: return (slot_node.get_child(0) as RichTextLabel).text +# --------------------------------------------------------------------------- +# Lifecycle — ensure clean GameState between every test +# --------------------------------------------------------------------------- + +func before_test() -> void: + ## Reset GameState fields touched by this suite so tests don't bleed into each other. + ## lattice_profile: tests that care about colour set it explicitly — default to baseline. + ## current_monologue: GameState integration tests need null as start state. + GameState.current_monologue = null + GameState.lattice_profile = "lattice_baseline" + +func after_test() -> void: + GameState.current_monologue = null + GameState.lattice_profile = "lattice_baseline" + + # --------------------------------------------------------------------------- # Initial state # --------------------------------------------------------------------------- @@ -370,3 +386,80 @@ func test_expire_timer_decrements_in_process() -> void: d._process(1.5) assert_float(d._visible[0].expire_timer).is_less(10.0) d.queue_free() + + +# --------------------------------------------------------------------------- +# GameState integration — current_monologue field (v5, #414) +# These tests do not instantiate the scene — they verify apply_snapshot() +# correctly populates and clears current_monologue so the display layer +# receives valid data (or null) every tick. +# +# Joint test plan (sprint-14/joint.md §Test Plan Alignment): +# "Verify current_monologue: None produces no display (no ghost text from previous tick)" +# --------------------------------------------------------------------------- + +func test_gamestate_monologue_null_when_snapshot_omits_field() -> void: + ## D-016: Snapshot without current_monologue must clear the field. + ## Prevents a stale line from a previous tick persisting as ghost text. + GameState.current_monologue = {"id": "stale", "text": "Old.", "duration_seconds": 3.0} + GameState.apply_snapshot({"tick": 2}) + assert_that(GameState.current_monologue).is_null() + + +func test_gamestate_monologue_set_from_snapshot() -> void: + ## apply_snapshot populates current_monologue when the field is a dict. + GameState.apply_snapshot({ + "tick": 1, + "current_monologue": {"id": "m001", "text": "A thought.", "duration_seconds": 5.0}, + }) + assert_that(GameState.current_monologue).is_not_null() + assert_that(GameState.current_monologue.get("text")).is_equal("A thought.") + + +func test_gamestate_monologue_null_when_field_is_non_dict() -> void: + ## Defensive: a non-dictionary value from the server must be rejected (null). + GameState.apply_snapshot({"tick": 1, "current_monologue": "not-a-dict"}) + assert_that(GameState.current_monologue).is_null() + + +func test_gamestate_monologue_duration_seconds_survives_round_trip() -> void: + ## duration_seconds feeds show_monologue()'s duration param — must not be lost. + GameState.apply_snapshot({ + "tick": 1, + "current_monologue": {"id": "m1", "text": "Test.", "duration_seconds": 7.5}, + }) + var dur: float = float(GameState.current_monologue.get("duration_seconds")) + assert_that(dur).is_equal_approx(7.5, 0.001) + + +func test_gamestate_monologue_id_survives_round_trip() -> void: + ## The id field is used by SimBridge carry-forward deduplication (#477). + GameState.apply_snapshot({ + "tick": 1, + "current_monologue": {"id": "enter_cargo_bay_001", "text": "Hmm.", "duration_seconds": 3.0}, + }) + assert_that(GameState.current_monologue.get("id")).is_equal("enter_cargo_bay_001") + + +func test_gamestate_monologue_replaced_by_next_snapshot() -> void: + ## Each snapshot with a monologue replaces the previous value — no accumulation. + GameState.apply_snapshot({ + "tick": 1, + "current_monologue": {"id": "first", "text": "First.", "duration_seconds": 3.0}, + }) + GameState.apply_snapshot({ + "tick": 2, + "current_monologue": {"id": "second", "text": "Second.", "duration_seconds": 3.0}, + }) + assert_that(GameState.current_monologue.get("id")).is_equal("second") + + +# --------------------------------------------------------------------------- +# D-049 Z-layer / canvas scope compliance +# Monologue display must be parented to CANVAS_UI (CanvasLayer 20), not the +# world layer. This constant check ensures it hasn't silently drifted. +# --------------------------------------------------------------------------- + +func test_canvas_ui_constant_is_20() -> void: + ## D-049: CANVAS_UI = 20 is the agreed HUD layer for monologue display. + assert_that(Constants.CANVAS_UI).is_equal(20)