Three test suites covering ImplantNavStack, ImplantRegistry, and ImplantApp lifecycle: - test_implant_nav_stack.gd: 26 tests — push/pop/replace/reset, push_default, is_empty, current/current_payload, signal emission, re-entrancy guard, stack-floor-to-default-on-last-pop. - test_implant_registry.gd: 21 tests — _is_valid_manifest validation, lazy scan flag, get_manifests / get_resolved_mode both trigger scan, cache on second call, real scan finds atlas and economics, no duplicate keys, _MODE_MAP coverage, get_app_instance null before instantiate_all, CURRENT_SCHEMA_VERSION = 1. - test_implant_app_lifecycle.gd: 18 tests — nav created in _ready, starts hidden, open FULLSCREEN/INSERT makes visible, GAMEPLAY/wrong path doesn't open, nav non-empty on open, close hides, app-switching closes active, preserves_state true/false, on_insert_deactivated gated on INSERT (closes) vs FULLSCREEN (no-op), register_screen adds hidden child, duplicate id does not overwrite (first-wins). Two team-lead fix-ups before commit (Stig caught the class_name parse-order issue but used the wrong gdUnit4 hook names): - before_each/after_each renamed to before_test/after_test per gdUnit4 API. test_game_state.gd's use of before_each appears to work by coincidence (that test resets autoload state rather than constructing objects, so the never-called hook didn't matter); tests that rely on hook-driven setup need the correct names. - test_register_screen_duplicate_id_does_not_overwrite rewritten to assert the actual contract (first-wins on _screens dict + duplicate screen is not reparented) instead of Control.visible default, which defaults to true regardless of registration. Also removed a stray client/ui/implant/apps/collision_test/app.tres fixture left over from Hoshe's earlier manual collision-warning verification. It was untracked and would have blocked atlas from registering at runtime (KEY_M collision, collision_test won the scan order). Not committing it. All three suites exit 0, totals 26/26, 42/42, 36/36 passed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
247 lines
6.8 KiB
GDScript
247 lines
6.8 KiB
GDScript
class_name TestImplantNavStack
|
|
extends GdUnitTestSuite
|
|
## Unit tests for ImplantNavStack (#844, D-191, PR #131 item 5).
|
|
## Tests push/pop/replace/reset, signal emission, current state, and re-entrancy guard.
|
|
## All tests are synchronous — ImplantNavStack mutations are synchronous by design.
|
|
|
|
# Untyped — class_name ImplantNavStack not yet registered at test-suite parse time.
|
|
var _nav = null
|
|
var _last_signal_id: String = ""
|
|
var _signal_count: int = 0
|
|
|
|
|
|
func before_test() -> void:
|
|
var NavStack := load("res://ui/implant/implant_nav_stack.gd")
|
|
_nav = NavStack.new()
|
|
add_child(_nav)
|
|
_last_signal_id = ""
|
|
_signal_count = 0
|
|
_nav.screen_changed.connect(_on_screen_changed)
|
|
|
|
|
|
func after_test() -> void:
|
|
_nav.queue_free()
|
|
_nav = null
|
|
|
|
|
|
func _on_screen_changed(id: String) -> void:
|
|
_last_signal_id = id
|
|
_signal_count += 1
|
|
|
|
|
|
# =============================================================================
|
|
# push / current / current_payload
|
|
# =============================================================================
|
|
|
|
|
|
func test_push_sets_current() -> void:
|
|
_nav.push("alpha")
|
|
assert_that(_nav.current()).is_equal("alpha")
|
|
|
|
|
|
func test_push_stacks() -> void:
|
|
_nav.push("alpha")
|
|
_nav.push("beta")
|
|
assert_that(_nav.current()).is_equal("beta")
|
|
|
|
|
|
func test_push_emits_screen_changed() -> void:
|
|
_nav.push("alpha")
|
|
assert_that(_last_signal_id).is_equal("alpha")
|
|
assert_that(_signal_count).is_equal(1)
|
|
|
|
|
|
func test_push_with_payload_accessible_via_current_payload() -> void:
|
|
_nav.push("alpha", {"key": "val"})
|
|
assert_that(_nav.current_payload().get("key", "")).is_equal("val")
|
|
|
|
|
|
func test_push_empty_payload_returns_empty_dict() -> void:
|
|
_nav.push("alpha")
|
|
assert_that(_nav.current_payload().is_empty()).is_true()
|
|
|
|
|
|
func test_push_payload_does_not_bleed_to_next_push() -> void:
|
|
_nav.push("alpha", {"key": "val"})
|
|
_nav.push("beta")
|
|
assert_that(_nav.current_payload().is_empty()).is_true()
|
|
|
|
|
|
# =============================================================================
|
|
# pop
|
|
# =============================================================================
|
|
|
|
|
|
func test_pop_removes_top() -> void:
|
|
_nav.push("alpha")
|
|
_nav.push("beta")
|
|
_nav.pop()
|
|
assert_that(_nav.current()).is_equal("alpha")
|
|
|
|
|
|
func test_pop_emits_screen_changed_to_previous() -> void:
|
|
_nav.push("alpha")
|
|
_nav.push("beta")
|
|
_signal_count = 0
|
|
_nav.pop()
|
|
assert_that(_last_signal_id).is_equal("alpha")
|
|
assert_that(_signal_count).is_equal(1)
|
|
|
|
|
|
func test_pop_to_empty_with_default_pushes_default() -> void:
|
|
# Stack-floor behaviour: pop() never leaves the stack empty when a default
|
|
# screen is set. Internally calls push(_default_screen_id).
|
|
_nav.set_default("home")
|
|
_nav.push("alpha")
|
|
_nav.pop()
|
|
assert_that(_nav.current()).is_equal("home")
|
|
|
|
|
|
func test_pop_to_empty_without_default_empties_stack() -> void:
|
|
_nav.push("alpha")
|
|
_nav.pop()
|
|
assert_that(_nav.is_empty()).is_true()
|
|
|
|
|
|
func test_pop_on_empty_stack_does_not_crash() -> void:
|
|
# Empty pop emits a warning and emits no signal.
|
|
_nav.pop()
|
|
assert_that(_signal_count).is_equal(0)
|
|
assert_that(_nav.is_empty()).is_true()
|
|
|
|
|
|
# =============================================================================
|
|
# replace
|
|
# =============================================================================
|
|
|
|
|
|
func test_replace_swaps_top() -> void:
|
|
_nav.push("alpha")
|
|
_nav.replace("beta")
|
|
assert_that(_nav.current()).is_equal("beta")
|
|
|
|
|
|
func test_replace_does_not_grow_stack() -> void:
|
|
_nav.push("alpha")
|
|
_nav.replace("beta")
|
|
# Stack has only "beta" — popping should empty it (no "alpha" below).
|
|
_nav.pop()
|
|
assert_that(_nav.is_empty()).is_true()
|
|
|
|
|
|
func test_replace_emits_screen_changed() -> void:
|
|
_nav.push("alpha")
|
|
_signal_count = 0
|
|
_nav.replace("beta")
|
|
assert_that(_last_signal_id).is_equal("beta")
|
|
assert_that(_signal_count).is_equal(1)
|
|
|
|
|
|
func test_replace_on_empty_stack_acts_as_push() -> void:
|
|
_nav.replace("alpha")
|
|
assert_that(_nav.current()).is_equal("alpha")
|
|
|
|
|
|
func test_replace_updates_payload() -> void:
|
|
_nav.push("alpha", {"step": 1})
|
|
_nav.replace("beta", {"step": 2})
|
|
assert_that(_nav.current_payload().get("step", 0)).is_equal(2)
|
|
|
|
|
|
# =============================================================================
|
|
# reset_to_default
|
|
# =============================================================================
|
|
|
|
|
|
func test_reset_to_default_clears_stack_and_pushes_default() -> void:
|
|
_nav.set_default("home")
|
|
_nav.push("alpha")
|
|
_nav.push("beta")
|
|
_nav.reset_to_default()
|
|
assert_that(_nav.current()).is_equal("home")
|
|
|
|
|
|
func test_reset_to_default_without_default_empties_stack() -> void:
|
|
_nav.push("alpha")
|
|
_nav.push("beta")
|
|
_nav.reset_to_default()
|
|
assert_that(_nav.is_empty()).is_true()
|
|
|
|
|
|
# =============================================================================
|
|
# push_default
|
|
# =============================================================================
|
|
|
|
|
|
func test_push_default_pushes_the_default_id() -> void:
|
|
_nav.set_default("home")
|
|
_nav.push_default()
|
|
assert_that(_nav.current()).is_equal("home")
|
|
|
|
|
|
func test_push_default_is_noop_if_no_default() -> void:
|
|
_nav.push_default()
|
|
assert_that(_nav.is_empty()).is_true()
|
|
assert_that(_signal_count).is_equal(0)
|
|
|
|
|
|
# =============================================================================
|
|
# is_empty
|
|
# =============================================================================
|
|
|
|
|
|
func test_is_empty_true_at_start() -> void:
|
|
assert_that(_nav.is_empty()).is_true()
|
|
|
|
|
|
func test_is_empty_false_after_push() -> void:
|
|
_nav.push("alpha")
|
|
assert_that(_nav.is_empty()).is_false()
|
|
|
|
|
|
func test_is_empty_true_after_pop_to_bottom() -> void:
|
|
_nav.push("alpha")
|
|
_nav.pop()
|
|
assert_that(_nav.is_empty()).is_true()
|
|
|
|
|
|
# =============================================================================
|
|
# Re-entrancy guard
|
|
# =============================================================================
|
|
|
|
|
|
func test_nested_push_from_signal_handler_is_blocked() -> void:
|
|
# Pushing from within a screen_changed handler must be rejected and must not
|
|
# corrupt the stack.
|
|
var nested_count := 0
|
|
_nav.screen_changed.connect(
|
|
func(_id: String) -> void:
|
|
nested_count += 1
|
|
if nested_count == 1:
|
|
_nav.push("nested") # must be blocked by _mutating guard
|
|
)
|
|
_nav.push("alpha")
|
|
# "nested" must NOT have been pushed — stack top is "alpha".
|
|
assert_that(_nav.current()).is_equal("alpha")
|
|
|
|
|
|
func test_pop_internal_push_of_default_is_not_blocked() -> void:
|
|
# pop() internally calls push(default) when the stack empties. This internal
|
|
# call must NOT be blocked by the re-entrancy guard.
|
|
_nav.set_default("home")
|
|
_nav.push("alpha")
|
|
_nav.pop() # pop alpha → empty → internal push("home")
|
|
assert_that(_nav.current()).is_equal("home")
|
|
|
|
|
|
func test_replace_from_signal_handler_is_blocked() -> void:
|
|
var nested_count := 0
|
|
_nav.screen_changed.connect(
|
|
func(_id: String) -> void:
|
|
nested_count += 1
|
|
if nested_count == 1:
|
|
_nav.replace("nested") # must be blocked
|
|
)
|
|
_nav.push("alpha")
|
|
assert_that(_nav.current()).is_equal("alpha")
|