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)
|
||||
Reference in New Issue
Block a user