Files
stickman/tests/test_editor_visual_harmonization.gd

296 lines
20 KiB
GDScript

extends SceneTree
const STICKMAN_SCENE := preload("res://scenes/main.tscn")
const PROP_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn")
const STAGE_DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
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", STAGE_DIALOG_STYLER.is_open(unsaved_dialog))
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 physics tile uses child-facing grip language", str((editor.get("_friction_label") as Label).text) == "Surface grip")
var preset_option: PropInspectorDropdown = editor.get("_preset_option") as PropInspectorDropdown
check("material choices show colored swatches in field and popup", preset_option.item_count == 5 and preset_option.get_item_icon(0) != null and preset_option.get_item_icon(4) != null and preset_option.icon != null)
var material_selected_row: StyleBoxFlat = preset_option.get_popup().get_theme_stylebox("hover") as StyleBoxFlat
var material_items_plain: bool = true
for index: int in preset_option.item_count:
material_items_plain = material_items_plain and not preset_option.get_popup().is_item_checkable(index)
check("material popup uses the full-row selection highlight without radio items", material_items_plain and material_selected_row.bg_color.is_equal_approx(Color("#103B4D")) and material_selected_row.border_color.is_equal_approx(Color("#22C6FF")))
await process_frame
preset_option.show_popup()
await process_frame
check("opening the material popup focuses its current value and spans the field", preset_option.get_popup().get_focused_item() == preset_option.selected and preset_option.get_popup().size.x >= preset_option.size.x)
check("dropdown popup icons reserve a 44 px row target", preset_option.get_popup().get_item_icon(0).get_width() >= 44)
preset_option.hide_popup()
preset_option.get_popup().emit_signal("id_pressed", 1)
check("choosing a material updates the field and editor model", preset_option.selected == 1 and int(editor.get("_material_preset")) == 1)
preset_option.get_popup().emit_signal("id_pressed", 0)
var primitive_option: PropInspectorDropdown = editor.get("_primitive_option") as PropInspectorDropdown
check("2D shape choices show flat icons in field and popup", primitive_option.item_count == 5 and primitive_option.get_item_icon(0) != null and primitive_option.get_item_icon(4) != null and primitive_option.icon != null)
var shape_items_plain: bool = true
for index: int in primitive_option.item_count:
shape_items_plain = shape_items_plain and not primitive_option.get_popup().is_item_checkable(index)
check("shape popup does not draw radio selectors", shape_items_plain)
primitive_option.show_popup()
await process_frame
check("shape popup highlights its current 2D choice", primitive_option.get_popup().get_focused_item() == primitive_option.selected)
primitive_option.hide_popup()
primitive_option.get_popup().emit_signal("id_pressed", 2)
check("choosing a 2D shape updates its parameters", primitive_option.selected == 2 and (editor.get("_param_spins") as Dictionary).has("base"))
primitive_option.get_popup().emit_signal("id_pressed", 0)
var tag_buttons: Dictionary = editor.get("_tag_checks") as Dictionary
var nature_button: Button = tag_buttons.get("nature") as Button
check("artwork tag chip has a colored icon and a wide touch target", nature_button != null and nature_button.icon != null and nature_button.custom_minimum_size.y >= 48.0)
var nature_icon: Texture2D = nature_button.icon
nature_button.set_pressed_no_signal(true)
nature_button.emit_signal("toggled", true)
var selected_tag_style := nature_button.get_theme_stylebox("pressed") as StyleBoxFlat
check("tag selection updates its model", (editor.get("_tags") as Array).has("nature"))
check("selected tag keeps its original colored icon without a badge", nature_button.icon == nature_icon)
check("selected tag style uses a cyan-tinted highlight", selected_tag_style.bg_color.is_equal_approx(Color("#103B4D")))
nature_button.set_pressed_no_signal(false)
nature_button.emit_signal("toggled", false)
check("clearing a tag removes it from the tag model", not (editor.get("_tags") as Array).has("nature"))
check("inspector sections use themed cards", editor.get("_material_section") is PanelContainer and editor.get("_add_section") is PanelContainer and editor.get("_tag_section") is PanelContainer)
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)