Files
settled-reach/client/tests/test_dialogue_sprint20.gd
T
jpmschweitzerandClaude Opus 4.6 6f9c97ca03 refactor(client): decouple dialogue_box from GameState and AudioManager (#558)
Replace 3 direct GameState.dialogue_active mutations and all
AudioManager.apply_dip/clear_dip calls with signals:
dialogue_state_changed, audio_dip_requested, audio_dip_cleared.
dialogue_box.gd now has zero references to GameState or AudioManager.
main.gd wires coordinator handlers in _ready() (D-020).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 21:59:47 +01:00

144 lines
5.3 KiB
GDScript

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