Production fixes surfaced by honest test triage: - hud_groups.gd: _set_group_z crashed on freed HUD nodes — the typed loop variable errors before the is_instance_valid guard runs; prune first - fog_state.gd: _resize cleared _prev_visible (world-space keys survive resizes), so pre-resize tiles never decayed VISIBLE→EXPLORED (D-059) Test debt (T-928/929/934/935/936/937/938/939, T-864, T-973): lambda local-capture bugs rewritten with array captures (now assert exact emission counts), e2e suites updated to the current handshake + StartupMessage protocol and stream-aware reads against the live binary, fog perf test measures steady state, chime test pins the shipped 800ms catalog asset (D-067 amended separately), monologue gdUnit4 API typo, battery-warning tests follow the MetaScreen on_open lifecycle. 3 sprint2 proof tests revived (corner_reveal had passed from the wrong tile — NPC3 blocks (18,14); route corrected). Soft-skips converted to real do_skip reporting. T-1068: 7 orphan .gd.uid deleted, _format_pop/_format_radius deduped into atlas_format.gd (preload, no class_name — headless cache). Suite: 1264 cases/20 failures → 1268/0, independently re-verified (2536/2536, exit 0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
150 lines
5.9 KiB
GDScript
150 lines
5.9 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)
|
|
# Array capture: GDScript lambdas capture locals by value — assignment
|
|
# inside the lambda never propagates out. Mutating an Array does.
|
|
var received: Array = []
|
|
box.dialogue_state_changed.connect(func(active: bool): received.append(active))
|
|
box.show_dialogue("NPC", "Hello.", [])
|
|
assert_array(received).override_failure_message(
|
|
"show_dialogue() must emit dialogue_state_changed(true) exactly once (#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.", [])
|
|
# Array capture — see test_show_dialogue_emits_dialogue_state_changed_true.
|
|
var received: Array = []
|
|
box.dialogue_state_changed.connect(func(active: bool): received.append(active))
|
|
box.hide_dialogue()
|
|
assert_array(received).override_failure_message(
|
|
"hide_dialogue() must emit dialogue_state_changed(false) exactly once (#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)
|
|
# Array capture — see test_show_dialogue_emits_dialogue_state_changed_true.
|
|
var received_profiles: Array = []
|
|
box.audio_dip_requested.connect(func(profile: String): received_profiles.append(profile))
|
|
box.show_dialogue("NPC", "Hello.", [])
|
|
assert_array(received_profiles).override_failure_message(
|
|
"show_dialogue() must emit audio_dip_requested('dialogue') exactly once (#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.", [])
|
|
# Array capture — see test_show_dialogue_emits_dialogue_state_changed_true.
|
|
var cleared: Array = []
|
|
box.audio_dip_cleared.connect(func(): cleared.append(true))
|
|
box.hide_dialogue()
|
|
assert_int(cleared.size()).override_failure_message(
|
|
"hide_dialogue() must emit audio_dip_cleared exactly once (#558)"
|
|
).is_equal(1)
|
|
|
|
|
|
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)
|
|
# Array capture — see test_show_dialogue_emits_dialogue_state_changed_true.
|
|
var cleared: Array = []
|
|
box.audio_dip_cleared.connect(func(): cleared.append(true))
|
|
box.show_dialogue("NPC", "Speak.", [])
|
|
box.hide_dialogue()
|
|
assert_int(cleared.size()).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()
|