## Sprint 20 #558: dialogue_box.gd decoupling tests. ## ## Verifies D-020 compliance: dialogue_box.gd emits signals instead of mutating ## GameState or calling AudioManager directly. main.gd wires the signal handlers. ## ## D-030: fixture-based, server-free, no subprocess required. class_name TestDialogueSprint20 extends GdUnitTestSuite func _make_dialogue_box() -> Control: if not ResourceLoader.exists("res://ui/dialogue_box.tscn"): push_warning("TestDialogueSprint20: dialogue_box.tscn not found — scene tests skipped") return null var node: Control = load("res://ui/dialogue_box.tscn").instantiate() add_child(node) return node func before_test() -> void: GameState.dialogue_active = false func after_test() -> void: GameState.dialogue_active = false # -- dialogue_state_changed signal ------------------------------------------- func test_show_dialogue_emits_dialogue_state_changed_true() -> void: ## show_dialogue() must emit dialogue_state_changed(true) not mutate GameState directly. var box := _make_dialogue_box() if box == null: return auto_free(box) var received: Variant = null box.dialogue_state_changed.connect(func(active: bool): received = active) box.show_dialogue("NPC", "Hello.", []) assert_that(received).override_failure_message( "show_dialogue() must emit dialogue_state_changed(true) (#558)" ).is_equal(true) func test_show_dialogue_does_not_mutate_game_state_directly() -> void: ## Without a connected handler, GameState.dialogue_active must stay false. ## Proves dialogue_box.gd has zero direct GameState mutation (D-020 #558). var box := _make_dialogue_box() if box == null: return auto_free(box) GameState.dialogue_active = false box.show_dialogue("NPC", "Hello.", []) assert_bool(GameState.dialogue_active).override_failure_message( "dialogue_box must not mutate GameState.dialogue_active directly (D-020 #558)" ).is_false() func test_hide_dialogue_emits_dialogue_state_changed_false() -> void: ## hide_dialogue() must emit dialogue_state_changed(false). var box := _make_dialogue_box() if box == null: return auto_free(box) box.show_dialogue("NPC", "Hello.", []) var received: Variant = null box.dialogue_state_changed.connect(func(active: bool): received = active) box.hide_dialogue() assert_that(received).override_failure_message( "hide_dialogue() must emit dialogue_state_changed(false) (#558)" ).is_equal(false) # -- audio_dip_requested / audio_dip_cleared signals ------------------------- func test_show_dialogue_emits_audio_dip_requested_dialogue() -> void: ## show_dialogue() must emit audio_dip_requested("dialogue") not call AudioManager. var box := _make_dialogue_box() if box == null: return auto_free(box) var received_profile: Variant = null box.audio_dip_requested.connect(func(profile: String): received_profile = profile) box.show_dialogue("NPC", "Hello.", []) assert_that(received_profile).override_failure_message( "show_dialogue() must emit audio_dip_requested('dialogue') (#558)" ).is_equal("dialogue") func test_show_dialogue_does_not_call_audio_manager_directly() -> void: ## Without a connected handler, AudioManager state must be unchanged by show_dialogue(). ## Verifies no direct AudioManager call in dialogue_box.gd (D-020 #558). var box := _make_dialogue_box() if box == null: return auto_free(box) var dip_before := AudioManager.get_active_dip() box.show_dialogue("NPC", "Hello.", []) var dip_after := AudioManager.get_active_dip() assert_str(dip_after).override_failure_message( "dialogue_box must not call AudioManager.apply_dip() directly (D-020 #558)" ).is_equal(dip_before) func test_hide_dialogue_emits_audio_dip_cleared() -> void: ## Ending a conversation must emit audio_dip_cleared not call AudioManager directly. var box := _make_dialogue_box() if box == null: return auto_free(box) box.show_dialogue("NPC", "Hello.", []) var cleared := false box.audio_dip_cleared.connect(func(): cleared = true) box.hide_dialogue() assert_bool(cleared).override_failure_message( "hide_dialogue() must emit audio_dip_cleared (#558)" ).is_true() func test_audio_dip_cleared_count_on_conversation_end() -> void: ## Verify audio_dip_cleared fires when conversation ends. var box := _make_dialogue_box() if box == null: return auto_free(box) var cleared_count := 0 box.audio_dip_cleared.connect(func(): cleared_count += 1) box.show_dialogue("NPC", "Speak.", []) box.hide_dialogue() assert_int(cleared_count).override_failure_message( "audio_dip_cleared must fire at least once when conversation ends (#558)" ).is_greater_equal(1) # -- coordinator wiring verification ----------------------------------------- func test_signal_handler_wires_game_state() -> void: ## Simulate main.gd: connect dialogue_state_changed to update GameState.dialogue_active. ## Verifies the coordinator pattern works end-to-end (D-020 #558). var box := _make_dialogue_box() if box == null: return auto_free(box) box.dialogue_state_changed.connect(func(active: bool): GameState.dialogue_active = active) box.show_dialogue("NPC", "Hello.", []) assert_bool(GameState.dialogue_active).override_failure_message( "With handler wired, GameState.dialogue_active must be true after show_dialogue (#558)" ).is_true() box.hide_dialogue() assert_bool(GameState.dialogue_active).override_failure_message( "With handler wired, GameState.dialogue_active must be false after hide_dialogue (#558)" ).is_false()