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