Files
stickman/tests/test_editor_context_menu.gd

210 lines
11 KiB
GDScript

extends SceneTree
const EDITOR_VISUAL_THEME := preload("res://scripts/shared/editor_ui/editor_visual_theme.gd")
const EDITOR_CONTEXT_MENU := preload("res://scripts/shared/editor_ui/editor_context_menu.gd")
const STAGE_DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
const PROP_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn")
const STICKMAN_SCENE := preload("res://scenes/main.tscn")
var _checks: int = 0
var _failures: int = 0
var _last_pressed_id: int = -1
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
root.size = Vector2i(1280, 800)
root.gui_embed_subwindows = true
var visual_theme: EditorVisualTheme = EDITOR_VISUAL_THEME.new()
check("shared editor theme loads", visual_theme.load_from(EDITOR_VISUAL_THEME.DEFAULT_PATH))
var menu: EditorContextMenu = EDITOR_CONTEXT_MENU.new()
root.add_child(menu)
menu.configure(visual_theme, &"prop")
menu.add_item("Copy Shape", 1)
menu.add_separator()
menu.add_destructive_item("Erase Shape", 2)
menu.id_pressed.connect(_on_menu_item_pressed)
var copy_button: Button = menu.get_row_button(1)
var erase_button: Button = menu.get_row_button(2)
check("normal and destructive rows are created", copy_button != null and erase_button != null)
check("rows meet the Studio List target height", copy_button != null and copy_button.custom_minimum_size.y == 48.0)
check("menu rows do not show a keyboard selector on popup", copy_button != null and copy_button.focus_mode == Control.FOCUS_NONE and not copy_button.has_focus())
check("destructive label uses semantic danger color", erase_button != null and erase_button.get_theme_color("font_color").is_equal_approx(visual_theme.chrome_color(&"destructive", &"prop")))
var panel_style: StyleBoxFlat = menu.get_theme_stylebox("panel") as StyleBoxFlat
var hover_style: StyleBoxFlat = copy_button.get_theme_stylebox("hover") as StyleBoxFlat
var focus_style: StyleBoxFlat = copy_button.get_theme_stylebox("focus") as StyleBoxFlat
check("popup shell uses shared raised surface", panel_style != null and panel_style.bg_color.is_equal_approx(visual_theme.chrome_color(&"surface", &"prop")))
check("hover row uses a cyan side cue", hover_style != null and hover_style.border_width_left == 3 and hover_style.border_color.is_equal_approx(visual_theme.chrome_color(&"edit", &"prop")))
check("keyboard focus has a distinct focus outline", focus_style != null and focus_style.border_width_left == 2 and focus_style.border_color.is_equal_approx(visual_theme.chrome_color(&"focus", &"prop")))
root.size = Vector2i(900, 600)
menu.popup_at(Vector2(root.size) - Vector2(4.0, 4.0))
await process_frame
check("popup opens at the requested edge", menu.visible)
check("popup stays inside the viewport", menu.position.x >= 0 and menu.position.y >= 0 and menu.position.x + menu.size.x <= root.size.x and menu.position.y + menu.size.y <= root.size.y)
var down_event := InputEventKey.new()
down_event.keycode = KEY_DOWN
down_event.pressed = true
menu.call("_input", down_event)
await process_frame
check("Down reveals focus on the first row", copy_button.has_focus())
var up_event := InputEventKey.new()
up_event.keycode = KEY_UP
up_event.pressed = true
menu.call("_input", up_event)
await process_frame
check("Up moves focus to the last row", erase_button.has_focus())
menu.call("_on_row_mouse_entered", copy_button)
await process_frame
check("pointer entry hides the keyboard selector", not copy_button.has_focus() and not erase_button.has_focus() and copy_button.focus_mode == Control.FOCUS_NONE)
menu.hide()
var avoid_rect := Rect2(340.0, 240.0, 220.0, 120.0)
menu.popup_at(avoid_rect.get_center(), avoid_rect)
await process_frame
check("popup keeps the clicked target unobstructed when room is available", not Rect2(Vector2(menu.position), Vector2(menu.size)).intersects(avoid_rect))
erase_button.emit_signal("pressed")
await process_frame
check("activating an item emits its id and closes the popup", _last_pressed_id == 2 and not menu.visible)
menu.queue_free()
await process_frame
root.size = Vector2i(1280, 800)
await _test_prop_context_menu()
await _test_stickman_context_menus()
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 _test_prop_context_menu() -> void:
var editor: Control = PROP_SCENE.instantiate() as Control
root.add_child(editor)
await process_frame
await process_frame
_dismiss_fixture_recovery_prompt(editor)
var canvas: PropCanvas = editor.get("_canvas") as PropCanvas
canvas.add_primitive("box", Vector2.ZERO)
var shape: Dictionary = (canvas.get("shapes") as Array)[0] as Dictionary
var bounds: Rect2 = canvas.call("_shape_bbox", shape) as Rect2
var center: Vector2 = bounds.get_center()
var zoom: float = float(canvas.get("_zoom"))
var pan: Vector2 = canvas.get("_pan_offset") as Vector2
var click := InputEventMouseButton.new()
click.global_position = canvas.global_position + center * zoom + pan
canvas.call("_handle_transform_right_click", click, center)
await process_frame
var menu: EditorContextMenu = canvas.get("_context_menu") as EditorContextMenu
check("Prop opens the shared Studio List menu", menu.visible)
check("Prop uses child-readable shape action labels", menu.get_row_button(3) != null and menu.get_row_button(3).text == "Copy Shape" and menu.get_row_button(5).text == "Erase Shape")
check("Prop destructive action uses danger color", menu.get_row_button(5) != null and menu.get_row_button(5).get_theme_color("font_color").is_equal_approx((editor.get("_editor_visual_theme") as EditorVisualTheme).chrome_color(&"destructive", &"prop")))
check("Prop menu remains inside the viewport", menu.position.x >= 0 and menu.position.y >= 0 and menu.position.x + menu.size.x <= root.size.x and menu.position.y + menu.size.y <= root.size.y)
var prop_target: Rect2 = canvas.call("_global_shape_target_rect", 0) as Rect2
check("Prop menu leaves the clicked shape visible", not Rect2(Vector2(menu.position), Vector2(menu.size)).intersects(prop_target))
check("Prop menu avoids an unnecessary scrollbar", not (menu.get_node("MenuShell/MenuScroll") as ScrollContainer).get_v_scroll_bar().visible)
await _save_capture("context_menu_prop.png")
menu.hide()
editor.queue_free()
await process_frame
func _test_stickman_context_menus() -> 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 head: BodyPartPanel = panels.get("head") as BodyPartPanel
head.add_shape(&"rectangle", head.call("drawing_center_world"))
var shape: Dictionary = (head.get("shapes") as Array)[0] as Dictionary
var points: PackedVector2Array = shape.get("points", PackedVector2Array()) as PackedVector2Array
var center := Vector2.ZERO
for point: Vector2 in points:
center += point
if not points.is_empty():
center /= float(points.size())
var click := InputEventMouseButton.new()
var drawing_area: Control = head.get("drawing_area") as Control
var head_zoom: float = float(head.get("_zoom"))
var head_pan: Vector2 = head.get("_pan_offset") as Vector2
click.global_position = drawing_area.global_position + head_pan + center * head_zoom
head.call("_handle_right_click", click, center)
await process_frame
var part_menu: EditorContextMenu = head.get("context_menu") as EditorContextMenu
check("Stickman body-part panels open the shared Studio List menu", part_menu.visible)
check("Stickman shape menu uses the shared shape wording", part_menu.get_row_button(9) != null and part_menu.get_row_button(9).text == "Copy Shape" and part_menu.get_row_button(5).text == "Erase Shape")
check("Stickman Erase Shape is coral", part_menu.get_row_button(5) != null and part_menu.get_row_button(5).get_theme_color("font_color").is_equal_approx((editor.get("_editor_visual_theme") as EditorVisualTheme).chrome_color(&"destructive", &"stickman")))
var part_target: Rect2 = head.call("_global_shape_target_rect", shape) as Rect2
check("Stickman part menu leaves the clicked shape visible", not Rect2(Vector2(part_menu.position), Vector2(part_menu.size)).intersects(part_target))
check("Stickman part menu avoids an unnecessary scrollbar", not (part_menu.get_node("MenuShell/MenuScroll") as ScrollContainer).get_v_scroll_bar().visible)
await _save_capture("context_menu_stickman_part.png")
part_menu.hide()
var preview: WholeStickmanPreview = editor.find_child("WholeStickmanPreview", true, false) as WholeStickmanPreview
var preview_area: Control = preview.get("preview_area") as Control
await process_frame
var part_bounds: Dictionary = preview.get("_part_bounds") as Dictionary
var head_bounds: Rect2 = part_bounds.get("head", Rect2()) as Rect2
var preview_zoom: float = float(preview.get("_zoom"))
var preview_pan: Vector2 = preview.get("_pan_offset") as Vector2
var preview_click := InputEventMouseButton.new()
preview_click.position = head_bounds.get_center() * preview_zoom + preview_pan
preview_click.global_position = preview_area.global_position + preview_click.position
preview.call("_handle_right_click", preview_click)
await process_frame
var preview_menu: EditorContextMenu = preview.get("_context_menu") as EditorContextMenu
check("Whole Stickman preview opens the shared Studio List menu", preview_menu.visible)
check("Whole Stickman uses child-readable flip labels", preview_menu.get_row_button(2) != null and preview_menu.get_row_button(2).text == "Flip Left-Right" and preview_menu.get_row_button(3).text == "Flip Up-Down")
var preview_target := Rect2(preview_area.global_position + preview_pan + head_bounds.position * preview_zoom, head_bounds.size * preview_zoom).grow(4.0)
check("Whole Stickman menu leaves the clicked part visible", not Rect2(Vector2(preview_menu.position), Vector2(preview_menu.size)).intersects(preview_target))
await _save_capture("context_menu_stickman_preview.png")
preview_menu.hide()
main_scene.queue_free()
await process_frame
func _save_capture(file_name: String) -> void:
if DisplayServer.get_name() == "headless":
print("SKIP visual capture under headless renderer: %s" % file_name)
return
var viewport_texture: ViewportTexture = root.get_texture()
if viewport_texture == null:
check("visual capture texture exists: %s" % file_name, false)
return
var image: Image = viewport_texture.get_image()
if image == null or image.is_empty():
check("visual capture image is available: %s" % file_name, false)
return
var path: String = ProjectSettings.globalize_path("res://.godot/" + file_name)
var error: Error = image.save_png(path)
check("visual capture saved: %s" % file_name, error == OK and not image.is_empty())
func _dismiss_fixture_recovery_prompt(editor: Control) -> void:
# A real user autosave may be present in user://. Hide the prompt for clean
# visual captures without choosing Restore or Discard or touching that file.
for child: Node in editor.get_children():
if child is ConfirmationDialog and (child as ConfirmationDialog).title == "Restore Prop?":
STAGE_DIALOG_STYLER.hide(child as ConfirmationDialog)
func _on_menu_item_pressed(id: int) -> void:
_last_pressed_id = id
func check(label: String, condition: bool) -> void:
_checks += 1
if condition:
print("PASS: %s" % label)
else:
_failures += 1
push_error("FAIL: %s" % label)