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:
@@ -0,0 +1,251 @@
|
||||
extends SceneTree
|
||||
|
||||
const STICKMAN_SCENE := preload("res://scenes/main.tscn")
|
||||
const PROP_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn")
|
||||
|
||||
var _checks: int = 0
|
||||
var _failures: int = 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("\n=== EDITOR VISUAL HARMONIZATION TEST ===")
|
||||
root.size = Vector2i(1280, 800)
|
||||
await _test_stickman_workspace_and_transactions()
|
||||
await _test_prop_reshape_and_chrome()
|
||||
print("----------------------------------------")
|
||||
if _failures == 0:
|
||||
print("RESULT: ALL PASSED (%d assertions)" % _checks)
|
||||
quit(0)
|
||||
else:
|
||||
print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks])
|
||||
quit(1)
|
||||
|
||||
|
||||
func check(label: String, condition: bool) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print("PASS: %s" % label)
|
||||
else:
|
||||
_failures += 1
|
||||
push_error("FAIL: %s" % label)
|
||||
|
||||
|
||||
func _test_stickman_workspace_and_transactions() -> void:
|
||||
var main_scene: Control = STICKMAN_SCENE.instantiate() as Control
|
||||
root.add_child(main_scene)
|
||||
await process_frame
|
||||
await process_frame
|
||||
var editor: Control = main_scene.find_child("StickmanEditor", true, false) as Control
|
||||
var panels: Dictionary = editor.get("_body_part_panels") as Dictionary
|
||||
var preview: Control = editor.find_child("WholeStickmanPreview", true, false) as Control
|
||||
var menu_bar: Control = editor.get_node("ChromeRoot/MenuBar") as Control
|
||||
var top_bar: Control = editor.get_node("ChromeRoot/TopBar") as Control
|
||||
var workspace_toolbar: Control = editor.get_node("ChromeRoot/WorkspaceToolbar") as Control
|
||||
var workspace_row: Control = editor.get_node("ChromeRoot/WorkspaceRow") as Control
|
||||
var status_bar: Control = editor.get_node("ChromeRoot/StatusBar") as Control
|
||||
var stickman_save_status: Label = editor.get_node("ChromeRoot/StatusBar/SaveStatus") as Label
|
||||
check("Stickman keeps ten live BodyPartPanel instances", panels.size() == 10)
|
||||
check("one permanent Whole Stickman preview exists", preview != null)
|
||||
check("wide layout adapts to Overview", int(editor.call("get_workspace_view")) == 0)
|
||||
check("Stickman chrome rows and workspace do not overlap", top_bar.position.y >= menu_bar.position.y + menu_bar.size.y - 1.0 and workspace_toolbar.position.y >= top_bar.position.y + top_bar.size.y - 1.0 and workspace_row.position.y >= workspace_toolbar.position.y + workspace_toolbar.size.y - 1.0 and status_bar.position.y >= workspace_row.position.y + workspace_row.size.y - 1.0)
|
||||
check("Overview body panels begin below the chrome", (panels.get("head") as Control).get_global_rect().position.y >= workspace_row.get_global_rect().position.y - 1.0)
|
||||
check("clean new Stickman shows explicit unsaved status", stickman_save_status.text == "Not saved yet")
|
||||
|
||||
var head: Control = panels.get("head") as Control
|
||||
var head_id: int = head.get_instance_id()
|
||||
check("head part supports authored shapes", bool(head.call("add_shape", &"rectangle", head.call("drawing_center_world"))))
|
||||
check("shape creation marks the document dirty", bool(editor.call("has_unsaved_changes")))
|
||||
check("Stickman dirty transition shows explicit status", stickman_save_status.text == "Unsaved changes")
|
||||
check("new shape is selected", int(head.call("selected_shape_index")) == 0)
|
||||
|
||||
var zoom: InputEventMouseButton = InputEventMouseButton.new()
|
||||
zoom.button_index = MOUSE_BUTTON_WHEEL_UP
|
||||
zoom.pressed = true
|
||||
zoom.position = Vector2(100.0, 80.0)
|
||||
head.call("_on_drawing_area_gui_input", zoom)
|
||||
var pan_down: InputEventMouseButton = InputEventMouseButton.new()
|
||||
pan_down.button_index = MOUSE_BUTTON_MIDDLE
|
||||
pan_down.pressed = true
|
||||
pan_down.position = Vector2(100.0, 80.0)
|
||||
head.call("_on_drawing_area_gui_input", pan_down)
|
||||
var pan_motion: InputEventMouseMotion = InputEventMouseMotion.new()
|
||||
pan_motion.position = Vector2(112.0, 91.0)
|
||||
head.call("_on_drawing_area_gui_input", pan_motion)
|
||||
var pan_up: InputEventMouseButton = InputEventMouseButton.new()
|
||||
pan_up.button_index = MOUSE_BUTTON_MIDDLE
|
||||
pan_up.pressed = false
|
||||
pan_up.position = Vector2(112.0, 91.0)
|
||||
head.call("_on_drawing_area_gui_input", pan_up)
|
||||
var saved_view: Dictionary = head.call("get_view_context") as Dictionary
|
||||
var saved_pan: Vector2 = saved_view.get("pan_offset", Vector2.ZERO) as Vector2
|
||||
check("panel zoom and pan are active before moving it", float(saved_view.get("zoom", 1.0)) > 1.0 and saved_pan.length() > 0.0)
|
||||
|
||||
var stickman_path: String = "user://editor_visual_harmonization_test.stk"
|
||||
check("Stickman saves through its document API", bool(editor.call("save_to_path", stickman_path)))
|
||||
check("successful save clears dirty state", not bool(editor.call("has_unsaved_changes")))
|
||||
check("saved Stickman shows explicit saved status", stickman_save_status.text == "Saved")
|
||||
var stickman_file: FileAccess = FileAccess.open(stickman_path, FileAccess.READ)
|
||||
var stickman_json: Variant = JSON.parse_string(stickman_file.get_as_text())
|
||||
stickman_file.close()
|
||||
var saved_parts: Dictionary = (stickman_json as Dictionary).get("body_parts", {}) as Dictionary
|
||||
var saved_head: Dictionary = saved_parts.get("head", {}) as Dictionary
|
||||
check("save preserves the existing 1.5 format", str((stickman_json as Dictionary).get("version", "")) == "1.5")
|
||||
check("save includes authored head shapes", (saved_head.get("shapes", []) as Array).size() == 1)
|
||||
check("save still writes guide offset metadata", saved_head.has("guide_offset"))
|
||||
|
||||
var before_switch_view: Dictionary = head.call("get_view_context") as Dictionary
|
||||
editor.call("request_workspace_view", 1, true)
|
||||
check("Focus view keeps the same panel instance", head.get_instance_id() == head_id and int(editor.call("get_workspace_view")) == 1)
|
||||
check("Focus view preserves panel zoom and pan", _view_contexts_equal(before_switch_view, head.call("get_view_context") as Dictionary))
|
||||
check("view choice does not dirty authored content", not bool(editor.call("has_unsaved_changes")))
|
||||
root.size = Vector2i(520, 800)
|
||||
await process_frame
|
||||
editor.call("_refresh_responsive_workspace")
|
||||
check("manual Focus choice survives a narrow resize", int(editor.call("get_workspace_view")) == 1)
|
||||
var whole_stickman_button: Button = editor.get_node("ChromeRoot/WorkspaceToolbar/WholeStickmanButton") as Button
|
||||
var back_to_part_button: Button = editor.get_node("ChromeRoot/WorkspaceToolbar/BackToPartButton") as Button
|
||||
var body_part_workspace: Control = editor.get_node("ChromeRoot/WorkspaceRow/BodyPartWorkspace") as Control
|
||||
check("narrow Focus keeps part controls and Whole Stickman access visible", body_part_workspace.visible and whole_stickman_button.visible and not preview.visible)
|
||||
whole_stickman_button.emit_signal("pressed")
|
||||
await process_frame
|
||||
check("narrow Whole Stickman control opens the permanent preview", not body_part_workspace.visible and preview.visible and back_to_part_button.visible and int(editor.call("get_workspace_view")) == 1)
|
||||
back_to_part_button.emit_signal("pressed")
|
||||
await process_frame
|
||||
check("Back to Part restores the same live panel at narrow width", body_part_workspace.visible and not preview.visible and whole_stickman_button.visible and head.get_instance_id() == head_id)
|
||||
root.size = Vector2i(1280, 800)
|
||||
await process_frame
|
||||
editor.call("_refresh_responsive_workspace")
|
||||
|
||||
editor.call("request_active_part", "torso")
|
||||
check("part picker switches the active focus panel", str(editor.call("get_active_part_name")) == "torso")
|
||||
editor.call("request_active_part", "head")
|
||||
check("switching back restores the original head instance", panels.get("head") == head and head.get_instance_id() == head_id)
|
||||
check("switching parts preserves head geometry and selection", int(head.call("selected_shape_index")) == 0 and (head.get("shapes") as Array).size() == 1)
|
||||
editor.call("request_workspace_view", 0, true)
|
||||
check("Overview restores the same authored panel", int(editor.call("get_workspace_view")) == 0 and head.get_instance_id() == head_id)
|
||||
|
||||
head.call("open_selected_color_picker")
|
||||
var picker: PopupPanel = head.get("color_picker_popup") as PopupPanel
|
||||
check("open color popup blocks presentation switching", picker.visible and not bool(head.call("can_switch_presentation")))
|
||||
editor.call("request_active_part", "torso")
|
||||
editor.call("request_workspace_view", 1, true)
|
||||
check("deferred view switch waits while popup is open", int(editor.call("get_workspace_view")) == 0 and str(editor.call("get_active_part_name")) == "head")
|
||||
picker.hide()
|
||||
await process_frame
|
||||
await process_frame
|
||||
check("deferred part and view switch resumes after popup closes", int(editor.call("get_workspace_view")) == 1 and str(editor.call("get_active_part_name")) == "torso")
|
||||
check("popup and workspace changes remain clean", not bool(editor.call("has_unsaved_changes")))
|
||||
editor.call("request_active_part", "head")
|
||||
var preview_area: Control = preview.get("preview_area") as Control
|
||||
await process_frame
|
||||
var raw_points: PackedVector2Array = preview.call("_collect_all_raw_points", "head") as PackedVector2Array
|
||||
var part_center: Vector2 = preview.call("_compute_center", raw_points) as Vector2
|
||||
var initial_part_position: Vector2 = preview.call("get_part_position", "head") as Vector2
|
||||
var mouse_down: InputEventMouseButton = InputEventMouseButton.new()
|
||||
mouse_down.button_index = MOUSE_BUTTON_LEFT
|
||||
mouse_down.pressed = true
|
||||
mouse_down.position = part_center
|
||||
preview.call("_on_preview_gui_input", mouse_down)
|
||||
var drag: InputEventMouseMotion = InputEventMouseMotion.new()
|
||||
drag.position = part_center + Vector2(16.0, 12.0)
|
||||
preview.call("_on_preview_gui_input", drag)
|
||||
var mouse_up: InputEventMouseButton = InputEventMouseButton.new()
|
||||
mouse_up.button_index = MOUSE_BUTTON_LEFT
|
||||
mouse_up.pressed = false
|
||||
mouse_up.position = drag.position
|
||||
preview.call("_on_preview_gui_input", mouse_up)
|
||||
check("preview drag commits an authored part transform", not (preview.call("get_part_position", "head") as Vector2).is_equal_approx(initial_part_position))
|
||||
check("committed preview transform marks document dirty", bool(editor.call("has_unsaved_changes")))
|
||||
check("preview input uses the live preview area", preview_area != null and preview_area.size.x > 0.0)
|
||||
check("save after transform keeps the existing format", bool(editor.call("save_to_path", stickman_path)) and not bool(editor.call("has_unsaved_changes")))
|
||||
preview.call("_mirror_part_x")
|
||||
check("mirror transform marks document dirty", bool(editor.call("has_unsaved_changes")) and (preview.call("get_part_scale", "head") as Vector2).x < 0.0)
|
||||
check("mirror transform can be saved", bool(editor.call("save_to_path", stickman_path)) and not bool(editor.call("has_unsaved_changes")))
|
||||
preview.call("_bring_forward_part")
|
||||
check("part order edit marks document dirty", bool(editor.call("has_unsaved_changes")))
|
||||
|
||||
check("second shape can be authored", bool(head.call("add_shape", &"line", head.call("drawing_center_world"))))
|
||||
editor.call("_on_file_menu_id_pressed", 2)
|
||||
var unsaved_dialog: ConfirmationDialog = editor.get("_unsaved_dialog") as ConfirmationDialog
|
||||
check("Start Over shows a save/discard/cancel transaction", unsaved_dialog.visible)
|
||||
unsaved_dialog.get_cancel_button().emit_signal("pressed")
|
||||
await process_frame
|
||||
check("Cancel leaves dirty content in place", bool(editor.call("has_unsaved_changes")) and (head.get("shapes") as Array).size() == 2)
|
||||
editor.call("_on_file_menu_id_pressed", 2)
|
||||
unsaved_dialog.emit_signal("custom_action", "discard")
|
||||
await process_frame
|
||||
check("Discard clears authored parts and dirty state", not bool(editor.call("has_unsaved_changes")) and (head.get("shapes") as Array).is_empty())
|
||||
check("clean new Stickman returns to explicit unsaved status", stickman_save_status.text == "Not saved yet")
|
||||
check("Start Over keeps the selected workspace view", int(editor.call("get_workspace_view")) == 1)
|
||||
DirAccess.remove_absolute(stickman_path)
|
||||
main_scene.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _test_prop_reshape_and_chrome() -> void:
|
||||
var editor: Control = PROP_SCENE.instantiate() as Control
|
||||
root.add_child(editor)
|
||||
await process_frame
|
||||
await process_frame
|
||||
var canvas: Control = editor.get("_canvas") as Control
|
||||
var save_status: Label = editor.get("_save_status") as Label
|
||||
check("Prop chrome uses wrapping top and mode rows", editor.get("_top_right") is FlowContainer and editor.get("_mode_bar") is FlowContainer)
|
||||
check("Prop action target meets 44 px minimum", (editor.get("_create_point_button") as Button).custom_minimum_size.y >= 44.0)
|
||||
check("new Prop has an explicit clean save status", str(save_status.text).ends_with("Not saved yet"))
|
||||
check("whole-prop action uses approved discoverable wording", str((editor.get("_whole_prop_resize_button") as Button).text) == "Move, Turn, or Resize Prop")
|
||||
check("material inspector uses child-facing grip language", str((editor.get("_friction_label") as Label).text).begins_with("Surface grip:"))
|
||||
check("anchor layer control uses the approved label", (editor.get("_anchor_z") as SpinBox).get_parent().get_child(0) is Label and str(((editor.get("_anchor_z") as SpinBox).get_parent().get_child(0) as Label).text) == "Held prop layer:")
|
||||
|
||||
canvas.call("add_primitive", "box", Vector2(150.0, 150.0))
|
||||
var shapes: Array = canvas.call("get_shape_data") as Array
|
||||
check("prop primitive is selected for editing", int(canvas.call("get_selected_index")) == 0 and shapes.size() == 1)
|
||||
check("Prop edit shows explicit unsaved status", str(save_status.text).ends_with("Unsaved changes"))
|
||||
var prop_path: String = "user://editor_visual_harmonization_test.prop"
|
||||
var save_result: Variant = await editor.call("_save_to", prop_path, false)
|
||||
check("clean Prop save shows explicit saved status", bool(save_result) and str(save_status.text).ends_with("Saved"))
|
||||
editor.call("_on_points_mode_pressed")
|
||||
var create_button: Button = editor.get("_create_point_button") as Button
|
||||
check("Reshape exposes the Create Point action", create_button.visible and create_button.text == "📍 Create Point")
|
||||
create_button.emit_signal("pressed")
|
||||
check("Create Point arms with a visible Esc hint", bool(canvas.call("is_create_point_armed")) and str((editor.get("_create_point_hint") as Label).text).contains("Press Esc"))
|
||||
var escape: InputEventKey = InputEventKey.new()
|
||||
escape.keycode = KEY_ESCAPE
|
||||
escape.pressed = true
|
||||
editor.call("_unhandled_key_input", escape)
|
||||
check("Esc cancels the armed point action", not bool(canvas.call("is_create_point_armed")))
|
||||
create_button.emit_signal("pressed")
|
||||
var internal_shapes: Array = canvas.get("shapes") as Array
|
||||
var first_shape: Dictionary = internal_shapes[0] as Dictionary
|
||||
var original_points: PackedVector2Array = first_shape.get("points", PackedVector2Array()) as PackedVector2Array
|
||||
var edge_midpoint: Vector2 = (original_points[0] + original_points[1]) * 0.5
|
||||
var canvas_zoom: float = float(canvas.get("_zoom"))
|
||||
var canvas_pan: Vector2 = canvas.get("_pan_offset") as Vector2
|
||||
var edge_screen_pos: Vector2 = edge_midpoint * canvas_zoom + canvas_pan
|
||||
check("toolbar click target resolves to the visible shape edge", int(canvas.call("hit_test_segment", edge_midpoint)) == 0)
|
||||
var edge_click: InputEventMouseButton = InputEventMouseButton.new()
|
||||
edge_click.button_index = MOUSE_BUTTON_LEFT
|
||||
edge_click.pressed = true
|
||||
edge_click.position = edge_screen_pos
|
||||
canvas.call("_on_canvas_gui_input", edge_click)
|
||||
shapes = canvas.call("get_shape_data") as Array
|
||||
var reshaped: Dictionary = shapes[0] as Dictionary
|
||||
var vertices: PackedVector2Array = reshaped.get("points", PackedVector2Array()) as PackedVector2Array
|
||||
check("armed edge click inserts a point", vertices.size() == 5)
|
||||
check("inserted point records reshaped state", bool(reshaped.get("reshaped", false)))
|
||||
check("inserted point marks Prop document dirty", bool(editor.get("_dirty")))
|
||||
check("Create Point updates Prop status to unsaved", str(save_status.text).ends_with("Unsaved changes"))
|
||||
check("one click disarms Create Point", not bool(canvas.call("is_create_point_armed")))
|
||||
if FileAccess.file_exists(prop_path):
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(prop_path))
|
||||
editor.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _view_contexts_equal(left: Dictionary, right: Dictionary) -> bool:
|
||||
var left_pan: Vector2 = left.get("pan_offset", Vector2.ZERO) as Vector2
|
||||
var right_pan: Vector2 = right.get("pan_offset", Vector2.ZERO) as Vector2
|
||||
return is_equal_approx(float(left.get("zoom", 1.0)), float(right.get("zoom", 1.0))) and left_pan.is_equal_approx(right_pan)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bulw0ym7dnnaj
|
||||
@@ -0,0 +1,72 @@
|
||||
extends SceneTree
|
||||
|
||||
const EDITOR_VISUAL_THEME := preload("res://scripts/shared/editor_ui/editor_visual_theme.gd")
|
||||
|
||||
var _checks: int = 0
|
||||
var _failures: int = 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var defaults: EditorVisualTheme = EDITOR_VISUAL_THEME.new()
|
||||
check("committed shared theme loads", defaults.load_from(EDITOR_VISUAL_THEME.DEFAULT_PATH))
|
||||
check("default body font is 18", defaults.chrome_px(&"body", &"prop") == 18)
|
||||
check("default touch target is 44 px", defaults.chrome_px(&"button_min_height", &"stickman") == 44)
|
||||
check("preview role preserves alpha", is_equal_approx(defaults.chrome_color(&"preview").a, 0.4))
|
||||
|
||||
var custom_path: String = "user://editor_visual_theme_test.json"
|
||||
var custom_file: FileAccess = FileAccess.open(custom_path, FileAccess.WRITE)
|
||||
custom_file.store_string('{"chrome":{"colors":{"edit":"#22C6FF","focus":"#FF00AA"},"targets":{"button_min_height":48}},"editors":{"prop":{"colors":{"edit":"#FF8800"}}}}')
|
||||
custom_file.close()
|
||||
var custom: EditorVisualTheme = EDITOR_VISUAL_THEME.new()
|
||||
check("custom shared theme loads", custom.load_from(custom_path))
|
||||
check("prop semantic override wins", custom.chrome_color(&"edit", &"prop").is_equal_approx(Color("#FF8800")))
|
||||
check("stickman uses common semantic default", custom.chrome_color(&"edit", &"stickman").is_equal_approx(Color("#22C6FF")))
|
||||
check("common touch target override applies", custom.chrome_px(&"button_min_height", &"prop") == 48)
|
||||
check("local focus token overrides default", custom.chrome_color(&"focus").is_equal_approx(Color("#FF00AA")))
|
||||
DirAccess.remove_absolute(custom_path)
|
||||
|
||||
var invalid_path: String = "user://editor_visual_theme_test_invalid.json"
|
||||
var invalid_file: FileAccess = FileAccess.open(invalid_path, FileAccess.WRITE)
|
||||
invalid_file.store_string('{"chrome":{"colors":{"edit":"electric"},"targets":{"button_min_height":"large"}}}')
|
||||
invalid_file.close()
|
||||
var invalid: EditorVisualTheme = EDITOR_VISUAL_THEME.new()
|
||||
invalid.load_from(invalid_path)
|
||||
check("invalid color falls back to semantic default", invalid.chrome_color(&"edit").is_equal_approx(Color("#22C6FF")))
|
||||
check("invalid size falls back to semantic default", invalid.chrome_px(&"button_min_height") == 44)
|
||||
DirAccess.remove_absolute(invalid_path)
|
||||
|
||||
var prop_theme: Theme = defaults.control_theme(&"prop")
|
||||
var stickman_theme: Theme = defaults.control_theme(&"stickman")
|
||||
var prop_focus: StyleBoxFlat = prop_theme.get_stylebox("focus", "Button") as StyleBoxFlat
|
||||
var stickman_focus: StyleBoxFlat = stickman_theme.get_stylebox("focus", "Button") as StyleBoxFlat
|
||||
check("each editor receives a separate local Theme", prop_theme.get_instance_id() != stickman_theme.get_instance_id())
|
||||
check("each editor receives separate focus styles", prop_focus.get_instance_id() != stickman_focus.get_instance_id())
|
||||
check("focus outline uses semantic focus token", prop_focus.border_color.is_equal_approx(defaults.chrome_color(&"focus")))
|
||||
|
||||
var action: Button = Button.new()
|
||||
defaults.style_button_role(action, &"primary", &"prop")
|
||||
var action_box: StyleBoxFlat = action.get_theme_stylebox("normal") as StyleBoxFlat
|
||||
check("primary action uses semantic action color", action_box.bg_color.is_equal_approx(defaults.chrome_color(&"action_primary", &"prop")))
|
||||
check("primary action reaches the minimum target size", action.custom_minimum_size.y >= 44.0)
|
||||
action.free()
|
||||
|
||||
print("----------------------------------------")
|
||||
if _failures == 0:
|
||||
print("RESULT: ALL PASSED (%d assertions)" % _checks)
|
||||
quit(0)
|
||||
else:
|
||||
print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks])
|
||||
quit(1)
|
||||
|
||||
|
||||
func check(label: String, condition: bool) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print("PASS: %s" % label)
|
||||
else:
|
||||
_failures += 1
|
||||
push_error("FAIL: %s" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cmve03xjcfcg8
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c1fx16sotrdpp
|
||||
@@ -20,8 +20,9 @@ func _check(label: String, value: bool) -> void:
|
||||
push_error("FAIL: %s" % label)
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1200, 800)
|
||||
var theme := THEME.new()
|
||||
_check("default inspector background is #262C39", theme.inspector_background_color() == Color("#262C39"))
|
||||
_check("default inspector background uses shared surface token", theme.inspector_background_color().is_equal_approx(Color("#252B38FA")))
|
||||
var preview := PREVIEW_SCENE.instantiate() as Control
|
||||
root.add_child(preview)
|
||||
await process_frame
|
||||
@@ -39,7 +40,6 @@ func _run() -> void:
|
||||
_check("front pose stops gait", int(preview.get("_motion")) == 0 and bool(preview.get("_walk_paused")))
|
||||
preview.queue_free()
|
||||
var editor := CANVAS_SCENE.instantiate() as Control
|
||||
editor.size = Vector2(1200.0, 800.0)
|
||||
root.add_child(editor)
|
||||
await process_frame
|
||||
var canvas := editor.get("_canvas") as PropCanvas
|
||||
@@ -95,12 +95,12 @@ func _run() -> void:
|
||||
var eye_data: Dictionary = PROP_FILE.load("res://content/props/eye.prop")
|
||||
_check("eye prop fixture is available", not eye_data.is_empty())
|
||||
editor.call("_apply_json_data", eye_data)
|
||||
editor.size = Vector2(1200.0, 600.0)
|
||||
root.size = Vector2i(1200, 600)
|
||||
canvas.set_edit_mode(PropCanvas.EditMode.ATTACH)
|
||||
await process_frame
|
||||
await process_frame
|
||||
var top_bar := editor.get_node("TopBar") as Control
|
||||
var status_bar := editor.get_node("StatusBar") as Control
|
||||
var top_bar := editor.get_node("ChromeRoot/TopBar") as Control
|
||||
var status_bar := editor.get_node("ChromeRoot/StatusBar") as Control
|
||||
var holding_preview := editor.get("_attach_preview") as Control
|
||||
_check("Attach layout stays between name and status bars", main_split.get_global_rect().position.y >= top_bar.get_global_rect().end.y - 1.0 and holding_preview.get_global_rect().end.y <= status_bar.get_global_rect().position.y + 1.0)
|
||||
var preview_container := holding_preview.get("_viewport_container") as SubViewportContainer
|
||||
|
||||
@@ -146,25 +146,24 @@ func _test_ui_tokens() -> void:
|
||||
var theme: PropTheme = PROP_THEME.new()
|
||||
theme.load_from(PROP_THEME.DEFAULT_PATH)
|
||||
check("font_body 18", theme.font_body() == 18)
|
||||
check("font_header 24", theme.font_header() == 24)
|
||||
check("font_menu 22", theme.font_menu() == 22)
|
||||
check("font_menu_bar 22", theme.font_menu_bar() == 22)
|
||||
check("font_mode 26", theme.font_mode() == 26)
|
||||
check("font_status 18", theme.font_status() == 18)
|
||||
check("font_header 22", theme.font_header() == 22)
|
||||
check("font_menu 20", theme.font_menu() == 20)
|
||||
check("font_menu_bar 20", theme.font_menu_bar() == 20)
|
||||
check("font_mode 24", theme.font_mode() == 24)
|
||||
check("font_status 16", theme.font_status() == 16)
|
||||
check("font_prop_name 20", theme.font_prop_name() == 20)
|
||||
check("font_context_menu 22", theme.font_context_menu() == 22)
|
||||
check("button_min_height 36", theme.button_min_height() == 36)
|
||||
check("row_min_height 36", theme.row_min_height() == 36)
|
||||
check("font_context_menu 20", theme.font_context_menu() == 20)
|
||||
check("button_min_height 44", theme.button_min_height() == 44)
|
||||
check("row_min_height 44", theme.row_min_height() == 44)
|
||||
check("mode_button_min_height 56", theme.mode_button_min_height() == 56)
|
||||
check("text_color token", theme.text_color().is_equal_approx(Color.from_string("#F2F2F2", Color.WHITE)))
|
||||
check("text_muted token", theme.text_muted_color().is_equal_approx(Color.from_string("#9A9A9A", Color.WHITE)))
|
||||
check("text_color token", theme.text_color().is_equal_approx(Color.from_string("#F5F7FA", Color.WHITE)))
|
||||
check("text_muted token", theme.text_muted_color().is_equal_approx(Color.from_string("#AAB2C0", Color.WHITE)))
|
||||
check("border token", theme.border_color().is_equal_approx(Color.from_string("#FFFFFF33", Color.WHITE)))
|
||||
check("accent token", theme.accent_color().is_equal_approx(Color.from_string("#4FA3FF", Color.WHITE)))
|
||||
check("accent token", theme.accent_color().is_equal_approx(Color.from_string("#22C6FF", Color.WHITE)))
|
||||
var path := "user://prop_theme_test_ui.json"
|
||||
_write_user_theme('{"version":"1.0","ui":{"fonts":{"body":24},"colors":{"accent":"#FF8800"}}}', path)
|
||||
var custom: PropTheme = PROP_THEME.new()
|
||||
custom.load_from(path)
|
||||
check("ui font override", custom.font_body() == 24)
|
||||
check("unoverridden ui token falls back", custom.font_header() == 24)
|
||||
check("ui accent override", custom.accent_color().is_equal_approx(Color.from_string("#FF8800", Color.WHITE)))
|
||||
check("legacy PropTheme ui font does not override shared tokens", custom.font_body() == 18)
|
||||
check("legacy PropTheme ui accent does not override shared tokens", custom.accent_color().is_equal_approx(Color.from_string("#22C6FF", Color.WHITE)))
|
||||
DirAccess.remove_absolute(path)
|
||||
|
||||
Reference in New Issue
Block a user