test(ui): GameState integration + D-049 compliance tests for monologue display (#122)

7 tests: snapshot round-trip, null/stale clearing, non-dict rejection,
duration/id preservation, canvas layer constant. before_test/after_test
lifecycle hooks prevent state bleed between test cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 18:43:41 +01:00
co-authored by Claude Opus 4.6
parent b49d78ef6a
commit c71ba8ebb3
+93
View File
@@ -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)