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)