Files
settled-reach/client/tests/test_ui_framework_sprint15.gd
T
jpmschweitzerandClaude Opus 4.6 eeb82535f6 feat(ui): minimap rendering with POI dots and border arrows (#151)
Circular 160px diegetic insert overlay on CanvasLayer 10. Nearby
POIs as colored dots (diamond=danger, square=evidence, circle=default),
distant POIs as directional border arrows. Player centered, fixed-north.
Moved from UILayer to InsertOverlay per D-013/D-049.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 02:29:46 +01:00

314 lines
11 KiB
GDScript

## Sprint 15 — Basic UI framework validation tests (#74)
## Validates HUD structure, z-layer hierarchy, insert_active control,
## and monologue display wiring per D-049, D-056, D-057, D-061, OQ-07.
class_name TestUIFrameworkSprint15
extends GdUnitTestSuite
var _instance: Node = null
func before_test() -> void:
SimBridge.reset_test_state()
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.insert_active = true
func after_test() -> void:
if _instance and is_instance_valid(_instance):
_instance.queue_free()
_instance = null
# -------------------------------------------------------------------------
# D-076: Layout constants
# -------------------------------------------------------------------------
func test_dialogue_max_width_set() -> void:
# DIALOGUE_MAX_WIDTH = 1200px (supersedes D-076 640px default per Tyre review).
assert_that(Constants.DIALOGUE_MAX_WIDTH).is_equal(1200)
# -------------------------------------------------------------------------
# D-049: Z-layer scene hierarchy
# -------------------------------------------------------------------------
func test_insert_overlay_is_canvas_layer_10() -> void:
# D-049: InsertOverlay = conceptual layer 6 (insert scope) = CanvasLayer 10.
# Constants.CANVAS_INSERT must match.
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
var insert_overlay: CanvasLayer = _instance.get_node("InsertOverlay")
assert_that(insert_overlay).is_not_null()
assert_that(insert_overlay.layer).is_equal(Constants.CANVAS_INSERT)
func test_ui_layer_is_canvas_layer_20() -> void:
# D-049: UILayer = conceptual layer 7 (UI/monologue scope) = CanvasLayer 20.
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
var ui_layer: CanvasLayer = _instance.get_node("UILayer")
assert_that(ui_layer).is_not_null()
assert_that(ui_layer.layer).is_equal(Constants.CANVAS_UI)
func test_modal_layer_is_canvas_layer_30() -> void:
# D-049: ModalLayer = pause/inventory modal scope = CanvasLayer 30.
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
var modal_layer: CanvasLayer = _instance.get_node("ModalLayer")
assert_that(modal_layer).is_not_null()
assert_that(modal_layer.layer).is_equal(Constants.CANVAS_MODAL)
func test_ui_layer_above_insert_overlay() -> void:
# D-049: UILayer (20) must render above InsertOverlay (10).
assert_that(Constants.CANVAS_UI).is_greater(Constants.CANVAS_INSERT)
func test_modal_layer_above_ui_layer() -> void:
# D-049: ModalLayer (30) must render above UILayer (20).
assert_that(Constants.CANVAS_MODAL).is_greater(Constants.CANVAS_UI)
# -------------------------------------------------------------------------
# D-049: Required nodes exist in correct layers
# -------------------------------------------------------------------------
func test_monologue_display_exists_in_ui_layer() -> void:
# D-049 / #117 / #414: MonologueDisplay must be in UILayer (layer 7).
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
assert_that(_instance.get_node_or_null("UILayer/MonologueDisplay")).is_not_null()
func test_stance_indicator_exists_in_ui_layer() -> void:
# D-053: StanceIndicator must be in UILayer (layer 7), top-right, color-coded.
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
assert_that(_instance.get_node_or_null("UILayer/StanceIndicator")).is_not_null()
func test_minimap_placeholder_exists_in_ui_layer() -> void:
# D-013/D-049: Minimap is on InsertOverlay (z-layer 6), NOT UILayer.
# Sprint 18 #151 (Stig): moved from UILayer to InsertOverlay per D-049 spec.
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
assert_that(_instance.get_node_or_null("InsertOverlay/Minimap")).is_not_null()
func test_hud_exists_in_ui_layer() -> void:
# D-049: HUD must be in UILayer.
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
assert_that(_instance.get_node_or_null("UILayer/HUD")).is_not_null()
func test_interaction_list_exists_in_insert_overlay() -> void:
# D-057: InteractionList must be in InsertOverlay (z-layer 6).
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
assert_that(_instance.get_node_or_null("InsertOverlay/InteractionList")).is_not_null()
func test_dialogue_box_exists_in_insert_overlay() -> void:
# D-061: DialogueBox must be in InsertOverlay (z-layer 6).
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
assert_that(_instance.get_node_or_null("InsertOverlay/DialogueBox")).is_not_null()
func test_world_radial_exists_in_insert_overlay() -> void:
# D-058: WorldRadial must be in InsertOverlay (z-layer 6).
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
assert_that(_instance.get_node_or_null("InsertOverlay/WorldRadial")).is_not_null()
func test_cursor_renderer_exists_in_ui_layer() -> void:
# D-056: CursorRenderer must be in UILayer (topmost, z-layer 7).
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
assert_that(_instance.get_node_or_null("UILayer/CursorRenderer")).is_not_null()
# -------------------------------------------------------------------------
# OQ-07 / D-056: insert_active controls z-layer 6 visibility
# -------------------------------------------------------------------------
func test_gamestate_insert_active_defaults_true() -> void:
# OQ-07: v0.1 characters all have inserts — default true.
assert_that(GameState.insert_active).is_true()
func test_insert_active_propagates_on_process() -> void:
# OQ-07 (#522): After apply_snapshot with insert_active=false,
# the next _process() call must propagate the state to z-layer-6 nodes.
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
# Inject a snapshot with insert_active = false
var snap := SimBridge._test_snapshot()
snap["insert_active"] = false
GameState.apply_snapshot(snap)
assert_that(GameState.insert_active).is_false()
func test_insert_active_true_from_snapshot() -> void:
# OQ-07: Snapshot with insert_active=true keeps GameState in default-on state.
var snap := SimBridge._test_snapshot()
snap["insert_active"] = true
GameState.apply_snapshot(snap)
assert_that(GameState.insert_active).is_true()
func test_insert_active_missing_field_defaults_true() -> void:
# OQ-07: Old servers without insert_active field must not disable the insert.
var snap := SimBridge._test_snapshot()
snap.erase("insert_active")
GameState.apply_snapshot(snap)
assert_that(GameState.insert_active).is_true()
# -------------------------------------------------------------------------
# #241 stub: follow_target_id for entity sprite system
# -------------------------------------------------------------------------
func test_follow_target_id_stub_exists() -> void:
# #72 / #241: follow_target_id stub must exist on GameState with default -1.
# Populated by server ticket #241 (Follow verb) when it lands.
assert_that(GameState.follow_target_id).is_equal(-1)
func test_follow_target_id_is_negative_one_by_default() -> void:
# #72: -1 means "not following" — client #72 checks this for entity highlight.
SimBridge.reset_test_state()
GameState.apply_snapshot(SimBridge._test_snapshot())
# Server doesn't send follow_target_id yet — must stay -1 after snapshot
assert_that(GameState.follow_target_id).is_equal(-1)
# -------------------------------------------------------------------------
# Monologue display wiring (#414)
# -------------------------------------------------------------------------
func test_monologue_display_receives_first_tick_monologue() -> void:
# #414 / #74: MonologueDisplay must show monologue from tick 1 test snapshot.
# Verifies the wiring: GameState.current_monologue → main.gd → MonologueDisplay.
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
# Tick 1 snapshot has a monologue (SimBridge test mode)
# _ready() consumes tick 0 (no monologue). _process() here gets tick 1.
_instance._process(0.016)
# If monologue_display received it, current_monologue is cleared (consume-once)
assert_that(GameState.current_monologue).is_null()
# -------------------------------------------------------------------------
# Regression: Sprint 14 integration proofs (D-030 regression markers)
# -------------------------------------------------------------------------
func test_fog_group_exists_in_world() -> void:
# Sprint 14 regression: fog rendering must still be present after sprint 15 changes.
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
assert_that(_instance.get_node_or_null("World/FogGroup")).is_not_null()
func test_floor_tiles_in_fog_group() -> void:
# Sprint 14 regression: FloorTiles must be in FogGroup (D-049 z:0).
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
assert_that(_instance.get_node_or_null("World/FogGroup/FloorTiles")).is_not_null()
func test_entities_in_ysort_group() -> void:
# Sprint 14 regression: Entities must be in YSortGroup for y-sort ordering (D-049 z:100).
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
assert_that(_instance.get_node_or_null("World/FogGroup/YSortGroup/Entities")).is_not_null()
# -------------------------------------------------------------------------
# #71: Tilemap z-filter — FloorTiles only renders floor level 0
# -------------------------------------------------------------------------
func test_tile_renderer_skips_nonzero_z() -> void:
# #71: Tiles with z != 0 must be filtered out by update_tiles().
var scene := load("res://scenes/main.tscn")
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
var tile_renderer: TileMapLayer = _instance.get_node("World/FogGroup/FloorTiles")
assert_that(tile_renderer).is_not_null()
# Feed tiles at z=0 and z=1
var tiles: Array = [
{"x": 0, "y": 0, "z": 0, "type": "floor"},
{"x": 1, "y": 0, "z": 1, "type": "floor"},
{"x": 2, "y": 0, "z": 0, "type": "wall"},
{"x": 3, "y": 0, "z": 2, "type": "door"},
]
tile_renderer.update_tiles(tiles)
# z=0 tiles should be present
assert_that(tile_renderer.get_cell_source_id(Vector2i(0, 0))).is_not_equal(-1)
assert_that(tile_renderer.get_cell_source_id(Vector2i(2, 0))).is_not_equal(-1)
# z=1 and z=2 tiles should NOT be present (-1 = no cell)
assert_that(tile_renderer.get_cell_source_id(Vector2i(1, 0))).is_equal(-1)
assert_that(tile_renderer.get_cell_source_id(Vector2i(3, 0))).is_equal(-1)