Files
settled-reach/client/tests/test_implant_nav_stack.gd
T
jpmschweitzerandClaude Opus 4.6 a8321702c3 test(ui): PR #131 review — implant primitive tests (item 5)
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>
2026-04-19 15:36:04 +02:00

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")