- test_character_creation_sprint28: before_each now seeds _selected_bookmark_id and _selected_location_id so the new disabled- guard in _on_start() (round 2) doesn't silently block 5 existing tests that call _on_start()/KEY_ENTER without setting up a valid bookmark selection. Restores the 2 tests Hoshe flagged as R2-H1 plus 3 siblings that would have degraded the same way under the guard. - tests/run-godot: LOG_FILE now includes $$ (PID) so concurrent runs across worktrees don't clobber each other's logs. Path is echoed back via the stdout JSON "log" field and the stderr hint line, so callers never need to predict it (R2-H2).
650 lines
21 KiB
GDScript
650 lines
21 KiB
GDScript
## Sprint 28 — Character creation screen tests (#705, Task #9)
|
||
## Updated sprint 36: W5/W6 restructure — 4-tab layout (Bookmark/Appearance/Skills/Debug),
|
||
## CharacterProfile signal type (#618/#680).
|
||
##
|
||
## Validates the CharacterCreation UI: scene instantiation, tab structure,
|
||
## signal emission (creation_confirmed / creation_cancelled), keyboard nav
|
||
## callbacks, randomize, color derivation helpers, and game flow wiring.
|
||
##
|
||
## NOTE: All tests run vacuously in headless — the 3D SubViewport scene cannot
|
||
## instantiate without a rendering context. Tests return early on _scene == null.
|
||
## Run non-headless for full coverage.
|
||
##
|
||
## Ticket: #705 | D-146, D-155, D-158, D-159, D-165
|
||
class_name TestCharacterCreationSprint28
|
||
extends GdUnitTestSuite
|
||
|
||
const SCENE_PATH := "res://scenes/character_creation.tscn"
|
||
|
||
var _scene: CharacterCreation = null
|
||
|
||
|
||
func before_each() -> void:
|
||
var packed := load(SCENE_PATH) as PackedScene
|
||
if packed == null:
|
||
# Skip all tests gracefully if scene not loadable in headless env
|
||
return
|
||
_scene = packed.instantiate() as CharacterCreation
|
||
add_child(_scene)
|
||
# Seed a valid bookmark/location so _on_start passes the disabled guard
|
||
# added in PR #134 (R2-Hoshe-1). Tests that verify the disabled state
|
||
# should explicitly clear these and call _update_start_btn_state().
|
||
_scene._selected_bookmark_id = "test-bookmark"
|
||
_scene._selected_location_id = "test-location"
|
||
if _scene.has_method("_update_start_btn_state"):
|
||
_scene._update_start_btn_state()
|
||
|
||
|
||
func after_each() -> void:
|
||
if is_instance_valid(_scene):
|
||
_scene.queue_free()
|
||
_scene = null
|
||
|
||
|
||
# =============================================================================
|
||
# Scene instantiation (Task #1)
|
||
# =============================================================================
|
||
|
||
func test_scene_instantiates() -> void:
|
||
if _scene == null:
|
||
return
|
||
assert_bool(is_instance_valid(_scene)).override_failure_message(
|
||
"character_creation.tscn must instantiate successfully"
|
||
).is_true()
|
||
|
||
|
||
func test_scene_is_character_creation_class() -> void:
|
||
if _scene == null:
|
||
return
|
||
assert_bool(_scene is CharacterCreation).override_failure_message(
|
||
"instantiated scene must be a CharacterCreation node"
|
||
).is_true()
|
||
|
||
|
||
func test_tab_container_has_four_tabs() -> void:
|
||
## W5 restructure: 4 top-level tabs — Bookmark / Appearance / Skills / Debug.
|
||
if _scene == null:
|
||
return
|
||
var tc: TabContainer = _scene.get_node_or_null("Layout/TabPanel/TabContainer")
|
||
assert_bool(tc != null).override_failure_message("TabContainer must exist").is_true()
|
||
if tc == null:
|
||
return
|
||
assert_int(tc.get_tab_count()).override_failure_message(
|
||
"TabContainer must have exactly 4 tabs (Bookmark/Appearance/Skills/Debug)"
|
||
).is_equal(4)
|
||
|
||
|
||
func test_tab_names() -> void:
|
||
## W5 restructure: top-level tabs are Bookmark/Appearance/Skills/Debug.
|
||
## Appearance sub-nav (Body/Head/Hair/Clothing/Accessories) is inside the Appearance tab.
|
||
if _scene == null:
|
||
return
|
||
var tc: TabContainer = _scene.get_node_or_null("Layout/TabPanel/TabContainer")
|
||
if tc == null:
|
||
return
|
||
var expected := ["Bookmark", "Appearance", "Skills", "Debug"]
|
||
for i in expected.size():
|
||
assert_str(tc.get_tab_title(i)).override_failure_message(
|
||
"Tab %d must be named '%s'" % [i, expected[i]]
|
||
).is_equal(expected[i])
|
||
|
||
|
||
func test_subviewport_exists() -> void:
|
||
if _scene == null:
|
||
return
|
||
var sv: SubViewport = _scene.get_node_or_null(
|
||
"Layout/PreviewPanel/SubViewportContainer/SubViewport"
|
||
)
|
||
assert_bool(sv != null).override_failure_message("SubViewport must exist in scene tree").is_true()
|
||
|
||
|
||
func test_subviewport_always_updates() -> void:
|
||
if _scene == null:
|
||
return
|
||
var sv: SubViewport = _scene.get_node_or_null(
|
||
"Layout/PreviewPanel/SubViewportContainer/SubViewport"
|
||
)
|
||
if sv == null:
|
||
return
|
||
# render_target_update_mode = 3 = ALWAYS (live 3D preview)
|
||
assert_int(sv.render_target_update_mode).override_failure_message(
|
||
"SubViewport must use ALWAYS update mode (3) for live preview"
|
||
).is_equal(3)
|
||
|
||
|
||
func test_preview_camera_exists() -> void:
|
||
if _scene == null:
|
||
return
|
||
var cam: Camera3D = _scene.get_node_or_null(
|
||
"Layout/PreviewPanel/SubViewportContainer/SubViewport/PreviewCamera"
|
||
)
|
||
assert_bool(cam != null).override_failure_message("PreviewCamera must exist in SubViewport").is_true()
|
||
if cam == null:
|
||
return
|
||
assert_bool(cam.current).override_failure_message("PreviewCamera must be active (current=true)").is_true()
|
||
|
||
|
||
func test_footer_buttons_exist() -> void:
|
||
if _scene == null:
|
||
return
|
||
var back: Button = _scene.get_node_or_null("Footer/BackBtn")
|
||
var rand_btn: Button = _scene.get_node_or_null("Footer/RandomizeBtn")
|
||
var start: Button = _scene.get_node_or_null("Footer/StartBtn")
|
||
assert_bool(back != null).override_failure_message("BackBtn must exist in Footer").is_true()
|
||
assert_bool(rand_btn != null).override_failure_message("RandomizeBtn must exist in Footer").is_true()
|
||
assert_bool(start != null).override_failure_message("StartBtn must exist in Footer").is_true()
|
||
|
||
|
||
# =============================================================================
|
||
# Signal emission (Tasks #1 and #8)
|
||
# =============================================================================
|
||
|
||
func test_creation_cancelled_emits_on_back() -> void:
|
||
if _scene == null:
|
||
return
|
||
var monitor := monitor_signals(_scene)
|
||
_scene._on_back()
|
||
assert_signal(monitor).is_emitted("creation_cancelled")
|
||
|
||
|
||
func test_creation_confirmed_emits_on_start() -> void:
|
||
if _scene == null:
|
||
return
|
||
var monitor := monitor_signals(_scene)
|
||
_scene._on_start()
|
||
assert_signal(monitor).is_emitted("creation_confirmed")
|
||
|
||
|
||
func test_creation_confirmed_carries_descriptor() -> void:
|
||
if _scene == null:
|
||
return
|
||
var received_profile = null # CharacterProfile — untyped avoids parse-time member resolution
|
||
_scene.creation_confirmed.connect(func(p): received_profile = p)
|
||
_scene._on_start()
|
||
assert_bool(received_profile != null).override_failure_message(
|
||
"creation_confirmed must pass a CharacterProfile"
|
||
).is_true()
|
||
assert_bool(received_profile is CharacterProfile).is_true()
|
||
|
||
|
||
# =============================================================================
|
||
# Descriptor defaults (Task #1)
|
||
# =============================================================================
|
||
|
||
func test_descriptor_initialized_on_ready() -> void:
|
||
if _scene == null:
|
||
return
|
||
var received_profile = null # CharacterProfile — untyped avoids parse-time member resolution
|
||
_scene.creation_confirmed.connect(func(p): received_profile = p)
|
||
_scene._on_start()
|
||
assert_bool(received_profile != null).is_true()
|
||
if received_profile == null:
|
||
return
|
||
var desc = received_profile.descriptor
|
||
assert_bool(desc != null).is_true()
|
||
if desc == null:
|
||
return
|
||
# Should default to a non-Child body type
|
||
assert_bool(desc.body_type != CharacterVisualDescriptor.BodyType.CHILD).override_failure_message(
|
||
"Default body type should not be Child"
|
||
).is_true()
|
||
# Skin tone should be in valid range
|
||
assert_bool(desc.skin_tone >= 0 and desc.skin_tone <= 8).override_failure_message(
|
||
"skin_tone must be 0–8"
|
||
).is_true()
|
||
|
||
|
||
# =============================================================================
|
||
# Rotation (Task #1, D-155)
|
||
# =============================================================================
|
||
|
||
func test_rotate_left_changes_facing() -> void:
|
||
if _scene == null:
|
||
return
|
||
var initial := _scene._facing_idx
|
||
_scene._on_rotate_left()
|
||
assert_int(_scene._facing_idx).override_failure_message(
|
||
"Q/rotate-left must advance facing_idx"
|
||
).is_not_equal(initial)
|
||
|
||
|
||
func test_rotate_right_changes_facing() -> void:
|
||
if _scene == null:
|
||
return
|
||
var initial := _scene._facing_idx
|
||
_scene._on_rotate_right()
|
||
assert_int(_scene._facing_idx).override_failure_message(
|
||
"E/rotate-right must change facing_idx"
|
||
).is_not_equal(initial)
|
||
|
||
|
||
func test_four_rotations_return_to_original() -> void:
|
||
if _scene == null:
|
||
return
|
||
var initial := _scene._facing_idx
|
||
_scene._on_rotate_left()
|
||
_scene._on_rotate_left()
|
||
_scene._on_rotate_left()
|
||
_scene._on_rotate_left()
|
||
assert_int(_scene._facing_idx).override_failure_message(
|
||
"4 left rotations must return to original facing"
|
||
).is_equal(initial)
|
||
|
||
|
||
# =============================================================================
|
||
# Camera angle toggle (Task #1, D-158)
|
||
# =============================================================================
|
||
|
||
func test_cam_angle_cycles_three_presets() -> void:
|
||
if _scene == null:
|
||
return
|
||
var initial := _scene._cam_pitch_idx
|
||
_scene._on_cam_angle_toggle()
|
||
_scene._on_cam_angle_toggle()
|
||
_scene._on_cam_angle_toggle()
|
||
assert_int(_scene._cam_pitch_idx).override_failure_message(
|
||
"3 cam angle toggles must return to original preset"
|
||
).is_equal(initial)
|
||
|
||
|
||
func test_cam_angle_default_is_frontal() -> void:
|
||
if _scene == null:
|
||
return
|
||
# frontal = index 0, pitch -5°
|
||
assert_int(_scene._cam_pitch_idx).override_failure_message(
|
||
"Default camera must be frontal (index 0, -5° per D-158)"
|
||
).is_equal(0)
|
||
|
||
|
||
# =============================================================================
|
||
# Body type selection (Task #2, D-159)
|
||
# =============================================================================
|
||
|
||
func test_body_type_selection_updates_descriptor() -> void:
|
||
if _scene == null:
|
||
return
|
||
_scene._on_body_type_selected(CharacterVisualDescriptor.BodyType.THIN_F)
|
||
var received_profile = null # CharacterProfile — untyped avoids parse-time member resolution
|
||
_scene.creation_confirmed.connect(func(p): received_profile = p)
|
||
_scene._on_start()
|
||
if received_profile == null:
|
||
return
|
||
assert_int(received_profile.descriptor.body_type as int).override_failure_message(
|
||
"Selecting THIN_F must update descriptor.body_type"
|
||
).is_equal(CharacterVisualDescriptor.BodyType.THIN_F)
|
||
|
||
|
||
func test_all_11_body_types_selectable() -> void:
|
||
if _scene == null:
|
||
return
|
||
# Verify all 11 body types can be selected without error and update descriptor
|
||
var all_types := CharacterVisualDescriptor.BodyType.values()
|
||
assert_int(all_types.size()).override_failure_message(
|
||
"Must have 11 body types per D-159"
|
||
).is_equal(11)
|
||
for bt in all_types:
|
||
_scene._on_body_type_selected(bt)
|
||
assert_int(_scene._descriptor.body_type as int).override_failure_message(
|
||
"Body type selection must update descriptor immediately"
|
||
).is_equal(bt)
|
||
|
||
|
||
# =============================================================================
|
||
# Skin tone (Tasks #2/#3, shared state)
|
||
# =============================================================================
|
||
|
||
func test_skin_tone_selection_updates_descriptor() -> void:
|
||
if _scene == null:
|
||
return
|
||
_scene._on_skin_tone_selected(5)
|
||
var received_profile = null # CharacterProfile — untyped avoids parse-time member resolution
|
||
_scene.creation_confirmed.connect(func(p): received_profile = p)
|
||
_scene._on_start()
|
||
if received_profile == null:
|
||
return
|
||
assert_int(received_profile.descriptor.skin_tone).override_failure_message(
|
||
"Selecting skin tone index 5 must update descriptor.skin_tone"
|
||
).is_equal(5)
|
||
|
||
|
||
func test_skin_tone_range_valid() -> void:
|
||
if _scene == null:
|
||
return
|
||
# All 9 indices must be selectable without error
|
||
for i in range(9):
|
||
_scene._on_skin_tone_selected(i)
|
||
assert_int(_scene._descriptor.skin_tone).is_equal(8) # last selected
|
||
|
||
|
||
# =============================================================================
|
||
# Hair selection (Task #4)
|
||
# =============================================================================
|
||
|
||
func test_hair_selection_updates_descriptor() -> void:
|
||
if _scene == null:
|
||
return
|
||
_scene._on_hair_selected("short_b")
|
||
assert_str(_scene._descriptor.hair_id).override_failure_message(
|
||
"Selecting hair 'short_b' must update descriptor.hair_id"
|
||
).is_equal("short_b")
|
||
|
||
|
||
func test_facial_hair_none_clears_id() -> void:
|
||
if _scene == null:
|
||
return
|
||
_scene._on_facial_hair_selected("beard")
|
||
assert_str(_scene._descriptor.facial_hair_id).is_equal("beard")
|
||
_scene._on_facial_hair_selected("")
|
||
assert_str(_scene._descriptor.facial_hair_id).is_equal("")
|
||
|
||
|
||
func test_eyebrow_selection_updates_descriptor() -> void:
|
||
if _scene == null:
|
||
return
|
||
_scene._on_eyebrow_selected("thick")
|
||
assert_str(_scene._descriptor.eyebrow_id).override_failure_message(
|
||
"Selecting eyebrow 'thick' must update descriptor.eyebrow_id"
|
||
).is_equal("thick")
|
||
|
||
|
||
# =============================================================================
|
||
# Color derivation helpers (Task #4/#5, D-165)
|
||
# =============================================================================
|
||
|
||
func test_derive_hair_highlight_lightens_primary() -> void:
|
||
if _scene == null:
|
||
return
|
||
var primary := Color(0.4, 0.2, 0.1)
|
||
var highlight := _scene._derive_hair_highlight(primary)
|
||
# Highlight should be lighter than primary
|
||
assert_bool(highlight.v > primary.v).override_failure_message(
|
||
"Hair highlight must be lighter than primary color"
|
||
).is_true()
|
||
|
||
|
||
func test_derive_secondary_darkens_primary() -> void:
|
||
if _scene == null:
|
||
return
|
||
var primary := Color(0.6, 0.5, 0.4)
|
||
var secondary := _scene._derive_secondary(primary)
|
||
assert_bool(secondary.v < primary.v).override_failure_message(
|
||
"Secondary color must be darker than primary"
|
||
).is_true()
|
||
|
||
|
||
func test_derive_accent_shifts_hue() -> void:
|
||
if _scene == null:
|
||
return
|
||
var primary := Color.from_hsv(0.0, 0.5, 0.7)
|
||
var accent := _scene._derive_accent(primary)
|
||
# Accent hue should differ from primary hue
|
||
assert_bool(abs(accent.h - primary.h) > 0.0).override_failure_message(
|
||
"Accent must have shifted hue from primary"
|
||
).is_true()
|
||
|
||
|
||
# =============================================================================
|
||
# Randomize (Task #8)
|
||
# =============================================================================
|
||
|
||
func test_randomize_changes_body_type() -> void:
|
||
if _scene == null:
|
||
return
|
||
# Run randomize multiple times to ensure it doesn't always pick the default
|
||
var changed := false
|
||
var original := _scene._descriptor.body_type
|
||
for _i in 20:
|
||
_scene._on_randomize()
|
||
if _scene._descriptor.body_type != original:
|
||
changed = true
|
||
break
|
||
assert_bool(changed).override_failure_message(
|
||
"Randomize must be able to change body type from default"
|
||
).is_true()
|
||
|
||
|
||
func test_randomize_produces_valid_skin_tone() -> void:
|
||
if _scene == null:
|
||
return
|
||
for _i in 10:
|
||
_scene._on_randomize()
|
||
var tone := _scene._descriptor.skin_tone
|
||
assert_bool(tone >= 0 and tone <= 8).override_failure_message(
|
||
"Randomize must produce skin_tone in range 0–8"
|
||
).is_true()
|
||
|
||
|
||
# =============================================================================
|
||
# Tab keyboard navigation (Task #8)
|
||
# =============================================================================
|
||
|
||
func test_tab_navigation_wraps() -> void:
|
||
if _scene == null:
|
||
return
|
||
var tc: TabContainer = _scene.get_node_or_null("Layout/TabPanel/TabContainer")
|
||
if tc == null:
|
||
return
|
||
tc.current_tab = 3 # last tab (Debug, index 3 of 4)
|
||
# Simulate Tab key forward — wraps to 0
|
||
var event := InputEventKey.new()
|
||
event.keycode = KEY_TAB
|
||
event.pressed = true
|
||
event.shift_pressed = false
|
||
_scene._input(event)
|
||
assert_int(tc.current_tab).override_failure_message(
|
||
"Tab key on last tab must wrap to first tab"
|
||
).is_equal(0)
|
||
|
||
|
||
func test_shift_tab_navigates_backward() -> void:
|
||
if _scene == null:
|
||
return
|
||
var tc: TabContainer = _scene.get_node_or_null("Layout/TabPanel/TabContainer")
|
||
if tc == null:
|
||
return
|
||
tc.current_tab = 2
|
||
var event := InputEventKey.new()
|
||
event.keycode = KEY_TAB
|
||
event.pressed = true
|
||
event.shift_pressed = true
|
||
_scene._input(event)
|
||
assert_int(tc.current_tab).override_failure_message(
|
||
"Shift+Tab must navigate to previous tab"
|
||
).is_equal(1)
|
||
|
||
|
||
func test_esc_emits_cancelled() -> void:
|
||
if _scene == null:
|
||
return
|
||
var monitor := monitor_signals(_scene)
|
||
var event := InputEventKey.new()
|
||
event.keycode = KEY_ESCAPE
|
||
event.pressed = true
|
||
_scene._input(event)
|
||
assert_signal(monitor).is_emitted("creation_cancelled")
|
||
|
||
|
||
func test_enter_emits_confirmed() -> void:
|
||
if _scene == null:
|
||
return
|
||
var monitor := monitor_signals(_scene)
|
||
var event := InputEventKey.new()
|
||
event.keycode = KEY_ENTER
|
||
event.pressed = true
|
||
_scene._input(event)
|
||
assert_signal(monitor).is_emitted("creation_confirmed")
|
||
|
||
|
||
func test_q_key_rotates_left() -> void:
|
||
if _scene == null:
|
||
return
|
||
var initial := _scene._facing_idx
|
||
var event := InputEventKey.new()
|
||
event.keycode = KEY_Q
|
||
event.pressed = true
|
||
_scene._input(event)
|
||
assert_int(_scene._facing_idx).override_failure_message(
|
||
"Q key must rotate character (facing_idx change)"
|
||
).is_not_equal(initial)
|
||
|
||
|
||
func test_r_key_triggers_randomize() -> void:
|
||
if _scene == null:
|
||
return
|
||
# Seed to a known state so change is detectable
|
||
_scene._descriptor.body_type = CharacterVisualDescriptor.BodyType.AVERAGE_M
|
||
_scene._descriptor.skin_tone = 0
|
||
var body_before := _scene._descriptor.body_type
|
||
var skin_before := _scene._descriptor.skin_tone
|
||
# Press R up to 10 times — must change at least one observable descriptor field
|
||
var changed := false
|
||
for _i in 10:
|
||
var event := InputEventKey.new()
|
||
event.keycode = KEY_R
|
||
event.pressed = true
|
||
_scene._input(event)
|
||
if _scene._descriptor.body_type != body_before or _scene._descriptor.skin_tone != skin_before:
|
||
changed = true
|
||
break
|
||
assert_bool(changed).override_failure_message(
|
||
"R key must trigger randomize and change body_type or skin_tone"
|
||
).is_true()
|
||
|
||
|
||
# =============================================================================
|
||
# Color picker modal (Task #7, D-165)
|
||
# =============================================================================
|
||
|
||
func test_color_picker_modal_hidden_by_default() -> void:
|
||
if _scene == null:
|
||
return
|
||
var modal: Control = _scene.get_node_or_null("ColorPickerModal")
|
||
assert_bool(modal != null).override_failure_message("ColorPickerModal node must exist").is_true()
|
||
if modal == null:
|
||
return
|
||
assert_bool(not modal.visible).override_failure_message(
|
||
"ColorPickerModal must be hidden at startup"
|
||
).is_true()
|
||
|
||
|
||
func test_open_color_picker_shows_modal() -> void:
|
||
if _scene == null:
|
||
return
|
||
var modal: Control = _scene.get_node_or_null("ColorPickerModal")
|
||
if modal == null:
|
||
return
|
||
var received: Color = Color.WHITE
|
||
_scene._open_color_picker(Color.RED, null, func(c): received = c)
|
||
assert_bool(modal.visible).override_failure_message(
|
||
"_open_color_picker must make modal visible"
|
||
).is_true()
|
||
# Cancel to clean up
|
||
_scene._on_modal_cancel()
|
||
|
||
|
||
func test_modal_cancel_hides_modal() -> void:
|
||
if _scene == null:
|
||
return
|
||
var modal: Control = _scene.get_node_or_null("ColorPickerModal")
|
||
if modal == null:
|
||
return
|
||
_scene._open_color_picker(Color.BLUE, null, func(_c): pass)
|
||
_scene._on_modal_cancel()
|
||
assert_bool(not modal.visible).override_failure_message(
|
||
"Cancel must hide the modal"
|
||
).is_true()
|
||
|
||
|
||
func test_modal_cancel_reverts_to_original() -> void:
|
||
if _scene == null:
|
||
return
|
||
var received: Color = Color.WHITE
|
||
var original := Color.RED
|
||
_scene._open_color_picker(original, null, func(c): received = c)
|
||
# Simulate palette swatch press (changes color)
|
||
_scene._on_modal_palette_swatch_pressed(Color.GREEN, null)
|
||
# Cancel — must revert to original
|
||
_scene._on_modal_cancel()
|
||
assert_bool(received.is_equal_approx(original)).override_failure_message(
|
||
"Cancel must revert callback to original color"
|
||
).is_true()
|
||
|
||
|
||
func test_modal_ok_closes_modal() -> void:
|
||
if _scene == null:
|
||
return
|
||
var modal: Control = _scene.get_node_or_null("ColorPickerModal")
|
||
if modal == null:
|
||
return
|
||
_scene._open_color_picker(Color.WHITE, null, func(_c): pass)
|
||
_scene._on_modal_ok()
|
||
assert_bool(not modal.visible).override_failure_message(
|
||
"OK must hide the modal"
|
||
).is_true()
|
||
|
||
|
||
func test_modal_esc_key_cancels_when_open() -> void:
|
||
if _scene == null:
|
||
return
|
||
var modal: Control = _scene.get_node_or_null("ColorPickerModal")
|
||
if modal == null:
|
||
return
|
||
_scene._open_color_picker(Color.WHITE, null, func(_c): pass)
|
||
assert_bool(modal.visible).is_true()
|
||
var event := InputEventKey.new()
|
||
event.keycode = KEY_ESCAPE
|
||
event.pressed = true
|
||
_scene._input(event)
|
||
assert_bool(not modal.visible).override_failure_message(
|
||
"ESC must cancel and close the modal when open"
|
||
).is_true()
|
||
|
||
|
||
# =============================================================================
|
||
# Palette generation (D-165)
|
||
# =============================================================================
|
||
|
||
func test_palette_has_54_swatches() -> void:
|
||
if _scene == null:
|
||
return
|
||
assert_int(_scene._modal_swatch_btns.size()).override_failure_message(
|
||
"D-165 palette must have 54 swatches (6 rows × 9 cols)"
|
||
).is_equal(54)
|
||
|
||
|
||
func test_recent_colors_slots() -> void:
|
||
if _scene == null:
|
||
return
|
||
assert_int(_scene._modal_recent_btns.size()).override_failure_message(
|
||
"Recent colors must have 9 slots per D-165"
|
||
).is_equal(9)
|
||
|
||
|
||
# =============================================================================
|
||
# Search filter helper
|
||
# =============================================================================
|
||
|
||
func test_search_filter_hides_non_matching_buttons() -> void:
|
||
if _scene == null:
|
||
return
|
||
var grid := GridContainer.new()
|
||
add_child(grid)
|
||
|
||
var btn_a := Button.new()
|
||
btn_a.text = "Alpha"
|
||
grid.add_child(btn_a)
|
||
|
||
var btn_b := Button.new()
|
||
btn_b.text = "Beta"
|
||
grid.add_child(btn_b)
|
||
|
||
_scene._apply_search_filter(grid, "alp")
|
||
assert_bool(btn_a.visible).override_failure_message("'Alpha' should be visible for query 'alp'").is_true()
|
||
assert_bool(not btn_b.visible).override_failure_message("'Beta' should be hidden for query 'alp'").is_true()
|
||
|
||
_scene._apply_search_filter(grid, "")
|
||
assert_bool(btn_a.visible).override_failure_message("All buttons visible with empty query").is_true()
|
||
assert_bool(btn_b.visible).override_failure_message("All buttons visible with empty query").is_true()
|
||
|
||
grid.queue_free()
|