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>
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
class_name TestImplantAppLifecycle
|
||||
extends GdUnitTestSuite
|
||||
## Lifecycle tests for ImplantApp base class (#844, D-191, PR #131 item 5).
|
||||
## Covers on_install → on_open → on_close hook ordering, nav-stack state at
|
||||
## each hook, preserves_state semantics, on_insert_deactivated gating,
|
||||
## and register_screen / current_screen_id behaviour.
|
||||
|
||||
# Loaded inside method bodies to avoid class_name parse-order trap.
|
||||
const APP_SCRIPT := "res://ui/implant/implant_app.gd"
|
||||
const MANIFEST_SCRIPT := "res://ui/implant/implant_app_manifest.gd"
|
||||
|
||||
const TEST_APP_PATH := "implant/lifecycle_test"
|
||||
|
||||
|
||||
# Returns an ImplantApp instance with a manifest, added to the scene tree.
|
||||
# _ready() fires on add_child, which calls on_install().
|
||||
func _make_app(preserves: bool = true, mode: String = "fullscreen"): # returns ImplantApp (untyped)
|
||||
var ManifestClass := load(MANIFEST_SCRIPT)
|
||||
var m = ManifestClass.new()
|
||||
m.app_path = TEST_APP_PATH
|
||||
m.default_mode = mode
|
||||
m.preserves_state = preserves
|
||||
m.schema_version = 1
|
||||
|
||||
var AppClass := load(APP_SCRIPT)
|
||||
var app = AppClass.new()
|
||||
app.manifest = m
|
||||
add_child(app) # fires _ready() → on_install()
|
||||
return app
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
# Restore HudGroups state — prevents cross-test bleed.
|
||||
HudGroups._active_app = ""
|
||||
HudGroups._active_mode = HudGroups.Mode.GAMEPLAY
|
||||
HudGroups._groups.erase(TEST_APP_PATH)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# on_install — called from _ready()
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_nav_created_after_ready() -> void:
|
||||
# nav is created in _ready() before on_install() fires.
|
||||
var app = _make_app()
|
||||
assert_that(app.nav).override_failure_message(
|
||||
"ImplantApp._ready() must create nav stack"
|
||||
).is_not_null()
|
||||
app.queue_free()
|
||||
|
||||
|
||||
func test_app_starts_invisible() -> void:
|
||||
var app = _make_app()
|
||||
assert_that(app.visible).override_failure_message(
|
||||
"ImplantApp must start hidden (visible = false in _ready)"
|
||||
).is_false()
|
||||
app.queue_free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# on_open — triggered via _internal_app_changed
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_open_fullscreen_makes_app_visible() -> void:
|
||||
var app = _make_app()
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
|
||||
assert_that(app.visible).override_failure_message(
|
||||
"on_open(FULLSCREEN) must make app visible"
|
||||
).is_true()
|
||||
app.queue_free()
|
||||
|
||||
|
||||
func test_open_insert_makes_app_visible() -> void:
|
||||
var app = _make_app(true, "insert")
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.INSERT)
|
||||
assert_that(app.visible).override_failure_message(
|
||||
"on_open(INSERT) must make app visible"
|
||||
).is_true()
|
||||
app.queue_free()
|
||||
|
||||
|
||||
func test_gameplay_mode_does_not_open_app() -> void:
|
||||
var app = _make_app()
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.GAMEPLAY)
|
||||
assert_that(app.visible).override_failure_message(
|
||||
"GAMEPLAY mode must not make app visible"
|
||||
).is_false()
|
||||
app.queue_free()
|
||||
|
||||
|
||||
func test_wrong_app_path_does_not_open() -> void:
|
||||
var app = _make_app()
|
||||
app._internal_app_changed("implant/other", HudGroups.Mode.FULLSCREEN)
|
||||
assert_that(app.visible).override_failure_message(
|
||||
"App must not open when app_path does not match manifest"
|
||||
).is_false()
|
||||
app.queue_free()
|
||||
|
||||
|
||||
func test_on_open_nav_is_non_empty() -> void:
|
||||
# Base class auto-pushes default on first open, so nav is guaranteed
|
||||
# non-empty when on_open fires.
|
||||
var app = _make_app()
|
||||
app.nav.set_default("home")
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
|
||||
assert_that(app.nav.is_empty()).override_failure_message(
|
||||
"nav must be non-empty when on_open fires — base class ensures push_default"
|
||||
).is_false()
|
||||
app.queue_free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# on_close — triggered via _internal_app_changed with GAMEPLAY
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_close_hides_app() -> void:
|
||||
var app = _make_app()
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
|
||||
assert_that(app.visible).is_true() # sanity
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.GAMEPLAY)
|
||||
assert_that(app.visible).override_failure_message(
|
||||
"GAMEPLAY mode must hide the app (on_close path)"
|
||||
).is_false()
|
||||
app.queue_free()
|
||||
|
||||
|
||||
func test_different_app_opened_closes_this_app() -> void:
|
||||
# If a different app's path is broadcast, this app must close if visible.
|
||||
var app = _make_app()
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
|
||||
assert_that(app.visible).is_true()
|
||||
app._internal_app_changed("implant/other", HudGroups.Mode.FULLSCREEN)
|
||||
assert_that(app.visible).override_failure_message(
|
||||
"App must hide when a different app_path is activated"
|
||||
).is_false()
|
||||
app.queue_free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# preserves_state
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_preserves_state_true_stack_survives_close_reopen() -> void:
|
||||
var app = _make_app(true)
|
||||
app.nav.set_default("home")
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN) # open → push "home"
|
||||
app.nav.push("details") # navigate deeper
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.GAMEPLAY) # close
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN) # reopen
|
||||
assert_that(app.nav.current()).override_failure_message(
|
||||
"preserves_state=true: nav stack must survive close/reopen cycle"
|
||||
).is_equal("details")
|
||||
app.queue_free()
|
||||
|
||||
|
||||
func test_preserves_state_false_stack_reset_on_reopen() -> void:
|
||||
var app = _make_app(false)
|
||||
app.nav.set_default("home")
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN) # open → reset → "home"
|
||||
app.nav.push("details") # navigate deeper
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.GAMEPLAY) # close
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN) # reopen → reset again
|
||||
assert_that(app.nav.current()).override_failure_message(
|
||||
"preserves_state=false: nav stack must reset to default on reopen"
|
||||
).is_equal("home")
|
||||
app.queue_free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# on_insert_deactivated — gated on INSERT mode
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_on_insert_deactivated_closes_insert_app() -> void:
|
||||
# Set HudGroups to INSERT mode for our test app so the method sees it.
|
||||
HudGroups._active_app = TEST_APP_PATH
|
||||
HudGroups._active_mode = HudGroups.Mode.INSERT
|
||||
var app = _make_app(true, "insert")
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.INSERT)
|
||||
assert_that(app.visible).is_true()
|
||||
app.on_insert_deactivated()
|
||||
# HudGroups.close_app() fires app_changed → GAMEPLAY → _internal_app_changed
|
||||
assert_that(app.visible).override_failure_message(
|
||||
"on_insert_deactivated must close INSERT-mode app"
|
||||
).is_false()
|
||||
app.queue_free()
|
||||
|
||||
|
||||
func test_on_insert_deactivated_does_not_close_fullscreen_app() -> void:
|
||||
# FULLSCREEN apps are not affected by insert deactivation by default.
|
||||
HudGroups._active_app = TEST_APP_PATH
|
||||
HudGroups._active_mode = HudGroups.Mode.FULLSCREEN
|
||||
var app = _make_app(true, "fullscreen")
|
||||
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
|
||||
assert_that(app.visible).is_true()
|
||||
app.on_insert_deactivated()
|
||||
assert_that(app.visible).override_failure_message(
|
||||
"on_insert_deactivated must NOT close FULLSCREEN app by default"
|
||||
).is_true()
|
||||
app.queue_free()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# register_screen / current_screen_id
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_register_screen_adds_screen_as_child_hidden() -> void:
|
||||
var app = _make_app()
|
||||
var screen := Control.new()
|
||||
app.register_screen("main", screen)
|
||||
assert_that(screen.get_parent() == app).override_failure_message(
|
||||
"register_screen must add screen as child of app"
|
||||
).is_true()
|
||||
assert_that(screen.visible).override_failure_message(
|
||||
"register_screen must start screen hidden"
|
||||
).is_false()
|
||||
app.queue_free()
|
||||
|
||||
|
||||
func test_nav_push_shows_registered_screen() -> void:
|
||||
var app = _make_app()
|
||||
var screen := Control.new()
|
||||
app.register_screen("main", screen)
|
||||
app.nav.push("main")
|
||||
assert_that(screen.visible).override_failure_message(
|
||||
"nav.push must make the registered screen visible via _on_screen_changed"
|
||||
).is_true()
|
||||
app.queue_free()
|
||||
|
||||
|
||||
func test_nav_push_hides_previous_screen() -> void:
|
||||
var app = _make_app()
|
||||
var screen_a := Control.new()
|
||||
var screen_b := Control.new()
|
||||
app.register_screen("a", screen_a)
|
||||
app.register_screen("b", screen_b)
|
||||
app.nav.push("a")
|
||||
app.nav.push("b")
|
||||
assert_that(screen_a.visible).override_failure_message(
|
||||
"Previous screen must be hidden when new screen is pushed"
|
||||
).is_false()
|
||||
assert_that(screen_b.visible).override_failure_message(
|
||||
"New screen must be visible after push"
|
||||
).is_true()
|
||||
app.queue_free()
|
||||
|
||||
|
||||
func test_current_screen_id_tracks_nav() -> void:
|
||||
var app = _make_app()
|
||||
var screen := Control.new()
|
||||
app.register_screen("main", screen)
|
||||
app.nav.push("main")
|
||||
assert_that(app.current_screen_id()).is_equal("main")
|
||||
app.queue_free()
|
||||
|
||||
|
||||
func test_register_screen_duplicate_id_does_not_overwrite() -> void:
|
||||
var app = _make_app()
|
||||
var screen_a := Control.new()
|
||||
var screen_b := Control.new()
|
||||
app.register_screen("main", screen_a)
|
||||
app.register_screen("main", screen_b) # duplicate — must warn and skip
|
||||
# First registration wins: _screens["main"] stays screen_a, screen_b is NOT
|
||||
# parented by register_screen. (The base only mutates state on success.)
|
||||
assert_that(app._screens["main"]).override_failure_message(
|
||||
"First registered screen must win on duplicate id"
|
||||
).is_same(screen_a)
|
||||
assert_that(screen_b.get_parent()).override_failure_message(
|
||||
"Duplicate screen must not be reparented to the app"
|
||||
).is_null()
|
||||
app.nav.push("main")
|
||||
assert_that(screen_a.visible).override_failure_message(
|
||||
"First registered screen must be visible after nav push"
|
||||
).is_true()
|
||||
screen_b.queue_free() # not a child of app — free manually
|
||||
app.queue_free()
|
||||
@@ -0,0 +1,246 @@
|
||||
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")
|
||||
@@ -0,0 +1,209 @@
|
||||
class_name TestImplantRegistry
|
||||
extends GdUnitTestSuite
|
||||
## Unit tests for ImplantRegistry autoload (#844, D-191, PR #131 item 5).
|
||||
## Tests lazy-scan, manifest validation, mode resolution, real-scan results.
|
||||
|
||||
# Loaded inside method bodies to avoid class_name parse-order trap.
|
||||
const MANIFEST_SCRIPT := "res://ui/implant/implant_app_manifest.gd"
|
||||
|
||||
|
||||
func _make_manifest(app_path: String, mode: String = "fullscreen", key: int = -1) -> Resource:
|
||||
var ManifestClass := load(MANIFEST_SCRIPT)
|
||||
var m = ManifestClass.new()
|
||||
m.app_path = app_path
|
||||
m.default_mode = mode
|
||||
m.default_key = key
|
||||
m.schema_version = 1
|
||||
return m
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
# Reset to a clean slate so each test gets a fresh scan pass.
|
||||
ImplantRegistry._scanned = false
|
||||
ImplantRegistry._manifests.clear()
|
||||
ImplantRegistry._resolved_modes.clear()
|
||||
ImplantRegistry._instances.clear()
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
ImplantRegistry._scanned = false
|
||||
ImplantRegistry._manifests.clear()
|
||||
ImplantRegistry._resolved_modes.clear()
|
||||
ImplantRegistry._instances.clear()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# _is_valid_manifest — white-box validation helper
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_invalid_manifest_null_returns_false() -> void:
|
||||
assert_that(ImplantRegistry._is_valid_manifest(null)).is_false()
|
||||
|
||||
|
||||
func test_invalid_manifest_string_returns_false() -> void:
|
||||
assert_that(ImplantRegistry._is_valid_manifest("not a resource")).is_false()
|
||||
|
||||
|
||||
func test_invalid_manifest_plain_resource_no_app_path_returns_false() -> void:
|
||||
# A bare Resource has no app_path property.
|
||||
assert_that(ImplantRegistry._is_valid_manifest(Resource.new())).is_false()
|
||||
|
||||
|
||||
func test_invalid_manifest_empty_app_path_returns_false() -> void:
|
||||
var m = _make_manifest("")
|
||||
assert_that(ImplantRegistry._is_valid_manifest(m)).is_false()
|
||||
|
||||
|
||||
func test_valid_manifest_with_app_path_returns_true() -> void:
|
||||
var m = _make_manifest("implant/test")
|
||||
assert_that(ImplantRegistry._is_valid_manifest(m)).is_true()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Lazy scan
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_scanned_false_before_any_call() -> void:
|
||||
assert_that(ImplantRegistry._scanned).is_false()
|
||||
|
||||
|
||||
func test_get_manifests_triggers_scan() -> void:
|
||||
assert_that(ImplantRegistry._scanned).is_false()
|
||||
ImplantRegistry.get_manifests()
|
||||
assert_that(ImplantRegistry._scanned).is_true()
|
||||
|
||||
|
||||
func test_get_resolved_mode_triggers_scan() -> void:
|
||||
assert_that(ImplantRegistry._scanned).is_false()
|
||||
ImplantRegistry.get_resolved_mode("implant/map")
|
||||
assert_that(ImplantRegistry._scanned).is_true()
|
||||
|
||||
|
||||
func test_second_get_manifests_uses_cache() -> void:
|
||||
# _scanned = true after first call; subsequent calls must not reset it.
|
||||
ImplantRegistry.get_manifests()
|
||||
assert_that(ImplantRegistry._scanned).is_true()
|
||||
ImplantRegistry.get_manifests()
|
||||
assert_that(ImplantRegistry._scanned).is_true()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Real-filesystem scan — verifies atlas and economics apps are found
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_real_scan_finds_atlas_app() -> void:
|
||||
var manifests := ImplantRegistry.get_manifests()
|
||||
var found := false
|
||||
for m in manifests:
|
||||
if m.app_path == "implant/map":
|
||||
found = true
|
||||
break
|
||||
assert_that(found).override_failure_message(
|
||||
"ImplantRegistry must find atlas app (implant/map) after scan"
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_real_scan_finds_economics_app() -> void:
|
||||
var manifests := ImplantRegistry.get_manifests()
|
||||
var found := false
|
||||
for m in manifests:
|
||||
if m.app_path == "implant/economics":
|
||||
found = true
|
||||
break
|
||||
assert_that(found).override_failure_message(
|
||||
"ImplantRegistry must find economics app (implant/economics) after scan"
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_real_scan_manifests_all_have_schema_version_1() -> void:
|
||||
var manifests := ImplantRegistry.get_manifests()
|
||||
for m in manifests:
|
||||
assert_that(m.schema_version).override_failure_message(
|
||||
"All shipped manifests must declare schema_version = 1 — got wrong version for %s" % m.app_path
|
||||
).is_equal(1)
|
||||
|
||||
|
||||
func test_real_scan_no_duplicate_keys() -> void:
|
||||
# Key collision detection: _scan() must drop the second manifest if two
|
||||
# declare the same default_key. Verify no duplicates survive in the result.
|
||||
var manifests := ImplantRegistry.get_manifests()
|
||||
var seen_keys: Dictionary = {}
|
||||
for m in manifests:
|
||||
var k: int = m.default_key
|
||||
if k >= 0:
|
||||
assert_that(not seen_keys.has(k)).override_failure_message(
|
||||
"Key %d bound to both '%s' and '%s' — collision not detected by registry" % [
|
||||
k, seen_keys.get(k, ""), m.app_path
|
||||
]
|
||||
).is_true()
|
||||
seen_keys[k] = m.app_path
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# get_resolved_mode
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_get_resolved_mode_atlas_is_fullscreen() -> void:
|
||||
assert_that(ImplantRegistry.get_resolved_mode("implant/map")).is_equal(HudGroups.Mode.FULLSCREEN)
|
||||
|
||||
|
||||
func test_get_resolved_mode_economics_is_insert() -> void:
|
||||
assert_that(ImplantRegistry.get_resolved_mode("implant/economics")).is_equal(HudGroups.Mode.INSERT)
|
||||
|
||||
|
||||
func test_get_resolved_mode_unknown_path_defaults_to_fullscreen() -> void:
|
||||
ImplantRegistry.get_manifests() # ensure scan ran
|
||||
assert_that(ImplantRegistry.get_resolved_mode("implant/nonexistent")).is_equal(HudGroups.Mode.FULLSCREEN)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# get_app_instance — before instantiate_all
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_get_app_instance_returns_null_before_instantiate_all() -> void:
|
||||
# Registry scans manifests but does not instantiate until instantiate_all()
|
||||
# is called from hud.gd. Querying before that must return null.
|
||||
ImplantRegistry.get_manifests()
|
||||
assert_that(ImplantRegistry.get_app_instance("implant/map")).is_null()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# _MODE_MAP — covers the invalid default_mode guard in _scan()
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_mode_map_contains_all_valid_mode_strings() -> void:
|
||||
assert_that(ImplantRegistry._MODE_MAP.has("gameplay")).is_true()
|
||||
assert_that(ImplantRegistry._MODE_MAP.has("insert")).is_true()
|
||||
assert_that(ImplantRegistry._MODE_MAP.has("fullscreen")).is_true()
|
||||
|
||||
|
||||
func test_mode_map_does_not_contain_invalid_strings() -> void:
|
||||
# _scan() rejects manifests whose default_mode is not in this map.
|
||||
assert_that(ImplantRegistry._MODE_MAP.has("bogus")).is_false()
|
||||
assert_that(ImplantRegistry._MODE_MAP.has("")).is_false()
|
||||
assert_that(ImplantRegistry._MODE_MAP.has("Fullscreen")).is_false() # case-sensitive
|
||||
|
||||
|
||||
func test_real_scan_manifests_have_valid_mode_strings() -> void:
|
||||
var manifests := ImplantRegistry.get_manifests()
|
||||
for m in manifests:
|
||||
assert_that(ImplantRegistry._MODE_MAP.has(m.default_mode)).override_failure_message(
|
||||
"Manifest %s has invalid default_mode '%s' — _scan should have rejected it" % [
|
||||
m.app_path, m.default_mode
|
||||
]
|
||||
).is_true()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Schema version constant
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_current_schema_version_is_1() -> void:
|
||||
assert_that(ImplantRegistry.CURRENT_SCHEMA_VERSION).is_equal(1)
|
||||
Reference in New Issue
Block a user