Add visual harmonization tests for editor UI components and ground finish chooser

- Implemented `test_editor_visual_harmonization.gd` to validate the Stickman editor and prop editor UI layouts, ensuring proper functionality and visual consistency.
- Created `test_editor_visual_theme.gd` to verify the loading and application of visual themes in the editor, including semantic overrides and default values.
- Introduced `test_phase19_ground_finish_chooser.gd` to test the ground finish chooser's functionality, including layout responsiveness, selection behavior, and interaction with the stage.
- Added new scripts for ground finish preview and chooser UI components to enhance the editor's visual representation of ground finishes.
This commit is contained in:
2026-10-23 15:20:12 -04:00
parent 0a5ef0a61e
commit ddb298f631
40 changed files with 3442 additions and 434 deletions
+235
View File
@@ -0,0 +1,235 @@
extends SceneTree
const SANDBOX_STAGE: PackedScene = preload("res://scenes/stage/sandbox_stage.tscn")
const DIALOG_STYLER: Script = preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
var _checks: int = 0
var _failures: int = 0
var _stage: SandboxStage
var _chooser: StageGroundFinishChooser
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
root.size = Vector2i(1280, 720)
_stage = SANDBOX_STAGE.instantiate() as SandboxStage
root.add_child(_stage)
await _wait_frames(3)
_open_through_palette_signal()
await _wait_frames(3)
_test_initial_gallery()
await _save_capture("phase19_visual_ground_finish_chooser_1280x720.png")
await _test_narrow_viewport_fit()
await _save_capture("phase19_visual_ground_finish_chooser_470x417.png")
_test_close_keeps_placement_unchanged()
root.size = Vector2i(1280, 720)
await _wait_frames(2)
_open_through_palette_signal()
await process_frame
DIALOG_STYLER.dismiss_on_escape(_chooser)
_test_escape_keeps_placement_unchanged()
_open_through_palette_signal()
await _wait_frames(2)
_test_finish_selection_is_a_draft()
await _save_capture("phase19_visual_ground_finish_chooser_dirt_selected.png")
_test_draw_commits_selected_finish()
if DisplayServer.get_name() != "headless":
Input.warp_mouse(Vector2i(900, 420))
await _wait_frames(3)
_test_ground_ghost_is_armed()
await _save_capture("phase19_visual_ground_finish_placement.png")
await _test_ground_click_commits_selected_finish()
_stage.queue_free()
await _wait_frames(3)
if _failures == 0:
print("RESULT: ALL PASSED (%d checks)" % _checks)
quit(0)
else:
print("RESULT: %d FAILURE(S) out of %d checks" % [_failures, _checks])
quit(1)
func _open_through_palette_signal() -> void:
var buttons: Dictionary = _stage.get("_palette_buttons") as Dictionary
var ground_button: AssetCard = buttons.get("ground") as AssetCard
_expect(ground_button != null, "Ground palette card exists")
if ground_button == null:
return
ground_button.set_pressed_no_signal(true)
ground_button.activated.emit(ground_button.payload())
_chooser = _stage.get("_ground_finish_chooser") as StageGroundFinishChooser
func _test_initial_gallery() -> void:
_expect(_chooser != null, "Ground finish chooser is built")
if _chooser == null:
return
_expect(DIALOG_STYLER.is_open(_chooser), "Ground card opens the modal chooser through the palette signal")
_expect(_chooser.title == "Choose Ground", "Chooser title matches the approved gallery")
_expect(_chooser.dialog_text == "Stickmen walk here", "Chooser explains the Ground role")
_expect(_chooser.cancel_button_text == "Close", "Footer dismissal action is Close")
_expect(_chooser.get_ok_button().text == "Draw Grass Ground", "Primary action names the selected finish")
var buttons: Dictionary = _chooser.choice_buttons()
_expect(buttons.size() == 7, "Gallery contains seven exclusive Ground finish choices")
var grass_button: Button = buttons.get(&"finish_ground_grass") as Button
_expect(grass_button != null and grass_button.button_pressed, "Current Grass finish is visibly selected")
_expect(_chooser.selected_preview_finish() == &"finish_ground_grass", "Large sample previews the selected finish")
var grid: GridContainer = _chooser.get("_choice_grid") as GridContainer
var wide_row: HBoxContainer = _chooser.get("_layout_row") as HBoxContainer
var preview_panel: Control = _chooser.get("_preview_panel") as Control
_expect(bool(_chooser.get("_is_wide_layout")) and grid != null and grid.columns == 3, "1280×720 opens the side-by-side three-column gallery")
_expect(grid != null and grid.get_parent() == wide_row and preview_panel != null and preview_panel.visible, "Wide gallery keeps choices beside the selected preview")
var card: Control = _chooser.get_meta(DIALOG_STYLER.CARD_META) as Control
var viewport_size: Vector2 = root.get_visible_rect().size
var card_rect: Rect2 = card.get_global_rect() if card != null else Rect2()
_expect(card != null and card_rect.size.x < viewport_size.x * 0.9, "Roomy chooser card leaves visible horizontal Stage margins")
_expect(card != null and card_rect.size.y < viewport_size.y * 0.82, "Roomy chooser card stays comfortably below the Stage height")
_expect(card != null and card_rect.position.x >= 20.0 and card_rect.end.x <= viewport_size.x - 20.0, "Roomy chooser remains inside the viewport width")
_expect(card != null and card_rect.position.y >= 20.0 and card_rect.end.y <= viewport_size.y - 20.0, "Roomy chooser remains inside the viewport height")
func _test_narrow_viewport_fit() -> void:
if _chooser == null:
return
root.size = Vector2i(470, 417)
await _wait_frames(2)
_chooser.call("_apply_responsive_layout")
DIALOG_STYLER.refit(_chooser)
await _wait_frames(2)
var card: Control = _chooser.get_meta(DIALOG_STYLER.CARD_META) as Control
var viewport_size: Vector2 = root.get_visible_rect().size
var card_rect: Rect2 = card.get_global_rect() if card != null else Rect2()
var grid: GridContainer = _chooser.get("_choice_grid") as GridContainer
var preview_panel: Control = _chooser.get("_preview_panel") as Control
_expect(card != null and card_rect.position.x >= 12.0 and card_rect.end.x <= viewport_size.x - 12.0, "Narrow chooser clamps within horizontal safe margins")
_expect(card != null and card_rect.position.y >= 12.0 and card_rect.end.y <= viewport_size.y - 12.0, "Narrow chooser clamps within vertical safe margins")
_expect(card != null and card_rect.size.x < viewport_size.x and card_rect.size.y < viewport_size.y, "Narrow chooser remains a card instead of a full-scene sheet")
_expect(bool(_chooser.get("_is_compact_layout")) and grid != null and grid.columns == 3, "470×417 uses the compact three-column gallery")
_expect(preview_panel != null and not preview_panel.visible, "Compact gallery hides the redundant large preview")
var body_scroll: ScrollContainer = _chooser.get_meta(DIALOG_STYLER.BODY_SCROLL_META) as ScrollContainer
var body_rect: Rect2 = body_scroll.get_global_rect() if body_scroll != null else Rect2()
var all_choices_visible: bool = grid != null and grid.get_child_count() == 7 and body_scroll != null
if all_choices_visible:
for child: Node in grid.get_children():
var choice: Control = child as Control
var choice_rect: Rect2 = choice.get_global_rect()
if not choice.visible or choice_rect.position.x < body_rect.position.x - 1.0 or choice_rect.end.x > body_rect.end.x + 1.0 or choice_rect.position.y < body_rect.position.y - 1.0 or choice_rect.end.y > body_rect.end.y + 1.0:
all_choices_visible = false
break
_expect(all_choices_visible, "Compact gallery exposes all seven finish cards without scrolling")
var actions: HBoxContainer = _chooser.get_meta(DIALOG_STYLER.ACTIONS_META) as HBoxContainer
var close_button: Button = DIALOG_STYLER.visual_button(_chooser.get_cancel_button())
var draw_button: Button = DIALOG_STYLER.visual_button(_chooser.get_ok_button())
_expect(actions != null and close_button != null and draw_button != null, "Narrow chooser keeps both footer actions in its fixed footer")
_expect(close_button != null and close_button.get_global_rect().intersects(card_rect), "Narrow chooser keeps Close reachable inside the card")
_expect(draw_button != null and draw_button.get_global_rect().intersects(card_rect), "Narrow chooser keeps Draw Ground reachable inside the card")
func _test_close_keeps_placement_unchanged() -> void:
if _chooser == null:
return
var close_button: Button = DIALOG_STYLER.visual_button(_chooser.get_cancel_button())
if close_button != null:
close_button.pressed.emit()
var placement_id: String = String(_stage.get("_placement_id"))
var new_finish: StringName = StringName(_stage.get("_new_ground_finish"))
var ground_button: Button = (_stage.get("_palette_buttons") as Dictionary).get("ground") as Button
_expect(not DIALOG_STYLER.is_open(_chooser), "Close dismisses the chooser")
_expect(placement_id.is_empty(), "Close does not arm Ground placement")
_expect(new_finish == &"finish_ground_grass", "Close discards the unconfirmed finish choice")
_expect(ground_button != null and not ground_button.button_pressed, "Close restores the Ground card to its inactive state")
func _test_escape_keeps_placement_unchanged() -> void:
if _chooser == null:
return
var placement_id: String = String(_stage.get("_placement_id"))
var ground_button: Button = (_stage.get("_palette_buttons") as Dictionary).get("ground") as Button
_expect(not DIALOG_STYLER.is_open(_chooser), "Escape dismisses the chooser")
_expect(placement_id.is_empty(), "Escape does not arm Ground placement")
_expect(ground_button != null and not ground_button.button_pressed, "Escape restores the Ground card to its inactive state")
func _test_finish_selection_is_a_draft() -> void:
if _chooser == null:
return
var dirt_button: Button = _chooser.choice_buttons().get(&"finish_ground_dirt") as Button
if dirt_button != null:
dirt_button.set_pressed_no_signal(true)
dirt_button.pressed.emit()
_expect(_chooser.selected_finish() == &"finish_ground_dirt", "Choosing Dirt updates the draft finish")
_expect(_chooser.selected_preview_finish() == &"finish_ground_dirt", "Choosing Dirt updates the large preview")
_expect(_chooser.get_ok_button().text == "Draw Dirt Ground", "Choosing Dirt updates the primary action")
_expect(StringName(_stage.get("_new_ground_finish")) == &"finish_ground_grass", "Draft selection does not mutate the active Ground finish")
_expect(String(_stage.get("_placement_id")).is_empty(), "Draft selection does not start placement")
func _test_draw_commits_selected_finish() -> void:
if _chooser == null:
return
var draw_button: Button = DIALOG_STYLER.visual_button(_chooser.get_ok_button())
if draw_button != null:
draw_button.pressed.emit()
var painter: StageTerrainPainter = _stage.get("_terrain_painter") as StageTerrainPainter
_expect(not DIALOG_STYLER.is_open(_chooser), "Draw Ground closes the chooser")
_expect(StringName(_stage.get("_new_ground_finish")) == &"finish_ground_dirt", "Draw Ground commits the selected finish")
_expect(String(_stage.get("_placement_id")) == "ground", "Draw Ground enters the existing Ground placement tool")
_expect(painter != null and StringName(painter.get("_ground_finish")) == &"finish_ground_dirt", "Ground painter receives the selected finish")
func _test_ground_ghost_is_armed() -> void:
var ghost: Node2D = _stage.get("_ghost") as Node2D
_expect(ghost != null and is_instance_valid(ghost), "Draw Ground shows the cursor-following Ground ghost")
func _test_ground_click_commits_selected_finish() -> void:
var stage_position := Vector2(900.0, 420.0)
var press := InputEventMouseButton.new()
press.position = stage_position
press.global_position = stage_position
press.button_index = MOUSE_BUTTON_LEFT
press.pressed = true
_stage._unhandled_input(press)
var release := InputEventMouseButton.new()
release.position = stage_position
release.global_position = stage_position
release.button_index = MOUSE_BUTTON_LEFT
release.pressed = false
_stage._unhandled_input(release)
await _wait_frames(3)
var placed_dirt_ground: bool = false
var world: Node2D = _stage.get_node("World") as Node2D
if world != null:
for child: Node in world.get_children():
if child is TerrainBlock and (child as TerrainBlock).spawn_id == "ground" and (child as TerrainBlock).finish_id == &"finish_ground_dirt":
placed_dirt_ground = true
break
_expect(placed_dirt_ground, "Production Ground input places the selected Dirt finish")
func _wait_frames(count: int) -> void:
for _frame_index: int in count:
await process_frame
func _save_capture(file_name: String) -> void:
if DisplayServer.get_name() == "headless":
return
await RenderingServer.frame_post_draw
var capture: Image = root.get_texture().get_image()
var path: String = ProjectSettings.globalize_path("res://tests/" + file_name)
var error: Error = capture.save_png(path)
_expect(error == OK, "render capture saved for visual inspection: %s" % path)
print("CAPTURE: %s size=%s result=%s" % [path, str(capture.get_size()), str(error)])
func _expect(condition: bool, description: String) -> void:
_checks += 1
if condition:
print("PASS: %s" % description)
else:
_failures += 1
push_error("FAIL: %s" % description)