scripting_tools.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. """
  6. from editor_python_test_tools.utils import TestHelper as helper
  7. from PySide2 import QtWidgets, QtTest, QtCore
  8. from PySide2.QtCore import Qt
  9. from editor_python_test_tools.utils import Report
  10. import pyside_utils
  11. import editor_python_test_tools.hydra_editor_utils as hydra
  12. from editor_python_test_tools.editor_entity_utils import EditorEntity
  13. import azlmbr.editor as editor
  14. import azlmbr.math as math
  15. import azlmbr.bus as bus
  16. import azlmbr.legacy.general as general
  17. import azlmbr.scriptcanvas as scriptcanvas
  18. from scripting_utils.scripting_constants import (SCRIPT_CANVAS_UI, ASSET_EDITOR_UI, NODE_PALETTE_UI, NODE_PALETTE_QT,
  19. TREE_VIEW_QT, SEARCH_FRAME_QT, SEARCH_FILTER_QT, SAVE_STRING, NAME_STRING,
  20. SAVE_ASSET_AS, WAIT_TIME_3, NODE_INSPECTOR_TITLE_KEY, WAIT_FRAMES,
  21. VARIABLE_MANAGER_QT, NODE_INSPECTOR_QT, NODE_INSPECTOR_UI, SCRIPT_EVENT_UI,
  22. VARIABLE_PALETTE_QT, ADD_BUTTON_QT, VARIABLE_TYPES, EVENTS_QT, DEFAULT_SCRIPT_EVENT,
  23. SCRIPT_EVENT_FILE_PATH, PARAMETERS_QT, VARIABLE_MANAGER_QT, NODE_INSPECTOR_QT,
  24. NODE_INSPECTOR_UI, VARIABLE_PALETTE_QT, ADD_BUTTON_QT, VARIABLE_TYPES,
  25. SCRIPT_CANVAS_COMPONENT_PROPERTY_PATH, ENTITY_STATES)
  26. class Tests():
  27. new_event_created = ("New Script Event created", "Failed to create a new event")
  28. child_event_created = ("Child Event created", "Failed to create Child Event")
  29. parameter_created = ("Successfully added parameter", "Failed to add parameter")
  30. parameter_removed = ("Successfully removed parameter", "Failed to remove parameter")
  31. def click_menu_option(window, option_text):
  32. """
  33. function for clicking an option from a Qt menu object. This function bypasses menu groups or categories. for example,
  34. if you want to click the Open option from the "File" category provide "Open" as your menu text instead of "File" then "Open".
  35. param window: the qt window object where the menu option is located
  36. param option_text: the label string used in the menu option that you want to click
  37. returns none
  38. """
  39. action = pyside_utils.find_child_by_pattern(window, {"text": option_text, "type": QtWidgets.QAction})
  40. action.trigger()
  41. def save_script_event_file(self, file_path):
  42. """
  43. function for saving a script event file with a user defined file path. Requires asset editor qt object to be initialized
  44. and any required fields in the asset editor to be filled in before asset can be saved.
  45. param self: the script object calling this function
  46. param file_path: full path to the file as a string
  47. returns: true if the Save action is successful and the * character disappears from the asset editor label
  48. """
  49. editor.AssetEditorWidgetRequestsBus(bus.Broadcast, SAVE_ASSET_AS, file_path)
  50. action = pyside_utils.find_child_by_pattern(self.asset_editor_menu_bar, {"type": QtWidgets.QAction, "iconText": SAVE_STRING})
  51. action.trigger()
  52. # wait till file is saved, to validate that check the text of QLabel at the bottom of the AssetEditor,
  53. # if there are no unsaved changes we will not have any * in the text
  54. label = self.asset_editor.findChild(QtWidgets.QLabel, "textEdit")
  55. return helper.wait_for_condition(lambda: "*" not in label.text(), WAIT_TIME_3)
  56. def initialize_editor_object(self):
  57. self.editor_main_window = pyside_utils.get_editor_main_window()
  58. def initialize_sc_editor_objects(self):
  59. self.sc_editor = self.editor_main_window.findChild(QtWidgets.QDockWidget, SCRIPT_CANVAS_UI)
  60. self.sc_editor_main_window = self.sc_editor.findChild(QtWidgets.QMainWindow)
  61. def initialize_variable_manager_object(self):
  62. self.variable_manager = self.sc_editor.findChild(QtWidgets.QDockWidget, VARIABLE_MANAGER_QT)
  63. if not self.variable_manager.isVisible():
  64. self.click_menu_option(self.sc_editor, VARIABLE_MANAGER_QT)
  65. def initialize_asset_editor_object(self):
  66. """
  67. function for initializing qt objects needed for testing around asset editor
  68. param self: the script object calling this function.
  69. returns: None
  70. """
  71. self.asset_editor = self.editor_main_window.findChild(QtWidgets.QDockWidget, ASSET_EDITOR_UI)
  72. self.asset_editor_widget = self.asset_editor.findChild(QtWidgets.QWidget, "AssetEditorWindowClass")
  73. self.asset_editor_row_container = self.asset_editor_widget.findChild(QtWidgets.QWidget, "ContainerForRows")
  74. self.asset_editor_menu_bar = self.asset_editor_widget.findChild(QtWidgets.QMenuBar)
  75. def initialize_node_palette_object(self):
  76. """
  77. function for initializing qt objects needed for testing around the script canvas editor
  78. param self: the script object calling this function
  79. returns: None
  80. """
  81. self.node_palette = self.sc_editor.findChild(QtWidgets.QDockWidget, NODE_PALETTE_QT)
  82. self.node_tree_view = self.node_palette.findChild(QtWidgets.QTreeView, TREE_VIEW_QT)
  83. self.node_tree_search_frame = self.node_palette.findChild(QtWidgets.QFrame, SEARCH_FRAME_QT)
  84. self.node_tree_search_box = self.node_tree_search_frame.findChild(QtWidgets.QLineEdit, SEARCH_FILTER_QT)
  85. def expand_qt_container_rows(self, object_name):
  86. """
  87. function used for expanding qt container rows with expandable children
  88. param self: The script object calling this function
  89. param object_name: qt object name as a string
  90. returns: none
  91. """
  92. children = self.asset_editor_row_container.findChildren(QtWidgets.QFrame, object_name)
  93. for child in children:
  94. check_box = child.findChild(QtWidgets.QCheckBox)
  95. if check_box and not check_box.isChecked():
  96. QtTest.QTest.mouseClick(check_box, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier)
  97. def open_node_palette(self):
  98. """
  99. function for checking if node palette is on and if not turn it on
  100. param self: the script calling this function
  101. returns none
  102. """
  103. if self.sc_editor.findChild(QtWidgets.QDockWidget, NODE_PALETTE_QT) is None:
  104. action = pyside_utils.find_child_by_pattern(self.sc_editor, {"text": NODE_PALETTE_UI, "type": QtWidgets.QAction})
  105. action.trigger()
  106. def open_script_canvas():
  107. """
  108. function for opening the script canvas UI
  109. returns true / false result of helper function's attempt
  110. """
  111. general.open_pane(SCRIPT_CANVAS_UI)
  112. result = helper.wait_for_condition(lambda: general.is_pane_visible(SCRIPT_CANVAS_UI), WAIT_TIME_3)
  113. return result
  114. def open_asset_editor():
  115. """
  116. function for opening the asset editor UI
  117. returns true/false result of helper function's attempt
  118. """
  119. general.open_pane(ASSET_EDITOR_UI)
  120. result = helper.wait_for_condition(lambda: general.is_pane_visible(ASSET_EDITOR_UI), WAIT_TIME_3)
  121. return result
  122. def canvas_node_palette_search(self, node_name, number_of_retries):
  123. """
  124. function for searching the script canvas node palette for user defined nodes. function takes a number of retries as
  125. an argument in case editor/script canvas lags during test.
  126. param self: The script calling this function
  127. param node_name: the name of the node being searched for
  128. param number_of_retries: the number of times to search (click on the search button)
  129. returns: boolean value of the search attempt
  130. """
  131. self.node_tree_search_box.setText(node_name)
  132. helper.wait_for_condition(lambda: self.node_tree_search_box.text() == node_name, WAIT_TIME_3)
  133. # Try clicking ENTER in search box multiple times
  134. found_node = False
  135. for _ in range(number_of_retries):
  136. QtTest.QTest.keyClick(self.node_tree_search_box, QtCore.Qt.Key_Enter, QtCore.Qt.NoModifier)
  137. found_node = helper.wait_for_condition(
  138. lambda: pyside_utils.find_child_by_pattern(self.node_tree_view, {"text": node_name}) is not None, WAIT_TIME_3)
  139. if found_node is True:
  140. break
  141. return found_node
  142. def get_node_palette_node_tree_qt_object (self):
  143. """
  144. function for retrieving the tree view qt object for the node palette
  145. params self: the script calling this function
  146. returns: a tree view qt object
  147. """
  148. node_palette_widget = self.sc_editor.findChild(QtWidgets.QDockWidget, NODE_PALETTE_QT)
  149. node_palette_node_tree = node_palette_widget.findChild(QtWidgets.QTreeView, TREE_VIEW_QT)
  150. return node_palette_node_tree
  151. def get_node_palette_category_qt_object(self, category_name):
  152. """
  153. function for retrieving the qt object for a node palette category
  154. param self: the script calling this function
  155. param category_name: string for the category label you are searching node palette for
  156. returns: the qt object for the node palette category
  157. """
  158. node_palette_node_tree = get_node_palette_node_tree_qt_object(self)
  159. node_palette_category = pyside_utils.find_child_by_pattern(node_palette_node_tree, category_name)
  160. return node_palette_category
  161. def get_node_inspector_node_titles(self, sc_graph_node_inspector, sc_graph):
  162. """
  163. function for retrieving the node inspector's node titles from all nodes in a script canvas graph. function takes
  164. a script canvas graph and node inspector qt widget.
  165. param self: the script calling this function
  166. param sc_graph_node_inspector: the sc graph node inspector qt widget
  167. param sc_graph: the sc graph qt widget
  168. returns: a list of node titles (i.e Print - Utilities/Debug). If there are duplicates of a node then the title
  169. will include ( X Selected) in the string.
  170. """
  171. node_inspector_scroll_area = sc_graph_node_inspector.findChild(QtWidgets.QScrollArea, "")
  172. # perform ctrl+a keystroke to highlight all nodes on the graph
  173. QtTest.QTest.keyClick(sc_graph, "a", Qt.ControlModifier, WAIT_FRAMES)
  174. node_inspector_backgrounds = node_inspector_scroll_area.findChildren(QtWidgets.QFrame, "Background")
  175. titles = []
  176. for background in node_inspector_backgrounds:
  177. background_title = background.findChild(QtWidgets.QLabel, NODE_INSPECTOR_TITLE_KEY)
  178. if background_title.text() is not "":
  179. titles.append(background_title.text())
  180. return titles
  181. def get_main_sc_window_qt_object():
  182. """
  183. function for getting the sc main window qt object.
  184. params: none
  185. returns: a qt widget main window object
  186. """
  187. editor_window = pyside_utils.get_editor_main_window()
  188. sc_editor = editor_window.findChild(QtWidgets.QDockWidget, SCRIPT_CANVAS_UI)
  189. return sc_editor.findChild(QtWidgets.QMainWindow)
  190. def create_new_sc_graph(sc_editor_main_window):
  191. """
  192. function for opening a new script canvas graph file. uses the sc editor window to trigger a new file action
  193. param self: the script calling this function
  194. param sc_editor_main_window: the qt object for the main sc_editor window
  195. returns: none
  196. """
  197. create_new_graph_action = pyside_utils.find_child_by_pattern(
  198. sc_editor_main_window, {"objectName": "action_New_Script", "type": QtWidgets.QAction}
  199. )
  200. create_new_graph_action.trigger()
  201. def create_new_variable(self, new_variable_type):
  202. """
  203. function for creating a new SC variable through variable manager
  204. param self: the script objecting calling this function
  205. param variable_type: The variable data type to create as a string. i.e "Boolean"
  206. returns: none
  207. """
  208. if type(new_variable_type) is not str:
  209. Report.critical_result(["Invalid variable type provided", ""], False)
  210. valid_type = False
  211. for this_type in VARIABLE_TYPES:
  212. if new_variable_type == this_type:
  213. valid_type = True
  214. if not valid_type:
  215. Report.critical_result(["Invalid variable type provided", ""], False)
  216. add_new_variable_button = self.variable_manager.findChild(QtWidgets.QPushButton, ADD_BUTTON_QT)
  217. add_new_variable_button.click() # Click on Create Variable button
  218. helper.wait_for_condition((
  219. lambda: self.variable_manager.findChild(QtWidgets.QTableView, VARIABLE_PALETTE_QT) is not None), WAIT_TIME_3)
  220. # Select variable type
  221. table_view = self.variable_manager.findChild(QtWidgets.QTableView, VARIABLE_PALETTE_QT)
  222. model_index = pyside_utils.find_child_by_pattern(table_view, new_variable_type)
  223. # Click on it to create variable
  224. pyside_utils.item_view_index_mouse_click(table_view, model_index)
  225. def get_sc_editor_node_inspector(sc_editor):
  226. """
  227. function for toggling the node inspector if it's not already turned on and returning the qt widget object
  228. param sc_editor: the script canvas editor qt object
  229. returns: the node inspector qt widget object
  230. """
  231. node_inspector_widget = sc_editor.findChild(QtWidgets.QDockWidget, NODE_INSPECTOR_QT)
  232. if sc_editor.findChild(QtWidgets.QDockWidget, NODE_INSPECTOR_QT) is None:
  233. action = pyside_utils.find_child_by_pattern(sc_editor, {"text": NODE_INSPECTOR_UI, "type": QtWidgets.QAction})
  234. action.trigger()
  235. return node_inspector_widget
  236. def create_script_event(self):
  237. """
  238. Function for creating a script event from the editor's asset editor.
  239. param self: the script calling this function
  240. returns None
  241. """
  242. action = pyside_utils.find_child_by_pattern(self.asset_editor_menu_bar, {"type": QtWidgets.QAction, "text": SCRIPT_EVENT_UI})
  243. action.trigger()
  244. result = helper.wait_for_condition(
  245. lambda: self.asset_editor_row_container.findChild(QtWidgets.QFrame, EVENTS_QT) is not None, WAIT_TIME_3
  246. )
  247. Report.result(Tests.new_event_created, result)
  248. # Add new child event
  249. add_event = self.asset_editor_row_container.findChild(QtWidgets.QFrame, EVENTS_QT).findChild(QtWidgets.QToolButton, "")
  250. add_event.click()
  251. result = helper.wait_for_condition(
  252. lambda: self.asset_editor_widget.findChild(QtWidgets.QFrame, DEFAULT_SCRIPT_EVENT) is not None, WAIT_TIME_3
  253. )
  254. Report.result(Tests.child_event_created, result)
  255. def create_script_event_parameter(self):
  256. add_param = self.asset_editor_row_container.findChild(QtWidgets.QFrame, "Parameters").findChild(QtWidgets.QToolButton, "")
  257. add_param.click()
  258. result = helper.wait_for_condition(
  259. lambda: self.asset_editor_widget.findChild(QtWidgets.QFrame, "[0]") is not None, WAIT_TIME_3
  260. )
  261. Report.result(Tests.parameter_created, result)
  262. def remove_script_event_parameter(self):
  263. remove_param = self.asset_editor_row_container.findChild(QtWidgets.QFrame, "[0]").findChild(QtWidgets.QToolButton, "")
  264. remove_param.click()
  265. result = helper.wait_for_condition(
  266. lambda: self.asset_editor_widget.findChild(QtWidgets.QFrame, "[0]") is None, WAIT_TIME_3
  267. )
  268. Report.result(Tests.parameter_removed, result)
  269. def add_empty_parameter_to_script_event(self, number_of_parameters):
  270. """
  271. Function for adding a new blank parameter to a script event
  272. param self: the script calling this function
  273. param number_of_parameters: the number of empty parameters to add
  274. returns none
  275. """
  276. helper.wait_for_condition(
  277. lambda: self.asset_editor_row_container.findChild(QtWidgets.QFrame, PARAMETERS_QT) is not None, WAIT_TIME_3)
  278. parameters = self.asset_editor_row_container.findChild(QtWidgets.QFrame, PARAMETERS_QT)
  279. add_parameter = parameters.findChild(QtWidgets.QToolButton, "")
  280. for _ in range(number_of_parameters):
  281. add_parameter.click()
  282. def get_script_event_parameter_name_text(self):
  283. """
  284. function for retrieving the name field of script event parameters
  285. param self: the script calling this function
  286. returns a container with all the parameters' editable name fields
  287. """
  288. parameter_names = self.asset_editor_row_container.findChildren(QtWidgets.QFrame, NAME_STRING)
  289. name_fields = []
  290. for parameter_name in parameter_names:
  291. name_fields.append(parameter_name.findChild(QtWidgets.QLineEdit))
  292. return name_fields
  293. def get_script_event_parameter_type_combobox(self):
  294. """
  295. function for retrieving the type field of script event parameters
  296. param self: the script calling this function
  297. returns a container with all the parameters' editable type combo boxes
  298. """
  299. parameter_types = self.asset_editor_row_container.findChildren(QtWidgets.QFrame, "Type")
  300. type_combo_boxes =[]
  301. for parameter_type in parameter_types:
  302. type_combo_boxes.append(parameter_type.findChild(QtWidgets.QComboBox))
  303. return type_combo_boxes
  304. def located_expected_tracer_lines(self, section_tracer, lines):
  305. """
  306. function for parsing game mode's console output for expected test lines. requires section_tracer. duplicates lines
  307. and error lines are not handled by this function
  308. param self: The script calling this function
  309. param section_tracer: python editor tracer object
  310. param lines: list of expected lines
  311. returns true if all the expected lines were detected in the parsed output
  312. """
  313. found_lines = [printInfo.message.strip() for printInfo in section_tracer.prints]
  314. expected_lines = len(lines)
  315. matching_lines = 0
  316. for line in lines:
  317. for found_line in found_lines:
  318. if line == found_line:
  319. matching_lines += 1
  320. return matching_lines >= expected_lines
  321. def create_entity_with_sc_component_asset(entity_name, source_file, position = math.Vector3(512.0, 512.0, 32.0)):
  322. """
  323. function for creating a new entity in the scene w/ a script canvas component. Function also adds as
  324. script canvas file to the script canvas component's source file property.
  325. param entity_name: the name you want to assign the entity
  326. param source_file: the path to script canvas file to be added to the script canvas component
  327. param position: the translation property of the new entity's transform
  328. returns: the entity created by this function
  329. """
  330. sourcehandle = scriptcanvas.SourceHandleFromPath(source_file)
  331. entity = hydra.Entity(entity_name)
  332. entity.create_entity(position, ["Script Canvas"])
  333. script_canvas_component = entity.components[0]
  334. hydra.set_component_property_value(script_canvas_component, SCRIPT_CANVAS_COMPONENT_PROPERTY_PATH, sourcehandle)
  335. return entity
  336. def create_entity_with_multiple_sc_component_asset(entity_name, source_files, position = math.Vector3(512.0, 512.0, 32.0)):
  337. """
  338. function for creating a new entity with multiple script canvas components and adding a source file to each.
  339. param entity_name: the name you want to assign the entity
  340. param source_files: a list of source files you want added to the script canvas components
  341. param position: the translation property of the new entity's transform
  342. returns: the entity created by this function
  343. """
  344. number_of_files = len(source_files)
  345. components_array =[]
  346. for num in range(number_of_files):
  347. components_array.append("Script Canvas")
  348. entity = hydra.Entity(entity_name)
  349. entity.create_entity(position, components_array)
  350. for num in range(number_of_files):
  351. script_canvas_component = entity.components[num]
  352. sourcehandle = scriptcanvas.SourceHandleFromPath(source_files[num])
  353. hydra.set_component_property_value(script_canvas_component, SCRIPT_CANVAS_COMPONENT_PROPERTY_PATH, sourcehandle)
  354. return entity
  355. def change_entity_sc_asset(entity, source_file, component_index = 0):
  356. """
  357. function for changing the source file component property value of an entity. Function assumes that there is a SC
  358. component somewhere in the list of components
  359. param entity: The entity with the SC component you want to update
  360. param source_file: The file you want to assign to the script canvas component property
  361. param component_index: the index of the sc component you want to update.
  362. returns true if the function was able to asign the source file ot the component
  363. """
  364. source_handle = scriptcanvas.SourceHandleFromPath(source_file)
  365. script_canvas_component = entity.components[component_index]
  366. hydra.set_component_property_value(script_canvas_component, SCRIPT_CANVAS_COMPONENT_PROPERTY_PATH, source_handle)
  367. script_file = hydra.get_component_property_value(script_canvas_component, SCRIPT_CANVAS_COMPONENT_PROPERTY_PATH)
  368. result = helper.wait_for_condition(lambda: script_file is not None, WAIT_TIME_3)
  369. return result
  370. def change_entity_sc_variable_entity(entity_name, target_entity_name, component_property_path, component_index = 0):
  371. """
  372. function for finding an entity by its name and changing an exposed entity variable on the script canvas component.
  373. param entity_name: the string name of the entity you want to modify
  374. param target_entity_name: The name of the entity you want to use as a variable
  375. param component_property_path: The component property path to the variable.
  376. param
  377. parama component_index: the index of the sc component. default 0
  378. returns None
  379. """
  380. entity = EditorEntity.find_editor_entity(entity_name)
  381. target_entity = EditorEntity.find_editor_entity(target_entity_name)
  382. sc_component = entity.get_components_of_type(["Script Canvas"])[component_index]
  383. sc_component.set_component_property_value(component_property_path, target_entity.id)
  384. def change_entity_start_status(entity_name, start_status):
  385. """
  386. function for finding an entity by name and changing its start status
  387. param entity_name: the string name of the entity you want to modify
  388. param start_status: the start status to update (active/inactive)
  389. returns None
  390. """
  391. entity = EditorEntity.find_editor_entity(entity_name)
  392. entity.set_start_status(start_status)
  393. def validate_entity_start_state_by_name(entity_name, expected_state):
  394. """
  395. function for finding and validating the start state of an entity by name. If the actual state does not match
  396. the expected state the function will attempt to set the state for you.
  397. param entity_name: the name of the entity you want to modify
  398. param expected_state: the expected start state of the entity
  399. """
  400. entity = EditorEntity.find_editor_entity(entity_name)
  401. if expected_state.lower() not in ENTITY_STATES.keys():
  402. raise ValueError(f"{expected_state} is an invalid option; valid options: active, inactive, or editor.")
  403. state = entity.get_start_status()
  404. if state != ENTITY_STATES[expected_state]:
  405. # If state fails to set, set_start_status will assert
  406. entity.set_start_status(expected_state)
  407. state = entity.get_start_status()
  408. # return the start state that we were able to set the entity to
  409. return state == ENTITY_STATES[expected_state]
  410. def validate_entity_exists_by_name(entity_name, test_results):
  411. """
  412. function for validating if an entity was properly created
  413. param entity_name: string name of the entity to validate
  414. param test_results: pass/fail tuple of result strings
  415. """
  416. entity = EditorEntity.find_editor_entity(entity_name)
  417. Report.critical_result(test_results, entity.id.IsValid())
  418. return entity