diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.gd b/addons/gdUnit4/src/ui/GdUnitInspector.gd index 55b225ec..18e57aa4 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspector.gd +++ b/addons/gdUnit4/src/ui/GdUnitInspector.gd @@ -47,9 +47,9 @@ enum EDITOR_ACTIONS { WINDOW_SELECT_BASE = 100 } -const MENU_ID_TEST_RUN := 1000 -const MENU_ID_TEST_DEBUG := 1001 -const MENU_ID_CREATE_TEST := 1010 +const MENU_ID_TEST_RUN := GdUnitContextMenuItem.MENU_ID.TEST_RUN +const MENU_ID_TEST_DEBUG := GdUnitContextMenuItem.MENU_ID.TEST_DEBUG +const MENU_ID_CREATE_TEST := GdUnitContextMenuItem.MENU_ID.CREATE_TEST # header @onready var _runButton :Button = $VBoxContainer/Header/ToolBar/Tools/run @@ -77,10 +77,19 @@ func _ready(): if Engine.is_editor_hint(): _getEditorThemes(_editor_interface) add_file_system_dock_context_menu() - add_script_editor_context_menu() # preload previous test execution _runner_config.load() + +func _enter_tree(): + if Engine.is_editor_hint(): + add_script_editor_context_menu() + + +func _exit_tree(): + ScriptEditorControls.unregister_context_menu() + + func _process(_delta): _check_test_run_stopped_manually() @@ -151,84 +160,44 @@ func _on_file_system_dock_context_menu_pressed(id :int, file_tree :Tree) -> void var debug = id == MENU_ID_TEST_DEBUG run_test_suites(selected_test_suites, debug) + func add_script_editor_context_menu(): - if _editor_interface == null: - return - var script_editor := _editor_interface.get_script_editor() - # register tab changed to modify the context menu for all script editors - var tab_containers := GdObjects.find_nodes_by_class(script_editor, "TabContainer", true) - var tab_container := tab_containers[0] as TabContainer - if not tab_container.is_connected("tab_changed", Callable(self, "_on_script_editor_tab_changed")): - tab_container.connect("tab_changed", Callable(self, "_on_script_editor_tab_changed").bind(tab_container)) - -func _on_script_editor_tab_changed(tab_index :int, tab_container :TabContainer): - var tab := tab_container.get_tab_control(tab_index) - # we only extend context menu for script editors - if tab.get_class() == "ScriptTextEditor": - var current_script := _editor_interface.get_script_editor().get_current_script() - if current_script != null: - extend_script_editor_popup(tab) - -func extend_script_editor_popup(tab_container :Control) -> void: - # find editor popup menus - var popups := GdObjects.find_nodes_by_class(tab_container, "PopupMenu", true) - # find the underlaying text editor (need for grab current cursor position) - var text_edits := GdObjects.find_nodes_by_class(tab_container, "CodeEdit", true) - # editor is not loaded yet? - if text_edits.size() == 0: - return - var text_edit :TextEdit = text_edits[0] as TextEdit + var is_test_suite := func is_visible(script :GDScript, is_test_suite :bool): + return GdObjects.is_test_suite(script) == is_test_suite + var is_enabled := func is_enabled(script :GDScript): + return !_runButton.disabled + var run_test := func run_test(script :Script, text_edit :TextEdit, debug :bool): + var cursor_line := text_edit.get_caret_line() + #run test case? + var regex := RegEx.new() + regex.compile("(^func[ ,\t])(test_[a-zA-Z0-9_]*)") + var result := regex.search(text_edit.get_line(cursor_line)) + if result: + var func_name := result.get_string(2).strip_edges() + prints("Run test:", func_name, "debug", debug) + if func_name.begins_with("test_"): + run_test_case(script.resource_path, func_name, debug) + return + # otherwise run the full test suite + var selected_test_suites := [script.resource_path] + run_test_suites(selected_test_suites, debug) + var create_test := func create_test(script :Script, text_edit :TextEdit): + var cursor_line := text_edit.get_caret_line() + var result = GdUnitTestSuiteBuilder.create(script, cursor_line) + if result.is_error(): + # show error dialog + push_error("Failed to create test case: %s" % result.error_message()) + return + var info := result.value() as Dictionary + ScriptEditorControls.edit_script(info.get("path"), info.get("line")) - for popup in popups: - if not popup.is_connected("about_to_popup", Callable(self, '_on_script_editor_context_menu_show')): - popup.connect("about_to_popup", Callable(self, '_on_script_editor_context_menu_show').bind(popup)) - if not popup.is_connected("id_pressed", Callable(self, '_on_fscript_editor_context_menu_pressed')): - popup.connect("id_pressed", Callable(self, "_on_fscript_editor_context_menu_pressed").bind(text_edit)) - -func _on_script_editor_context_menu_show(context_menu :PopupMenu): - var current_script := _editor_interface.get_script_editor().get_current_script() - if GdObjects.is_test_suite(current_script): - context_menu.add_separator() - # save menu entry index - var current_index := context_menu.get_item_count() - context_menu.add_item("Run Tests", MENU_ID_TEST_RUN) - context_menu.add_item("Debug Tests", MENU_ID_TEST_DEBUG) - # deactivate menu enties if currently a run in progress - context_menu.set_item_disabled(current_index+0, _runButton.disabled) - context_menu.set_item_disabled(current_index+1, _runButton.disabled) - return - context_menu.add_separator() - # save menu entry index - var current_index := context_menu.get_item_count() - context_menu.add_item("Create Test", MENU_ID_CREATE_TEST) + var menu := [ + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Tests", is_test_suite.bind(true), is_enabled, run_test.bind(false)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_DEBUG, "Debug Tests", is_test_suite.bind(true), is_enabled, run_test.bind(true)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.CREATE_TEST, "Create Test", is_test_suite.bind(false), is_enabled, create_test) + ] + ScriptEditorControls.register_context_menu(menu) -func _on_fscript_editor_context_menu_pressed(id :int, text_edit :TextEdit): - if id != MENU_ID_TEST_RUN && id != MENU_ID_TEST_DEBUG && id != MENU_ID_CREATE_TEST: - return - var current_script := ScriptEditorControls.script_editor().get_current_script() - if current_script == null: - prints("no script selected") - return - var cursor_line := text_edit.get_caret_line() - # create new test case? - if id == MENU_ID_CREATE_TEST: - add_test_to_test_suite(current_script, cursor_line) - return - # run test case? - var regex := RegEx.new() - regex.compile("(^func[ ,\t])(test_[a-zA-Z0-9_]*)") - var result := regex.search(text_edit.get_line(cursor_line)) - var debug = id == MENU_ID_TEST_DEBUG - if result: - var func_name := result.get_string(2).strip_edges() - prints("Run test:", func_name, "debug", debug) - if func_name.begins_with("test_"): - run_test_case(current_script.resource_path, func_name, debug) - return - # otherwise run the full test suite - var selected_test_suites := [current_script.resource_path] - run_test_suites(selected_test_suites, debug) -# ------------------------------------------------------------------------------------ func run_test_suites(test_suite_paths :Array, debug :bool, rerun :bool=false) -> void: # create new runner runner_config for fresh run otherwise use saved one @@ -252,6 +221,7 @@ func run_test_case(test_suite_resource_path :String, test_case :String, debug :b return _gdUnit_run(debug) + func _gdUnit_run(debug :bool) -> void: # don't start is already running if _is_running: @@ -304,16 +274,6 @@ func _gdUnit_stop(client_id :int) -> void: _current_runner_process_id = -1 -func add_test_to_test_suite(source_script :Script, current_line_number :int) -> void: - var result = GdUnitTestSuiteBuilder.create(source_script, current_line_number) - if result.is_error(): - # show error dialog - push_error("Failed to create test case: %s" % result.error_message()) - return - var info := result.value() as Dictionary - ScriptEditorControls.edit_script(info.get("path"), info.get("line")) - - ################################################################################ # Event signal receiver ################################################################################ diff --git a/addons/gdUnit4/src/ui/ScriptEditorControls.gd b/addons/gdUnit4/src/ui/ScriptEditorControls.gd index e032ecc1..137888e9 100644 --- a/addons/gdUnit4/src/ui/ScriptEditorControls.gd +++ b/addons/gdUnit4/src/ui/ScriptEditorControls.gd @@ -2,6 +2,7 @@ class_name ScriptEditorControls extends RefCounted + # https://github.com/godotengine/godot/blob/master/editor/plugins/script_editor_plugin.h # the Editor menu popup items enum { @@ -56,6 +57,9 @@ static func script_editor() -> ScriptEditor: # The script is saved when is opened in the editor. # The script is closed when is set to true. static func save_an_open_script(script_path :String, close := false) -> bool: + #prints("save_an_open_script", script_path, close) + if !Engine.is_editor_hint(): + return false var editor_interface := editor_interface() var script_editor := script_editor() var editor_popup := _menu_popup() @@ -64,12 +68,11 @@ static func save_an_open_script(script_path :String, close := false) -> bool: for open_script in script_editor.get_open_scripts(): if open_script.resource_path == script_path: # select the script in the editor - #var e: ScriptEditorBase = script_editor.get_open_script_editors()[open_file_index] editor_interface.edit_script(open_script, 0); # save and close - editor_popup.emit_signal("id_pressed", FILE_SAVE) + editor_popup.id_pressed.emit(FILE_SAVE) if close: - editor_popup.emit_signal("id_pressed", FILE_CLOSE) + editor_popup.id_pressed.emit(FILE_CLOSE) return true open_file_index +=1 return false @@ -77,7 +80,8 @@ static func save_an_open_script(script_path :String, close := false) -> bool: # Saves all opened script static func save_all_open_script() -> void: - _menu_popup().emit_signal("id_pressed", FILE_SAVE_ALL) + if Engine.is_editor_hint(): + _menu_popup().id_pressed.emit(FILE_SAVE_ALL) # Edits the given script. @@ -95,6 +99,26 @@ static func edit_script(script_path :String, line_number :int = -1): editor_interface.edit_script(script, line_number) +# Register the given context menu to the current script editor +# Is called when the plugin is activated +# The active script is connected to the ScriptEditorContextMenuHandler +static func register_context_menu(menu :Array) -> void: + script_editor().editor_script_changed.connect(ScriptEditorContextMenuHandler.create(menu).bind(script_editor())) + + +# Unregisteres all registerend context menus and gives the ScriptEditorContextMenuHandler> free +# Is called when the plugin is deactivated +static func unregister_context_menu() -> void: + for connection in script_editor().editor_script_changed.get_connections(): + var cb :Callable = connection["callable"] + if cb.get_object() is ScriptEditorContextMenuHandler: + cb.get_object().free() + + +static func filesystem_add_context_menu() -> void: + pass + + static func _menu_popup() -> PopupMenu: return script_editor().get_child(0).get_child(0).get_child(0).get_popup() diff --git a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd new file mode 100644 index 00000000..0eea89e3 --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd @@ -0,0 +1,45 @@ +class_name GdUnitContextMenuItem + +enum MENU_ID { + TEST_RUN = 1000, + TEST_DEBUG = 1001, + CREATE_TEST = 1010, +} + +var _is_visible :Callable +var _is_enabled :Callable +var _runnable: Callable + + +func _init(id :MENU_ID, name :StringName, is_visible :Callable, is_enabled: Callable, runnable: Callable): + self.id = id + self.name = name + _is_visible = is_visible + _is_enabled = is_enabled + _runnable = runnable + + +var id: MENU_ID: + set(value): + id = value + get: + return id + + +var name: StringName: + set(value): + name = value + get: + return name + + +func is_enabled(script :GDScript) -> bool: + return _is_enabled.call(script) + + +func is_visible(script :GDScript) -> bool: + return _is_visible.call(script) + + +func execute(args :Array) -> void: + _runnable.callv(args) diff --git a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd new file mode 100644 index 00000000..ff074bb4 --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd @@ -0,0 +1,44 @@ +class_name ScriptEditorContextMenuHandler +extends Object + +var _context_menus := Dictionary() + + +func _init(context_menus :Array[GdUnitContextMenuItem]): + for menu in context_menus: + _context_menus[menu.id] = menu + + +static func create(context_menus :Array[GdUnitContextMenuItem]) -> Callable: + return Callable(ScriptEditorContextMenuHandler.new(context_menus), "on_script_changed") + + +func on_script_changed(script :GDScript, editor :ScriptEditor): + #prints("ContextMenuHandler:on_script_changed", script, editor) + var current_editor := editor.get_current_editor() + var popups := GdObjects.find_nodes_by_class(current_editor, "PopupMenu", true) + for popup in popups: + if not popup.is_connected("about_to_popup", Callable(self, 'on_context_menu_show')): + popup.connect("about_to_popup", Callable(self, 'on_context_menu_show').bind(script, popup)) + if not popup.is_connected("id_pressed", Callable(self, 'on_context_menu_pressed')): + popup.connect("id_pressed", Callable(self, "on_context_menu_pressed").bind(script, current_editor.get_base_editor())) + + +func on_context_menu_show(script :GDScript, context_menu :PopupMenu): + #prints("on_context_menu_show", _context_menus.keys(), context_menu, self) + context_menu.add_separator() + var current_index := context_menu.get_item_count() + for menu_id in _context_menus.keys(): + var menu_item :GdUnitContextMenuItem = _context_menus[menu_id] + if menu_item.is_visible(script): + context_menu.add_item(menu_item.name, menu_id) + context_menu.set_item_disabled(current_index, !menu_item.is_enabled(script)) + current_index += 1 + + +func on_context_menu_pressed(id :int, script :GDScript, text_edit :TextEdit): + #prints("on_context_menu_pressed", id, script, text_edit) + if !_context_menus.has(id): + return + var menu_item :GdUnitContextMenuItem = _context_menus[id] + menu_item.execute([script, text_edit]) diff --git a/addons/gdUnit4/test/core/GdUnitTestSuiteBuilderTest.gd b/addons/gdUnit4/test/core/GdUnitTestSuiteBuilderTest.gd index e1a681e7..295087a6 100644 --- a/addons/gdUnit4/test/core/GdUnitTestSuiteBuilderTest.gd +++ b/addons/gdUnit4/test/core/GdUnitTestSuiteBuilderTest.gd @@ -38,7 +38,7 @@ func test_create_gd_success() -> void: assert_result(result).is_success() var info := result.value() as Dictionary assert_str(info.get("path")).is_equal("user://tmp/test/examples/test_person_test.gd") - assert_int(info.get("line")).is_equal(9) + assert_int(info.get("line")).is_equal(11) assert_tests(load(info.get("path"))).contains_exactly(["test_first_name"]) # create additional test checked existing suite based checked function selected by line 15 @@ -47,7 +47,7 @@ func test_create_gd_success() -> void: assert_result(result).is_success() info = result.value() as Dictionary assert_str(info.get("path")).is_equal("user://tmp/test/examples/test_person_test.gd") - assert_int(info.get("line")).is_equal(13) + assert_int(info.get("line")).is_equal(16) assert_tests(load(info.get("path"))).contains_exactly_in_any_order(["test_first_name", "test_fully_name"]) func test_create_gd_fail() -> void: diff --git a/addons/gdUnit4/test/core/TestSuiteScannerTest.gd b/addons/gdUnit4/test/core/TestSuiteScannerTest.gd index 04f5d360..4f19e3df 100644 --- a/addons/gdUnit4/test/core/TestSuiteScannerTest.gd +++ b/addons/gdUnit4/test/core/TestSuiteScannerTest.gd @@ -179,7 +179,7 @@ func test_create_test_case(): var result := _TestSuiteScanner.create_test_case(test_suite_path, "last_name", source_path) assert_that(result.is_success()).is_true() var info :Dictionary = result.value() - assert_int(info.get("line")).is_equal(9) + assert_int(info.get("line")).is_equal(11) assert_file(info.get("path")).exists()\ .is_file()\ .is_script()\ @@ -193,14 +193,15 @@ func test_create_test_case(): "# TestSuite generated from", "const __source = '%s'" % source_path, "", + "", "func test_last_name() -> void:", - " # remove_at this line and complete your test", + " # remove this line and complete your test", " assert_not_yet_implemented()", ""]) # try to add again result = _TestSuiteScanner.create_test_case(test_suite_path, "last_name", source_path) assert_that(result.is_success()).is_true() - assert_that(result.value()).is_equal({"line" : 10, "path": test_suite_path}) + assert_that(result.value()).is_equal({"line" : 11, "path": test_suite_path}) # https://github.com/MikeSchulze/gdUnit4/issues/25 func test_build_test_suite_path() -> void: @@ -295,7 +296,7 @@ func test_scan_by_inheritance_class_path() -> void: ts.free() func test_get_test_case_line_number() -> void: - assert_int(_TestSuiteScanner.get_test_case_line_number("res://addons/gdUnit4/test/core/TestSuiteScannerTest.gd", "get_test_case_line_number")).is_equal(297) + assert_int(_TestSuiteScanner.get_test_case_line_number("res://addons/gdUnit4/test/core/TestSuiteScannerTest.gd", "get_test_case_line_number")).is_equal(298) assert_int(_TestSuiteScanner.get_test_case_line_number("res://addons/gdUnit4/test/core/TestSuiteScannerTest.gd", "unknown")).is_equal(-1) func test__to_naming_convention() -> void: diff --git a/project.godot b/project.godot index 4427177c..04a37507 100644 --- a/project.godot +++ b/project.godot @@ -429,6 +429,11 @@ _global_script_classes=[{ "language": &"GDScript", "path": "res://addons/gdUnit4/src/GdUnitConstants.gd" }, { +"base": "RefCounted", +"class": &"GdUnitContextMenuItem", +"language": &"GDScript", +"path": "res://addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd" +}, { "base": "GdUnitAssert", "class": &"GdUnitDictionaryAssert", "language": &"GDScript", @@ -1114,6 +1119,11 @@ _global_script_classes=[{ "language": &"GDScript", "path": "res://addons/gdUnit4/test/core/ResultTest.gd" }, { +"base": "Object", +"class": &"ScriptEditorContextMenuHandler", +"language": &"GDScript", +"path": "res://addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd" +}, { "base": "RefCounted", "class": &"ScriptEditorControls", "language": &"GDScript", @@ -1289,6 +1299,7 @@ _global_script_class_icons={ "GdUnitClassDoubler": "", "GdUnitClassDoublerTest": "", "GdUnitConstants": "", +"GdUnitContextMenuItem": "", "GdUnitDictionaryAssert": "", "GdUnitDictionaryAssertImpl": "", "GdUnitDictionaryAssertImplTest": "", @@ -1426,6 +1437,7 @@ _global_script_class_icons={ "RPCMessage": "", "Result": "", "ResultTest": "", +"ScriptEditorContextMenuHandler": "", "ScriptEditorControls": "", "ScriptWithClassName": "", "SnakeCaseWithClassName": "",