diff --git a/README.md b/README.md
index d232a11..130f00a 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
-
+
@@ -701,4 +701,3 @@ WebRunner uses a rotating file handler for logging. This project is licensed under the [MIT License](LICENSE). -Copyright (c) 2021~2023 JE-Chen diff --git a/docs/source/API/api_index.rst b/docs/source/API/api_index.rst index dd7393b..03d217a 100644 --- a/docs/source/API/api_index.rst +++ b/docs/source/API/api_index.rst @@ -12,11 +12,11 @@ WebRunner API Documentation utils/generate_report.rst utils/json.rst utils/package_manager.rst + utils/project.rst utils/socket_server.rst utils/test_object.rst - utils/test_reocrd.rst + utils/test_record.rst utils/xml.rst - utils/scheduler.rst wrapper/element.rst wrapper/manager.rst wrapper/utils.rst diff --git a/docs/source/API/utils/assert.rst b/docs/source/API/utils/assert.rst index e0cb67f..0e8d8e3 100644 --- a/docs/source/API/utils/assert.rst +++ b/docs/source/API/utils/assert.rst @@ -1,78 +1,92 @@ Assert API ----- +========== + +``je_web_runner.utils.assert_value.result_check`` + +Functions for validating WebDriver and WebElement properties. +Raises ``WebRunnerAssertException`` on validation failure. .. code-block:: python def _make_webdriver_check_dict(webdriver_to_check: WebDriver) -> dict: """ - use to check webdriver current info - :param webdriver_to_check: what webdriver we want to check - :return: webdriver detail dict + Build a dictionary of the WebDriver's current state for validation. + + :param webdriver_to_check: WebDriver instance to inspect + :return: dict of WebDriver properties (name, title, current_url, etc.) """ .. code-block:: python def _make_web_element_check_dict(web_element_to_check: WebElement) -> dict: """ - use to check web element current info - :param web_element_to_check: what web element we want to check - :return: web element detail dict + Build a dictionary of the WebElement's current state for validation. + + :param web_element_to_check: WebElement instance to inspect + :return: dict of WebElement properties (tag_name, text, enabled, displayed, etc.) """ .. code-block:: python - def check_value(element_name: str, element_value: typing.Any, result_check_dict: dict) -> None: + def check_value(element_name: str, element_value: Any, result_check_dict: dict) -> None: """ - use to check state - :param element_name: the name of element we want to check - :param element_value: what value element should be - :param result_check_dict: the dict include data name and value to check check_dict is valid or not - :return: None + Check a single value against a result dictionary. + + :param element_name: key to look up in result_check_dict + :param element_value: expected value + :param result_check_dict: dictionary of actual values + :raises WebRunnerAssertException: if values don't match """ .. code-block:: python def check_values(check_dict: dict, result_check_dict: dict) -> None: """ - :param check_dict: dict include data name and value to check - :param result_check_dict: the dict include data name and value to check check_dict is valid or not - :return: None + Check multiple key-value pairs against a result dictionary. + + :param check_dict: dict of {name: expected_value} pairs + :param result_check_dict: dictionary of actual values + :raises WebRunnerAssertException: if any value doesn't match """ .. code-block:: python - def check_webdriver_value(element_name: str, element_value: typing.Any, webdriver_to_check: WebDriver) -> None: + def check_webdriver_value(element_name: str, element_value: Any, webdriver_to_check: WebDriver) -> None: """ - :param element_name: the name of element we want to check - :param element_value: what value element should be - :param webdriver_to_check: the dict include data name and value to check result_dict is valid or not - :return: None + Check a single WebDriver property against an expected value. + + :param element_name: property name (e.g., "name", "title") + :param element_value: expected value + :param webdriver_to_check: WebDriver instance to validate """ .. code-block:: python def check_webdriver_details(webdriver_to_check: WebDriver, result_check_dict: dict) -> None: """ - :param webdriver_to_check: what webdriver we want to check - :param result_check_dict: the dict include data name and value to check result_dict is valid or not - :return: None + Validate multiple WebDriver properties. + + :param webdriver_to_check: WebDriver instance to validate + :param result_check_dict: dict of {property: expected_value} """ .. code-block:: python - def check_web_element_value(element_name: str, element_value: typing.Any, web_element_to_check: WebElement) -> None: + def check_web_element_value(element_name: str, element_value: Any, web_element_to_check: WebElement) -> None: """ - :param element_name: the name of element we want to check - :param element_value: what value element should be - :param web_element_to_check: the dict include data name and value to check result_dict is valid or not - :return: None + Check a single WebElement property against an expected value. + + :param element_name: property name (e.g., "tag_name", "enabled") + :param element_value: expected value + :param web_element_to_check: WebElement instance to validate """ .. code-block:: python def check_web_element_details(web_element_to_check: WebElement, result_check_dict: dict) -> None: """ - :param web_element_to_check: what web element we want to check - :param result_check_dict: the dict include data name and value to check result_dict is valid or not - :return: None - """ \ No newline at end of file + Validate multiple WebElement properties. + + :param web_element_to_check: WebElement instance to validate + :param result_check_dict: dict of {property: expected_value} + """ diff --git a/docs/source/API/utils/callback.rst b/docs/source/API/utils/callback.rst index 1b728a7..d246f80 100644 --- a/docs/source/API/utils/callback.rst +++ b/docs/source/API/utils/callback.rst @@ -1,21 +1,44 @@ Callback API ----- +============ + +``je_web_runner.utils.callback.callback_function_executor`` + +Class: CallbackFunctionExecutor +------------------------------- + +Executes trigger functions with callback support. +Shares the same ``event_dict`` command mapping as the standard Executor. + +.. code-block:: python + + class CallbackFunctionExecutor: + + event_dict: dict + # Same command mapping as Executor (WR_* commands) + + def callback_function( + self, + trigger_function_name: str, + callback_function: Callable, + callback_function_param: Union[dict, list, None] = None, + callback_param_method: str = "kwargs", + **kwargs + ): + """ + Execute a trigger function, then execute a callback function. + + :param trigger_function_name: function name to trigger (must exist in event_dict) + :param callback_function: callback function to execute after trigger + :param callback_function_param: parameters for callback (dict for kwargs, list for args) + :param callback_param_method: "kwargs" or "args" + :param kwargs: parameters passed to the trigger function + :return: return value of the trigger function + :raises CallbackExecutorException: if trigger function not found or invalid param method + """ + +Global Instance +--------------- .. code-block:: python - def callback_function( - self, - trigger_function_name: str, - callback_function: typing.Callable, - callback_function_param: [dict, None] = None, - callback_param_method: str = "kwargs", - **kwargs - ): - """ - :param trigger_function_name: what function we want to trigger only accept function in event_dict - :param callback_function: what function we want to callback - :param callback_function_param: callback function's param only accept dict - :param callback_param_method: what type param will use on callback function only accept kwargs and args - :param kwargs: trigger_function's param - :return: trigger_function_name return value - """ \ No newline at end of file + callback_executor = CallbackFunctionExecutor() diff --git a/docs/source/API/utils/executor.rst b/docs/source/API/utils/executor.rst index 2537787..b3d43e5 100644 --- a/docs/source/API/utils/executor.rst +++ b/docs/source/API/utils/executor.rst @@ -1,215 +1,144 @@ Executor API ----- +============ + +``je_web_runner.utils.executor.action_executor`` + +Class: Executor +--------------- + +The action execution engine that maps command strings to callable functions. .. code-block:: python - import builtins - import sys - import time - import types - from inspect import getmembers, isbuiltin - - from je_web_runner.je_web_runner.manager.webrunner_manager import web_runner - from je_web_runner.utils.exception.exception_tags import add_command_exception_tag - from je_web_runner.utils.exception.exception_tags import executor_data_error, executor_list_error - from je_web_runner.utils.exception.exceptions import WebRunnerExecuteException, WebRunnerAddCommandException - from je_web_runner.utils.generate_report.generate_html_report import generate_html_report - from je_web_runner.utils.generate_report.generate_html_report import generate_html - from je_web_runner.utils.generate_report.generate_json_report import generate_json - from je_web_runner.utils.generate_report.generate_json_report import generate_json_report - from je_web_runner.utils.generate_report.generate_xml_report import generate_xml - from je_web_runner.utils.generate_report.generate_xml_report import generate_xml_report - from je_web_runner.utils.json.json_file.json_file import read_action_json - from je_web_runner.utils.package_manager.package_manager_class import package_manager - from je_web_runner.utils.test_object.test_object_record.test_object_record_class import test_object_record - from je_web_runner.utils.test_record.test_record_class import test_record_instance - - - class Executor(object): - - def __init__(self): - self.event_dict = { - # webdriver manager - "WR_get_webdriver_manager": web_runner.new_driver, - "WR_change_index_of_webdriver": web_runner.change_webdriver, - "WR_quit": web_runner.quit, - # test object - "WR_SaveTestObject": test_object_record.save_test_object, - "WR_CleanTestObject": test_object_record.clean_record, - # webdriver wrapper - "WR_set_driver": web_runner.webdriver_wrapper.set_driver, - "WR_set_webdriver_options_capability": web_runner.webdriver_wrapper.set_driver, - "WR_find_element": web_runner.webdriver_wrapper.find_element_with_test_object_record, - "WR_find_elements": web_runner.webdriver_wrapper.find_elements_with_test_object_record, - "WR_implicitly_wait": web_runner.webdriver_wrapper.implicitly_wait, - "WR_explict_wait": web_runner.webdriver_wrapper.explict_wait, - "WR_to_url": web_runner.webdriver_wrapper.to_url, - "WR_forward": web_runner.webdriver_wrapper.forward, - "WR_back": web_runner.webdriver_wrapper.back, - "WR_refresh": web_runner.webdriver_wrapper.refresh, - "WR_switch": web_runner.webdriver_wrapper.switch, - "WR_set_script_timeout": web_runner.webdriver_wrapper.set_script_timeout, - "WR_set_page_load_timeout": web_runner.webdriver_wrapper.set_page_load_timeout, - "WR_get_cookies": web_runner.webdriver_wrapper.get_cookies, - "WR_get_cookie": web_runner.webdriver_wrapper.get_cookie, - "WR_add_cookie": web_runner.webdriver_wrapper.add_cookie, - "WR_delete_cookie": web_runner.webdriver_wrapper.delete_cookie, - "WR_delete_all_cookies": web_runner.webdriver_wrapper.delete_all_cookies, - "WR_execute": web_runner.webdriver_wrapper.execute, - "WR_execute_script": web_runner.webdriver_wrapper.execute_script, - "WR_execute_async_script": web_runner.webdriver_wrapper.execute_async_script, - "WR_move_to_element": web_runner.webdriver_wrapper.move_to_element_with_test_object, - "WR_move_to_element_with_offset": web_runner.webdriver_wrapper.move_to_element_with_offset_and_test_object, - "WR_drag_and_drop": web_runner.webdriver_wrapper.drag_and_drop_with_test_object, - "WR_drag_and_drop_offset": web_runner.webdriver_wrapper.drag_and_drop_offset_with_test_object, - "WR_perform": web_runner.webdriver_wrapper.perform, - "WR_reset_actions": web_runner.webdriver_wrapper.reset_actions, - "WR_left_click": web_runner.webdriver_wrapper.left_click_with_test_object, - "WR_left_click_and_hold": web_runner.webdriver_wrapper.left_click_and_hold_with_test_object, - "WR_right_click": web_runner.webdriver_wrapper.right_click_with_test_object, - "WR_left_double_click": web_runner.webdriver_wrapper.left_double_click_with_test_object, - "WR_release": web_runner.webdriver_wrapper.release_with_test_object, - "WR_press_key": web_runner.webdriver_wrapper.press_key_with_test_object, - "WR_release_key": web_runner.webdriver_wrapper.release_key_with_test_object, - "WR_move_by_offset": web_runner.webdriver_wrapper.move_by_offset, - "WR_pause": web_runner.webdriver_wrapper.pause, - "WR_send_keys": web_runner.webdriver_wrapper.send_keys, - "WR_send_keys_to_element": web_runner.webdriver_wrapper.send_keys_to_element_with_test_object, - "WR_scroll": web_runner.webdriver_wrapper.scroll, - "WR_check_current_webdriver": web_runner.webdriver_wrapper.check_current_webdriver, - "WR_maximize_window": web_runner.webdriver_wrapper.maximize_window, - "WR_fullscreen_window": web_runner.webdriver_wrapper.fullscreen_window, - "WR_minimize_window": web_runner.webdriver_wrapper.minimize_window, - "WR_set_window_size": web_runner.webdriver_wrapper.set_window_size, - "WR_set_window_position": web_runner.webdriver_wrapper.set_window_position, - "WR_get_window_position": web_runner.webdriver_wrapper.get_window_position, - "WR_get_window_rect": web_runner.webdriver_wrapper.get_window_rect, - "WR_set_window_rect": web_runner.webdriver_wrapper.set_window_rect, - "WR_get_screenshot_as_png": web_runner.webdriver_wrapper.get_screenshot_as_png, - "WR_get_screenshot_as_base64": web_runner.webdriver_wrapper.get_screenshot_as_base64, - "WR_get_log": web_runner.webdriver_wrapper.get_log, - "WR_single_quit": web_runner.webdriver_wrapper.quit, - # web element - "WR_element_submit": web_runner.webdriver_element.submit, - "WR_element_clear": web_runner.webdriver_element.clear, - "WR_element_get_property": web_runner.webdriver_element.get_property, - "WR_element_get_dom_attribute": web_runner.webdriver_element.get_dom_attribute, - "WR_element_get_attribute": web_runner.webdriver_element.get_attribute, - "WR_element_is_selected": web_runner.webdriver_element.is_selected, - "WR_element_is_enabled": web_runner.webdriver_element.is_enabled, - "WR_input_to_element": web_runner.webdriver_element.input_to_element, - "WR_click_element": web_runner.webdriver_element.click_element, - "WR_element_is_displayed": web_runner.webdriver_element.is_displayed, - "WR_element_value_of_css_property": web_runner.webdriver_element.value_of_css_property, - "WR_element_screenshot": web_runner.webdriver_element.screenshot, - "WR_element_change_web_element": web_runner.webdriver_element.change_web_element, - "WR_element_check_current_web_element": web_runner.webdriver_element.check_current_web_element, - "WR_element_get_select": web_runner.webdriver_element.get_select, - # init test record - "WR_set_record_enable": test_record_instance.set_record_enable, - # generate report - "WR_generate_html": generate_html, - "WR_generate_html_report": generate_html_report, - "WR_generate_json": generate_json, - "WR_generate_json_report": generate_json_report, - "WR_generate_xml": generate_xml, - "WR_generate_xml_report": generate_xml_report, - # execute - "WR_execute_action": self.execute_action, - "WR_execute_files": self.execute_files, - # Add package - "WR_add_package_to_executor": package_manager.add_package_to_executor, - "WR_add_package_to_callback_executor": package_manager.add_package_to_callback_executor, - } - # get all builtin function and add to event dict - for function in getmembers(builtins, isbuiltin): - self.event_dict.update({str(function[0]): function[1]}) + class Executor: - def _execute_event(self, action: list): + event_dict: dict + # Maps command names (str) to callable functions. + # Includes all WR_* commands and Python built-in functions. + + def execute_action(self, action_list: Union[list, dict]) -> dict: """ - :param action: execute action - :return: what event return + Execute a sequence of actions. + + :param action_list: list of actions or dict with "webdriver_wrapper" key + Format: [["command", {params}], ["command"], ...] + :return: dict mapping each action description to its return value + :raises WebRunnerExecuteException: if action_list is empty or invalid """ - event = self.event_dict.get(action[0]) - if len(action) == 2: - if isinstance(action[1], dict): - return event(**action[1]) - else: - return event(*action[1]) - elif len(action) == 1: - return event() - else: - raise WebRunnerExecuteException(executor_data_error + " " + str(action)) - - def execute_action(self, action_list: [list, dict]) -> dict: + + def execute_files(self, execute_files_list: list) -> list: """ - use to execute action on list - :param action_list: like this structure - [ - ["WR_get_webdriver_manager", {"webdriver_name": "firefox"}], - ["WR_to_url", {"url": "https://www.google.com"}], - ["WR_quit"] - ] - for loop and use execute_event function to execute - :return: recode string, response as list + Execute actions from JSON files. + + :param execute_files_list: list of file paths to JSON action files + :return: list of execution result dicts (one per file) """ - if type(action_list) is dict: - action_list = action_list.get("web_runner", None) - if action_list is None: - raise WebRunnerExecuteException(executor_list_error) - execute_record_dict = dict() - try: - if len(action_list) > 0 or type(action_list) is not list: - pass - else: - raise WebRunnerExecuteException(executor_list_error) - except Exception as error: - print(repr(error), file=sys.stderr) - for action in action_list: - try: - event_response = self._execute_event(action) - execute_record = "execute: " + str(action) - execute_record_dict.update({execute_record: event_response}) - except Exception as error: - print(repr(error), file=sys.stderr) - print(action, file=sys.stderr) - execute_record = "execute: " + str(action) - execute_record_dict.update({execute_record: repr(error)}) - for key, value in execute_record_dict.items(): - print(key) - print(value) - return execute_record_dict - def execute_files(self, execute_files_list: list) -> list: + def _execute_event(self, action: list): """ - :param execute_files_list: list include execute files path - :return: every execute detail as list + Execute a single action from the event dictionary. + + :param action: ["command_name"] or ["command_name", {kwargs}] or ["command_name", [args]] + :return: return value of the executed function + :raises WebRunnerExecuteException: if command not found or invalid format """ - execute_detail_list = list() - for file in execute_files_list: - execute_detail_list.append(self.execute_action(read_action_json(file))) - return execute_detail_list +Registered Commands (event_dict) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - executor = Executor() - package_manager.executor = executor +**WebDriver Manager:** +``WR_get_webdriver_manager``, ``WR_change_index_of_webdriver``, ``WR_quit`` +**Test Object:** +``WR_SaveTestObject``, ``WR_CleanTestObject`` - def add_command_to_executor(command_dict: dict): - """ - :param command_dict: command dict to add into executor command dict - :return:None +**WebDriver Wrapper (Navigation):** +``WR_to_url``, ``WR_forward``, ``WR_back``, ``WR_refresh``, ``WR_switch`` + +**WebDriver Wrapper (Element Finding):** +``WR_find_element``, ``WR_find_elements`` + +**WebDriver Wrapper (Wait):** +``WR_implicitly_wait``, ``WR_explict_wait``, ``WR_set_script_timeout``, ``WR_set_page_load_timeout`` + +**WebDriver Wrapper (Cookies):** +``WR_get_cookies``, ``WR_get_cookie``, ``WR_add_cookie``, ``WR_delete_cookie``, ``WR_delete_all_cookies`` + +**WebDriver Wrapper (JavaScript):** +``WR_execute``, ``WR_execute_script``, ``WR_execute_async_script`` + +**WebDriver Wrapper (Mouse):** +``WR_left_click``, ``WR_right_click``, ``WR_left_double_click``, +``WR_left_click_and_hold``, ``WR_release``, +``WR_move_to_element``, ``WR_move_to_element_with_offset``, ``WR_move_by_offset``, +``WR_drag_and_drop``, ``WR_drag_and_drop_offset`` + +**WebDriver Wrapper (Keyboard):** +``WR_press_key``, ``WR_release_key``, ``WR_send_keys``, ``WR_send_keys_to_element`` + +**WebDriver Wrapper (Actions):** +``WR_perform``, ``WR_reset_actions``, ``WR_pause``, ``WR_scroll`` + +**WebDriver Wrapper (Window):** +``WR_maximize_window``, ``WR_fullscreen_window``, ``WR_minimize_window``, +``WR_set_window_size``, ``WR_set_window_position``, ``WR_get_window_position``, +``WR_get_window_rect``, ``WR_set_window_rect`` + +**WebDriver Wrapper (Screenshot/Log):** +``WR_get_screenshot_as_png``, ``WR_get_screenshot_as_base64``, ``WR_get_log`` + +**WebDriver Wrapper (Other):** +``WR_set_driver``, ``WR_set_webdriver_options_capability``, +``WR_check_current_webdriver``, ``WR_single_quit`` + +**Web Element:** +``WR_click_element``, ``WR_input_to_element``, +``WR_element_submit``, ``WR_element_clear``, +``WR_element_get_property``, ``WR_element_get_dom_attribute``, ``WR_element_get_attribute``, +``WR_element_is_selected``, ``WR_element_is_enabled``, ``WR_element_is_displayed``, +``WR_element_value_of_css_property``, ``WR_element_screenshot``, +``WR_element_change_web_element``, ``WR_element_check_current_web_element``, +``WR_element_get_select`` + +**Test Record:** +``WR_set_record_enable`` + +**Report Generation:** +``WR_generate_html``, ``WR_generate_html_report``, +``WR_generate_json``, ``WR_generate_json_report``, +``WR_generate_xml``, ``WR_generate_xml_report`` + +**Nested Execution:** +``WR_execute_action``, ``WR_execute_files`` + +**Package Management:** +``WR_add_package_to_executor``, ``WR_add_package_to_callback_executor`` + +**Python Built-ins:** +All Python built-in functions (``print``, ``len``, ``type``, etc.) + +Module-level Functions +---------------------- + +.. code-block:: python + + def add_command_to_executor(command_dict: dict) -> None: """ - for command_name, command in command_dict.items(): - if isinstance(command, (types.MethodType, types.FunctionType)): - executor.event_dict.update({command_name: command}) - else: - raise WebRunnerAddCommandException(add_command_exception_tag) + Dynamically add commands to the global Executor. + :param command_dict: {command_name: function} + :raises WebRunnerAddCommandException: if value is not a function/method + """ def execute_action(action_list: list) -> dict: - return executor.execute_action(action_list) - + """Global convenience function to execute actions.""" def execute_files(execute_files_list: list) -> list: - return executor.execute_files(execute_files_list) + """Global convenience function to execute actions from files.""" + +Global Instance +--------------- + +.. code-block:: python + + executor = Executor() diff --git a/docs/source/API/utils/file_process.rst b/docs/source/API/utils/file_process.rst index cd193cf..0ffc9e9 100644 --- a/docs/source/API/utils/file_process.rst +++ b/docs/source/API/utils/file_process.rst @@ -1,12 +1,18 @@ File Process API ----- +================ + +``je_web_runner.utils.file_process.get_dir_file_list`` .. code-block:: python - def get_dir_files_as_list(dir_path: str = getcwd(), default_search_file_extension: str = ".json") -> List[str]: + def get_dir_files_as_list( + dir_path: str = getcwd(), + default_search_file_extension: str = ".json" + ) -> List[str]: + """ + Recursively walk a directory and return files matching the given extension. + + :param dir_path: directory to search (default: current working directory) + :param default_search_file_extension: file extension to filter (default: ".json") + :return: list of absolute file paths, or empty list if none found """ - get dir file when end with default_search_file_extension - :param dir_path: which dir we want to walk and get file list - :param default_search_file_extension: which extension we want to search - :return: [] if nothing searched or [file1, file2.... files] file was searched - """ \ No newline at end of file diff --git a/docs/source/API/utils/generate_report.rst b/docs/source/API/utils/generate_report.rst index 2f8664e..6743f95 100644 --- a/docs/source/API/utils/generate_report.rst +++ b/docs/source/API/utils/generate_report.rst @@ -1,45 +1,76 @@ -Generate report API ----- +Generate Report API +=================== + +``je_web_runner.utils.generate_report`` + +HTML Report +----------- + +``je_web_runner.utils.generate_report.generate_html_report`` .. code-block:: python def generate_html() -> str: """ - :return: html_string - """ + Generate a complete HTML report string from test_record_instance. -.. code-block:: python + The report contains color-coded tables: + - aqua (event_table_head) for successful actions + - red (failure_table_head) for failed actions - def generate_html_report(html_name: str = "default_name"): + :return: complete HTML document string + :raises WebRunnerHTMLException: if no test records exist """ - this function will create and save html report on current folder - :param html_name: save html file name + + def generate_html_report(html_name: str = "default_name") -> None: + """ + Generate and save an HTML report file. + Thread-safe (uses threading.Lock). + + :param html_name: output file name without extension (creates {html_name}.html) """ +JSON Report +----------- + +``je_web_runner.utils.generate_report.generate_json_report`` + .. code-block:: python - def generate_json(): - """ - :return: success test dict and failure test dict + def generate_json() -> tuple: """ + Generate JSON report data from test_record_instance. -.. code-block:: python + :return: tuple of (success_dict, failure_dict) + """ - def generate_json_report(json_file_name: str = "default_name"): + def generate_json_report(json_file_name: str = "default_name") -> None: """ - :param json_file_name: save json file's name + Generate and save JSON report files. + Creates two files: {json_file_name}_success.json and {json_file_name}_failure.json. + Thread-safe (uses threading.Lock). + + :param json_file_name: base file name without extension """ +XML Report +---------- + +``je_web_runner.utils.generate_report.generate_xml_report`` + .. code-block:: python - def generate_xml(): + def generate_xml() -> tuple: """ - :return: success test dict and failure test dict + Generate XML report data from test_record_instance. + + :return: tuple of (success_records, failure_records) """ -.. code-block:: python + def generate_xml_report(xml_file_name: str = "default_name") -> None: + """ + Generate and save XML report files. + Creates two files: {xml_file_name}_success.xml and {xml_file_name}_failure.xml. - def generate_xml_report(xml_file_name: str = "default_name"): + :param xml_file_name: base file name without extension """ - :param xml_file_name: save xml use xml_file_name - """ \ No newline at end of file diff --git a/docs/source/API/utils/json.rst b/docs/source/API/utils/json.rst index d48a756..f4a2a0f 100644 --- a/docs/source/API/utils/json.rst +++ b/docs/source/API/utils/json.rst @@ -1,37 +1,45 @@ -Json File API ----- +JSON Utilities API +================== + +JSON File Operations +-------------------- + +``je_web_runner.utils.json.json_file.json_file`` .. code-block:: python def read_action_json(json_file_path: str) -> list: """ - read the action json - :param json_file_path json file's path to read - """ + Read an action JSON file and return its contents. + Thread-safe (uses threading.Lock). -.. code-block:: python + :param json_file_path: path to the JSON file + :return: list of actions parsed from the file + :raises WebRunnerJsonException: if reading or parsing fails + """ - def write_action_json(json_save_path: str, action_json: list): + def write_action_json(json_save_path: str, action_json: list) -> None: """ - write action json - :param json_save_path json save path - :param action_json the json str include action to write + Write an action list to a JSON file with indentation. + Thread-safe (uses threading.Lock). + + :param json_save_path: path to save the JSON file + :param action_json: list of actions to write """ -.. code-block:: python +JSON Formatting +--------------- - def __process_json(json_string: str, **kwargs) -> str: - """ - :param json_string: full json str (not json type) - :param kwargs: any another kwargs for dumps - :return: reformat str - """ +``je_web_runner.utils.json.json_format.json_process`` .. code-block:: python def reformat_json(json_string: str, **kwargs) -> str: """ - :param json_string: Valid json string - :param kwargs: __process_json params - :return: reformat json string - """ \ No newline at end of file + Reformat a JSON string with indentation (pretty-print). + + :param json_string: valid JSON string to reformat + :param kwargs: additional kwargs passed to json.dumps + :return: reformatted JSON string + :raises WebRunnerJsonException: if the string is not valid JSON + """ diff --git a/docs/source/API/utils/package_manager.rst b/docs/source/API/utils/package_manager.rst index 2460401..b31b1d6 100644 --- a/docs/source/API/utils/package_manager.rst +++ b/docs/source/API/utils/package_manager.rst @@ -1,101 +1,70 @@ Package Manager API ----- +=================== + +``je_web_runner.utils.package_manager.package_manager_class`` + +Class: PackageManager +--------------------- + +Dynamically imports Python packages and registers their functions/classes +into the Executor or CallbackExecutor event dictionaries. .. code-block:: python - from importlib import import_module - from importlib.util import find_spec - from inspect import getmembers, isfunction, isbuiltin, isclass - from sys import stderr + class PackageManager: + installed_package_dict: dict + # Cache of imported packages {name: module} - class PackageManager(object): + executor: Executor + # Reference to the global Executor instance - def __init__(self): - self.installed_package_dict = { - } - self.executor = None - self.callback_executor = None + callback_executor: CallbackFunctionExecutor + # Reference to the global CallbackFunctionExecutor instance def check_package(self, package: str): """ - :param package: package to check exists or not - :return: package if find else None + Check if a package exists and import it. + + :param package: package name to check + :return: imported module if found, None otherwise """ - if self.installed_package_dict.get(package, None) is None: - found_spec = find_spec(package) - if found_spec is not None: - try: - installed_package = import_module(found_spec.name) - self.installed_package_dict.update( - {found_spec.name: installed_package}) - except ModuleNotFoundError as error: - print(repr(error), file=stderr) - return self.installed_package_dict.get(package, None) - - def add_package_to_executor(self, package): + + def add_package_to_executor(self, package: str) -> None: """ - :param package: package's function will add to executor + Add all functions and classes from a package to the Executor's event_dict. + Functions are registered as "{package}_{function_name}". + + :param package: package name to import and register """ - self.add_package_to_target( - package=package, - target=self.executor - ) - def add_package_to_callback_executor(self, package): + def add_package_to_callback_executor(self, package: str) -> None: """ - :param package: package's function will add to callback_executor + Add all functions and classes from a package to the CallbackExecutor's event_dict. + + :param package: package name to import and register """ - self.add_package_to_target( - package=package, - target=self.callback_executor - ) - def get_member(self, package, predicate, target): + def get_member(self, package: str, predicate, target) -> None: """ - :param package: package we want to get member - :param predicate: predicate - :param target: which event_dict will be added + Extract members matching a predicate and add them to a target executor. + + :param package: package name + :param predicate: inspect predicate (isfunction, isbuiltin, isclass) + :param target: executor instance with event_dict attribute """ - installed_package = self.check_package(package) - if installed_package is not None and target is not None: - for member in getmembers(installed_package, predicate): - target.event_dict.update( - {str(package) + "_" + str(member[0]): member[1]}) - elif installed_package is None: - print(repr(ModuleNotFoundError(f"Can't find package {package}")), - file=stderr) - else: - print(f"Executor error {self.executor}", file=stderr) - - def add_package_to_target(self, package, target): + + def add_package_to_target(self, package: str, target) -> None: """ - :param package: package we want to get member - :param target: which event_dict will be added + Add functions, builtins, and classes from a package to a target executor. + + :param package: package name + :param target: executor instance with event_dict attribute """ - try: - self.get_member( - package=package, - predicate=isfunction, - target=target - ) - self.get_member( - package=package, - predicate=isbuiltin, - target=target - ) - self.get_member( - package=package, - predicate=isfunction, - target=target - ) - self.get_member( - package=package, - predicate=isclass, - target=target - ) - except Exception as error: - print(repr(error), file=stderr) - - - package_manager: PackageManager = PackageManager() + +Global Instance +--------------- + +.. code-block:: python + + package_manager = PackageManager() diff --git a/docs/source/API/utils/project.rst b/docs/source/API/utils/project.rst index 9a6f2e0..9198f78 100644 --- a/docs/source/API/utils/project.rst +++ b/docs/source/API/utils/project.rst @@ -1,33 +1,31 @@ Project API ----- +=========== + +``je_web_runner.utils.project.create_project_structure`` .. code-block:: python def create_dir(dir_name: str) -> None: """ - :param dir_name: create dir use dir name - :return: None - """ - Path(dir_name).mkdir( - parents=True, - exist_ok=True - ) + Create a directory (with parents, no error if exists). -.. code-block:: python + :param dir_name: directory path to create + """ def create_template(parent_name: str, project_path: str = None) -> None: """ - create template ob project dir - :param parent_name: project name - :param project_path: project create path - :return: None - """ + Create template files (keyword JSONs and executor Python scripts) + in the project directory. -.. code-block:: python + :param parent_name: project name (subdirectory name) + :param project_path: project base path (default: current working directory) + """ def create_project_dir(project_path: str = None, parent_name: str = "WebRunner") -> None: """ - :param parent_name: project name - :param project_path: project create path - :return: None - """ \ No newline at end of file + Create project directory structure and generate template files. + Creates keyword/ and executor/ subdirectories with sample files. + + :param project_path: path where the project will be created (default: cwd) + :param parent_name: top-level project directory name (default: "WebRunner") + """ diff --git a/docs/source/API/utils/scheduler.rst b/docs/source/API/utils/scheduler.rst deleted file mode 100644 index 7ee025f..0000000 --- a/docs/source/API/utils/scheduler.rst +++ /dev/null @@ -1,192 +0,0 @@ -Scheduler API ----- - -.. code-block:: python - - def add_blocking_job( - self, func: Callable, trigger: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, id: str = None, name: str = None, - misfire_grace_time: int = undefined, coalesce: bool = undefined, max_instances: int = undefined, - next_run_time: datetime = undefined, jobstore: str = 'default', executor: str = 'default', - replace_existing: bool = False, **trigger_args: Any) -> Job: - """ - Just an apscheduler add job wrapper. - :param func: callable (or a textual reference to one) to run at the given time - :param str|apscheduler.triggers.base.BaseTrigger trigger: trigger that determines when - ``func`` is called - :param list|tuple args: list of positional arguments to call func with - :param dict kwargs: dict of keyword arguments to call func with - :param str|unicode id: explicit identifier for the job (for modifying it later) - :param str|unicode name: textual description of the job - :param int misfire_grace_time: seconds after the designated runtime that the job is still - allowed to be run (or ``None`` to allow the job to run no matter how late it is) - :param bool coalesce: run once instead of many times if the scheduler determines that the - job should be run more than once in succession - :param int max_instances: maximum number of concurrently running instances allowed for this - job - :param datetime next_run_time: when to first run the job, regardless of the trigger (pass - ``None`` to add the job as paused) - :param str|unicode jobstore: alias of the job store to store the job in - :param str|unicode executor: alias of the executor to run the job with - :param bool replace_existing: ``True`` to replace an existing job with the same ``id`` - (but retain the number of runs from the existing one) - :return: Job - """ - -.. code-block:: python - - def add_nonblocking_job( - self, func: Callable, trigger: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, id: str = None, name: str = None, - misfire_grace_time: int = undefined, coalesce: bool = undefined, max_instances: int = undefined, - next_run_time: datetime = undefined, jobstore: str = 'default', executor: str = 'default', - replace_existing: bool = False, **trigger_args: Any) -> Job: - """ - Just an apscheduler add job wrapper. - :param func: callable (or a textual reference to one) to run at the given time - :param str|apscheduler.triggers.base.BaseTrigger trigger: trigger that determines when - ``func`` is called - :param list|tuple args: list of positional arguments to call func with - :param dict kwargs: dict of keyword arguments to call func with - :param str|unicode id: explicit identifier for the job (for modifying it later) - :param str|unicode name: textual description of the job - :param int misfire_grace_time: seconds after the designated runtime that the job is still - allowed to be run (or ``None`` to allow the job to run no matter how late it is) - :param bool coalesce: run once instead of many times if the scheduler determines that the - job should be run more than once in succession - :param int max_instances: maximum number of concurrently running instances allowed for this - job - :param datetime next_run_time: when to first run the job, regardless of the trigger (pass - ``None`` to add the job as paused) - :param str|unicode jobstore: alias of the job store to store the job in - :param str|unicode executor: alias of the executor to run the job with - :param bool replace_existing: ``True`` to replace an existing job with the same ``id`` - (but retain the number of runs from the existing one) - :return: Job - """ - -.. code-block:: python - - def get_blocking_scheduler(self) -> BlockingScheduler: - """ - Return self blocking scheduler - :return: BlockingScheduler - """ - -.. code-block:: python - - def get_nonblocking_scheduler(self) -> BackgroundScheduler: - """ - Return self background scheduler - :return: BackgroundScheduler - """ - -.. code-block:: python - - def start_block_scheduler(self, *args: Any, **kwargs: Any) -> None: - """ - Start blocking scheduler - :return: None - """ - -.. code-block:: python - - def start_nonblocking_scheduler(self, *args: Any, **kwargs: Any) -> None: - """ - Start background scheduler - :return: None - """ - -.. code-block:: python - - def start_all_scheduler(self, *args: Any, **kwargs: Any) -> None: - """ - Start background and blocking scheduler - :return: None - """ - -.. code-block:: python - - def add_interval_blocking_secondly( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, seconds: int = 1, **trigger_args: Any) -> Job: - -.. code-block:: python - - def add_interval_blocking_minutely( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, minutes: int = 1, **trigger_args: Any) -> Job: - -.. code-block:: python - - def add_interval_blocking_hourly( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, hours: int = 1, **trigger_args: Any) -> Job: - -.. code-block:: python - - def add_interval_blocking_daily( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, days: int = 1, **trigger_args: Any) -> Job: - -.. code-block:: python - - def add_interval_blocking_weekly( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, weeks: int = 1, **trigger_args: Any) -> Job: - -.. code-block:: python - - def add_interval_nonblocking_secondly( - self, function: Callable, id: str = None, args: list = None, - kwargs: dict = None, seconds: int = 1, **trigger_args: Any) -> Job: - -.. code-block:: python - - def add_interval_nonblocking_minutely( - self, function: Callable, id: str = None, args: list = None, - kwargs: dict = None, minutes: int = 1, **trigger_args: Any) -> Job: - -.. code-block:: python - - def add_interval_nonblocking_hourly( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, hours: int = 1, **trigger_args: Any) -> Job: - -.. code-block:: python - - def add_interval_nonblocking_daily( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, days: int = 1, **trigger_args: Any) -> Job: - -.. code-block:: python - - def add_interval_nonblocking_weekly( - self, function: Callable, id: str = None, args: Union[list, tuple] = None, - kwargs: dict = None, weeks: int = 1, **trigger_args: Any) -> Job: - -.. code-block:: python - - def add_cron_blocking( - self, function: Callable, id: str = None, **trigger_args: Any) -> Job: - -.. code-block:: python - - def add_cron_nonblocking( - self, function: Callable, id: str = None, **trigger_args: Any) -> Job: - -.. code-block:: python - - def remove_blocking_job(self, id: str, jobstore: str = 'default') -> Any: - -.. code-block:: python - - def remove_nonblocking_job(self, id: str, jobstore: str = 'default') -> Any: - -.. code-block:: python - - def shutdown_blocking_scheduler(self, wait: bool = False) -> None: - -.. code-block:: python - - def shutdown_nonblocking_scheduler(self, wait: bool = False) -> None: diff --git a/docs/source/API/utils/socket_server.rst b/docs/source/API/utils/socket_server.rst index 913e2e8..38db36b 100644 --- a/docs/source/API/utils/socket_server.rst +++ b/docs/source/API/utils/socket_server.rst @@ -1,56 +1,54 @@ -.. code-block:: python +Socket Server API +================= - import json - import socketserver - import sys - import threading +``je_web_runner.utils.socket_server.web_runner_socket_server`` - from je_web_runner.utils.executor.action_executor import execute_action +Class: TCPServerHandler +----------------------- +.. code-block:: python class TCPServerHandler(socketserver.BaseRequestHandler): + """ + Request handler for the WebRunner TCP server. + + Receives UTF-8 encoded JSON action strings (max 8192 bytes), + executes them via execute_action(), and returns results. - def handle(self): - command_string = str(self.request.recv(8192).strip(), encoding="utf-8") - socket = self.request - print("command is: " + command_string, flush=True) - if command_string == "quit_server": - self.server.shutdown() - self.server.close_flag = True - print("Now quit server", flush=True) - else: - try: - execute_str = json.loads(command_string) - for execute_function, execute_return in execute_action(execute_str).items(): - socket.sendto(str(execute_return).encode("utf-8"), self.client_address) - socket.sendto("\n".encode("utf-8"), self.client_address) - socket.sendto("Return_Data_Over_JE".encode("utf-8"), self.client_address) - socket.sendto("\n".encode("utf-8"), self.client_address) - except Exception as error: - try: - socket.sendto(str(error).encode("utf-8"), self.client_address) - socket.sendto("\n".encode("utf-8"), self.client_address) - socket.sendto("Return_Data_Over_JE".encode("utf-8"), self.client_address) - socket.sendto("\n".encode("utf-8"), self.client_address) - except Exception as error: - print(repr(error)) + Special command: "quit_server" shuts down the server. + Response protocol: + - Each result is sent as UTF-8 followed by newline + - Final message: "Return_Data_Over_JE\n" + """ + +Class: TCPServer +---------------- + +.. code-block:: python class TCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + """ + Multi-threaded TCP server. + + Attributes: + close_flag (bool): Set to True when server receives shutdown command. + """ + +Function: start_web_runner_socket_server +---------------------------------------- + +.. code-block:: python + + def start_web_runner_socket_server(host: str = "localhost", port: int = 9941) -> TCPServer: + """ + Start the WebRunner TCP Socket Server in a background daemon thread. + + Host and port can be overridden via sys.argv: + - 1 arg: host + - 2 args: host, port - def __init__(self, server_address, RequestHandlerClass): - super().__init__(server_address, RequestHandlerClass) - self.close_flag: bool = False - - - def start_web_runner_socket_server(host: str = "localhost", port: int = 9941): - if len(sys.argv) == 2: - host = sys.argv[1] - elif len(sys.argv) == 3: - host = sys.argv[1] - port = int(sys.argv[2]) - server = TCPServer((host, port), TCPServerHandler) - server_thread = threading.Thread(target=server.serve_forever) - server_thread.daemon = True - server_thread.start() - return server + :param host: server host (default: "localhost") + :param port: server port (default: 9941) + :return: TCPServer instance (already serving) + """ diff --git a/docs/source/API/utils/test_object.rst b/docs/source/API/utils/test_object.rst index 8769b3b..af50514 100644 --- a/docs/source/API/utils/test_object.rst +++ b/docs/source/API/utils/test_object.rst @@ -1,15 +1,76 @@ +Test Object API +=============== + +``je_web_runner.utils.test_object.test_object_class`` + +Class: TestObject +----------------- + .. code-block:: python - def create_test_object(object_type, test_object_name: str) -> TestObject: + class TestObject: """ - :param object_type: test object type should in type_list - :param test_object_name: this test object name - :return: test object + Encapsulates element locator information. + + Attributes: + test_object_type (str): locator strategy (e.g., "name", "id", "xpath") + test_object_name (str): locator value (e.g., "search", "//div[@id='main']") """ + def __init__(self, test_object_name: str, test_object_type: str): + ... + +Functions +--------- + .. code-block:: python + def create_test_object(object_type: str, test_object_name: str) -> TestObject: + """ + Factory function to create a TestObject. + + :param object_type: locator strategy (must be in type_list) + :param test_object_name: locator value + :return: TestObject instance + """ + def get_test_object_type_list() -> list: """ - :return: list include what type should be used to create test object + Get available locator types from selenium.webdriver.common.by.By. + + :return: list of type strings + ['ID', 'NAME', 'XPATH', 'CSS_SELECTOR', 'CLASS_NAME', + 'TAG_NAME', 'LINK_TEXT', 'PARTIAL_LINK_TEXT'] + """ + +TestObjectRecord +---------------- + +``je_web_runner.utils.test_object.test_object_record.test_object_record_class`` + +.. code-block:: python + + class TestObjectRecord: + """ + Stores TestObject instances by name for later retrieval. + Used by _with_test_object methods and the Action Executor. + + Attributes: + test_object_record_dict (dict): {name: TestObject} storage """ + + def save_test_object(self, test_object_name: str, object_type: str) -> None: + """Save a TestObject by name.""" + + def remove_test_object(self, test_object_name: str) -> Union[TestObject, bool]: + """Remove and return a TestObject by name. Returns False if not found.""" + + def clean_record(self) -> None: + """Clear all stored TestObjects.""" + +Global Instance +~~~~~~~~~~~~~~~ + +.. code-block:: python + + test_object_record = TestObjectRecord() diff --git a/docs/source/API/utils/test_record.rst b/docs/source/API/utils/test_record.rst new file mode 100644 index 0000000..a6b8774 --- /dev/null +++ b/docs/source/API/utils/test_record.rst @@ -0,0 +1,61 @@ +Test Record API +=============== + +``je_web_runner.utils.test_record.test_record_class`` + +Class: TestRecord +----------------- + +.. code-block:: python + + class TestRecord: + """ + Manages recording of test actions for audit and report generation. + + Attributes: + test_record_list (list): list of recorded action dicts + init_record (bool): whether recording is enabled (default: False) + """ + + def set_record_enable(self, set_enable: bool = True) -> None: + """ + Enable or disable test recording. + + :param set_enable: True to enable, False to disable + """ + + def clean_record(self) -> None: + """Clear all recorded actions.""" + +Function: record_action_to_list +------------------------------- + +.. code-block:: python + + def record_action_to_list( + function_name: str, + local_param: Union[dict, None], + program_exception: Union[Exception, None] = None + ) -> None: + """ + Record a function execution to the global test_record_instance. + + Each record is a dict: + { + "function_name": str, + "local_param": dict or None, + "time": str (timestamp), + "program_exception": str ("None" or exception repr) + } + + :param function_name: name of the executed function + :param local_param: parameters passed to the function + :param program_exception: exception that occurred (None if success) + """ + +Global Instance +--------------- + +.. code-block:: python + + test_record_instance = TestRecord() diff --git a/docs/source/API/utils/test_reocrd.rst b/docs/source/API/utils/test_reocrd.rst deleted file mode 100644 index a158d2a..0000000 --- a/docs/source/API/utils/test_reocrd.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. code-block:: python - - def record_action_to_list(function_name: str, local_param: Union[vars, None], - program_exception: Union[Exception, None] = None): - """ - :param function_name: what function call this method - :param local_param: what param used - :param program_exception: what exception happened - :return: None - """ \ No newline at end of file diff --git a/docs/source/API/utils/xml.rst b/docs/source/API/utils/xml.rst index e69de29..df2c41e 100644 --- a/docs/source/API/utils/xml.rst +++ b/docs/source/API/utils/xml.rst @@ -0,0 +1,54 @@ +XML Utilities API +================= + +``je_web_runner.utils.xml.xml_file.xml_file`` + +Class: XMLParser +---------------- + +.. code-block:: python + + class XMLParser: + """ + XML parser that supports parsing from string or file. + + Attributes: + element_tree: xml.etree.ElementTree module + tree: parsed ElementTree + xml_root: root Element of the parsed XML + xml_from_type (str): source type ("string" or "file") + """ + + def __init__(self, xml_string: str, xml_type: str = "string"): + """ + :param xml_string: XML content string or file path + :param xml_type: "string" to parse from string, "file" to parse from file + :raises XMLTypeException: if xml_type is not "string" or "file" + """ + + def xml_parser_from_string(self, **kwargs) -> Element: + """ + Parse XML from a string. + + :return: root Element + """ + + def xml_parser_from_file(self, **kwargs) -> Element: + """ + Parse XML from a file. + + :return: root Element + """ + +Function: reformat_xml_file +--------------------------- + +.. code-block:: python + + def reformat_xml_file(xml_string: str) -> str: + """ + Pretty-print an XML string with indentation. + + :param xml_string: XML string to format + :return: formatted XML string + """ diff --git a/docs/source/API/wrapper/element.rst b/docs/source/API/wrapper/element.rst index 1d6f76f..9bf35dd 100644 --- a/docs/source/API/wrapper/element.rst +++ b/docs/source/API/wrapper/element.rst @@ -1,95 +1,74 @@ Element API ----- +=========== -.. code-block:: python +``je_web_runner.element.web_element_wrapper`` - def submit(self) -> None: - """ - current web element submit - :return: None - """ +Class: WebElementWrapper +------------------------ - def clear(self) -> None: - """ - current web element clear - :return: None - """ +Wraps Selenium WebElement with convenience methods for interaction and validation. - def get_property(self, name: str) -> str: - """ - :param name: name of property - :return: property value as str - """ +**Attributes:** - def get_dom_attribute(self, name: str) -> str: - """ - :param name: name of dom - :return: dom attribute value as str - """ +- ``current_web_element`` (``WebElement | None``): currently active element +- ``current_web_element_list`` (``List[WebElement] | None``): list of found elements - def get_attribute(self, name: str) -> str: - """ - :param name: name of web element - :return:web element attribute value as str - """ +**Methods:** - def is_selected(self) -> bool: - """ - check current web element is selected or not - :return: True or False - """ +.. code-block:: python - def is_enabled(self) -> bool: - """ - check current web element is enable or not - :return: True or False - """ + def click_element(self) -> None: + """Click the current WebElement.""" def input_to_element(self, input_value: str) -> None: - """ - input value to current web element - :param input_value: what value we want to input to current web element - :return: None - """ + """Type text into the current WebElement via send_keys.""" - def click_element(self) -> None: - """ - click current web element - :return: None - """ + def clear(self) -> None: + """Clear the content of the current WebElement.""" - def is_displayed(self) -> bool: - """ - check current web element is displayed or not - :return: True or False - """ + def submit(self) -> None: + """Submit the current WebElement's form.""" - def value_of_css_property(self, property_name: str) -> str: - """ - :param property_name: name of property - :return: css property value as str - """ + def get_attribute(self, name: str) -> str | None: + """Get an HTML attribute value.""" - def screenshot(self, filename: str) -> bool: - """ - :param filename: full file name not need .png extension - :return: Save True or not - """ + def get_property(self, name: str) -> None | str | bool | WebElement | dict: + """Get a JavaScript property value.""" + + def get_dom_attribute(self, name: str) -> str | None: + """Get a DOM attribute value.""" + + def is_displayed(self) -> bool | None: + """Check if the element is visible.""" + + def is_enabled(self) -> bool | None: + """Check if the element is enabled.""" + + def is_selected(self) -> bool | None: + """Check if the element is selected (checkbox/radio).""" + + def value_of_css_property(self, property_name: str) -> str | None: + """Get a CSS property value.""" + + def screenshot(self, filename: str) -> bool | None: + """Take a screenshot of the element. Saves as {filename}.png.""" def change_web_element(self, element_index: int) -> None: - """ - :param element_index: change to web element index - :return: web element list [element_index] - """ + """Switch active element to one from current_web_element_list by index.""" def check_current_web_element(self, check_dict: dict) -> None: """ - :param check_dict: check web element dict {name: should be value} - :return: None + Validate the current WebElement's properties. + :param check_dict: {property_name: expected_value} + :raises WebRunnerAssertException: if validation fails """ - def get_select(self) -> Select: - """ - get Select(current web element) - :return: Select(current web element) - """ + def get_select(self) -> Select | None: + """Get a Selenium Select wrapper for dropdown elements.""" + +Global Instance +--------------- + +.. code-block:: python + + web_element_wrapper = WebElementWrapper() diff --git a/docs/source/API/wrapper/manager.rst b/docs/source/API/wrapper/manager.rst index e2c0d31..dfb1518 100644 --- a/docs/source/API/wrapper/manager.rst +++ b/docs/source/API/wrapper/manager.rst @@ -1,46 +1,72 @@ Manager API ----- +=========== + +``je_web_runner.manager.webrunner_manager`` + +Class: WebdriverManager +----------------------- + +Manages multiple WebDriver instances for parallel browser automation. + +**Attributes:** + +- ``webdriver_wrapper`` (``WebDriverWrapper``): WebDriver operations wrapper +- ``webdriver_element`` (``WebElementWrapper``): element operations wrapper +- ``current_webdriver`` (``WebDriver | None``): currently active WebDriver + +**Methods:** .. code-block:: python - def new_driver(self, webdriver_name: str, **kwargs) -> None: + def new_driver(self, webdriver_name: str, options: List[str] = None, **kwargs) -> None: """ - use to create new webdriver instance - :param webdriver_name: which webdriver we want to use [chrome, chromium, firefox, edge, ie] - :param kwargs: webdriver download manager param - :return: None + Create a new WebDriver instance and add it to the managed list. + + :param webdriver_name: browser name [chrome, chromium, firefox, edge, ie] + :param options: browser startup arguments (e.g., ["--headless"]) + :param kwargs: additional parameters passed to set_driver() """ def change_webdriver(self, index_of_webdriver: int) -> None: """ - change to target webdriver - :param index_of_webdriver: change current webdriver to choose index webdriver - :return: None + Switch the active WebDriver to the one at the given index. + + :param index_of_webdriver: index in the internal WebDriver list """ def close_current_webdriver(self) -> None: - """ - close current webdriver - :return: None - """ + """Close and remove the currently active WebDriver.""" def close_choose_webdriver(self, webdriver_index: int) -> None: """ - close choose webdriver - :param webdriver_index: close choose webdriver on current webdriver list - :return: None + Close a specific WebDriver by index. + + :param webdriver_index: index in the internal WebDriver list """ def quit(self) -> None: """ - close and quit all webdriver instance - :return: None + Close and quit ALL managed WebDriver instances. + Also cleans up all TestObjectRecord entries. """ +Factory Function +---------------- + +.. code-block:: python + def get_webdriver_manager(webdriver_name: str, **kwargs) -> WebdriverManager: """ - use to get webdriver instance - :param webdriver_name: which webdriver we want to use [chrome, chromium, firefox, edge, ie] - :param kwargs: webdriver download manager param - :return: Webdriver manager instance + Get the global WebdriverManager and create a new driver. + + :param webdriver_name: browser name [chrome, chromium, firefox, edge, ie] + :param kwargs: additional parameters (e.g., options) + :return: global WebdriverManager instance """ + +Global Instance +--------------- + +.. code-block:: python + + web_runner = WebdriverManager() diff --git a/docs/source/API/wrapper/utils.rst b/docs/source/API/wrapper/utils.rst index 8513992..087858d 100644 --- a/docs/source/API/wrapper/utils.rst +++ b/docs/source/API/wrapper/utils.rst @@ -1,100 +1,51 @@ -Utils API ----- +Selenium Utilities API +====================== + +Desired Capabilities +-------------------- -* Desired Capabilities +``je_web_runner.utils.selenium_utils_wrapper.desired_capabilities.desired_capabilities`` .. code-block:: python - def get_desired_capabilities_keys() -> Union[str, Any]: + def get_desired_capabilities_keys() -> dict_keys: """ - :return: return all webdriver you can get desired capabilities + Get available WebDriver/browser names. + + :return: dict_keys of available browsers (e.g., 'CHROME', 'FIREFOX', 'EDGE', ...) """ - def get_desired_capabilities(webdriver_name: str) -> \ - [ - DesiredCapabilities.FIREFOX.copy(), - DesiredCapabilities.CHROME.copy(), - DesiredCapabilities.EDGE.copy(), - DesiredCapabilities.SAFARI.copy(), - ]: + def get_desired_capabilities(webdriver_name: str) -> dict: """ - choose webdriver to get desired capabilities - :param webdriver_name: name to get desired capabilities - :return: desired capabilities + Get DesiredCapabilities for a specific browser. + + :param webdriver_name: browser name (e.g., "CHROME", "FIREFOX") + :return: capabilities dict """ -* keys +Keys +---- -.. code-block:: python +``je_web_runner.utils.selenium_utils_wrapper.keys.selenium_keys`` + +Re-export of ``selenium.webdriver.common.keys.Keys``. - class Keys: - """Set of special keys codes.""" +Provides constants for special keyboard keys: - NULL = "\ue000" - CANCEL = "\ue001" # ^break - HELP = "\ue002" - BACKSPACE = "\ue003" - BACK_SPACE = BACKSPACE - TAB = "\ue004" - CLEAR = "\ue005" - RETURN = "\ue006" - ENTER = "\ue007" - SHIFT = "\ue008" - LEFT_SHIFT = SHIFT - CONTROL = "\ue009" - LEFT_CONTROL = CONTROL - ALT = "\ue00a" - LEFT_ALT = ALT - PAUSE = "\ue00b" - ESCAPE = "\ue00c" - SPACE = "\ue00d" - PAGE_UP = "\ue00e" - PAGE_DOWN = "\ue00f" - END = "\ue010" - HOME = "\ue011" - LEFT = "\ue012" - ARROW_LEFT = LEFT - UP = "\ue013" - ARROW_UP = UP - RIGHT = "\ue014" - ARROW_RIGHT = RIGHT - DOWN = "\ue015" - ARROW_DOWN = DOWN - INSERT = "\ue016" - DELETE = "\ue017" - SEMICOLON = "\ue018" - EQUALS = "\ue019" +**Standard keys:** +``NULL``, ``CANCEL``, ``HELP``, ``BACKSPACE``, ``TAB``, ``CLEAR``, +``RETURN``, ``ENTER``, ``SHIFT``, ``LEFT_SHIFT``, ``CONTROL``, ``LEFT_CONTROL``, +``ALT``, ``LEFT_ALT``, ``PAUSE``, ``ESCAPE``, ``SPACE``, +``PAGE_UP``, ``PAGE_DOWN``, ``END``, ``HOME``, +``LEFT``, ``UP``, ``RIGHT``, ``DOWN``, +``INSERT``, ``DELETE``, ``SEMICOLON``, ``EQUALS`` - NUMPAD0 = "\ue01a" # number pad keys - NUMPAD1 = "\ue01b" - NUMPAD2 = "\ue01c" - NUMPAD3 = "\ue01d" - NUMPAD4 = "\ue01e" - NUMPAD5 = "\ue01f" - NUMPAD6 = "\ue020" - NUMPAD7 = "\ue021" - NUMPAD8 = "\ue022" - NUMPAD9 = "\ue023" - MULTIPLY = "\ue024" - ADD = "\ue025" - SEPARATOR = "\ue026" - SUBTRACT = "\ue027" - DECIMAL = "\ue028" - DIVIDE = "\ue029" +**Number pad:** +``NUMPAD0`` through ``NUMPAD9``, +``MULTIPLY``, ``ADD``, ``SEPARATOR``, ``SUBTRACT``, ``DECIMAL``, ``DIVIDE`` - F1 = "\ue031" # function keys - F2 = "\ue032" - F3 = "\ue033" - F4 = "\ue034" - F5 = "\ue035" - F6 = "\ue036" - F7 = "\ue037" - F8 = "\ue038" - F9 = "\ue039" - F10 = "\ue03a" - F11 = "\ue03b" - F12 = "\ue03c" +**Function keys:** +``F1`` through ``F12`` - META = "\ue03d" - COMMAND = "\ue03d" - ZENKAKU_HANKAKU = "\ue040" +**Special:** +``META``, ``COMMAND``, ``ZENKAKU_HANKAKU`` diff --git a/docs/source/API/wrapper/webdriver.rst b/docs/source/API/wrapper/webdriver.rst index 015fc64..af1c705 100644 --- a/docs/source/API/wrapper/webdriver.rst +++ b/docs/source/API/wrapper/webdriver.rst @@ -1,528 +1,206 @@ WebDriver API ----- +============= + +Options Configuration +--------------------- + +``je_web_runner.webdriver.webdriver_with_options`` -* With options .. code-block:: python - def set_webdriver_options_argument(webdriver_name: str, argument_iterable: [list, set]) -> \ - Union[ - webdriver.chrome.options.Options, webdriver.chrome.options.Options, - webdriver.edge.options.Options, webdriver.ie.options.Options - ]: + def set_webdriver_options_argument( + webdriver_name: str, + argument_iterable: Union[List[str], Set[str]] + ) -> Options: """ - :param webdriver_name: use name to open webdriver manager and download webdriver - :param argument_iterable: start webdriver argument - :return: webdriver + Set browser startup arguments. + + :param webdriver_name: browser name (chrome, firefox, edge, etc.) + :param argument_iterable: list of arguments (e.g., ["--headless", "--disable-gpu"]) + :return: configured Options object + :raises WebRunnerOptionsWrongTypeException: if argument_iterable is wrong type + """ + + def set_webdriver_options_capability_wrapper( + webdriver_name: str, + key_and_vale_dict: dict + ) -> Options: """ + Set browser capabilities. + + :param webdriver_name: browser name + :param key_and_vale_dict: capabilities dict (e.g., {"acceptInsecureCerts": True}) + :return: configured Options object + """ + +WebDriverWrapper +---------------- + +``je_web_runner.webdriver.webdriver_wrapper`` + +Class: WebDriverWrapper +~~~~~~~~~~~~~~~~~~~~~~~ + +Core WebDriver wrapper with 80+ methods. + +**Attributes:** + +- ``current_webdriver`` (``WebDriver | None``): active WebDriver +- ``_webdriver_name`` (``str | None``): name of current driver +- ``_action_chain`` (``ActionChains | None``): action chains instance + +**Driver Setup:** + +.. code-block:: python + + def set_driver(self, webdriver_name: str, webdriver_manager_option_dict: dict = None, + options: List[str] = None, **kwargs) -> WebDriver: + """Start a new WebDriver instance.""" + + def set_webdriver_options_capability(self, key_and_vale_dict: dict) -> Options: + """Set capabilities on current WebDriver's options.""" + +**Element Finding:** + +.. code-block:: python + + def find_element(self, test_object: TestObject) -> WebElement | None: + """Find a single element using TestObject locator.""" + + def find_elements(self, test_object: TestObject) -> list[WebElement] | None: + """Find multiple elements using TestObject locator.""" + + def find_element_with_test_object_record(self, element_name: str) -> WebElement | None: + """Find element using a saved TestObjectRecord name.""" + + def find_elements_with_test_object_record(self, element_name: str) -> list[WebElement] | None: + """Find multiple elements using a saved TestObjectRecord name.""" + +**Wait:** + +.. code-block:: python + + def implicitly_wait(self, time_to_wait: int) -> None: + def explict_wait(self, wait_condition, timeout: int = 10, poll_frequency: float = 0.5): + def set_script_timeout(self, timeout: int) -> None: + def set_page_load_timeout(self, timeout: int) -> None: + +**Navigation:** + +.. code-block:: python + + def to_url(self, url: str) -> None: + def forward(self) -> None: + def back(self) -> None: + def refresh(self) -> None: + +**Context Switching:** - def set_webdriver_options_capability_wrapper(webdriver_name: str, key_and_vale_dict: dict) -> \ - Union[ - webdriver.chrome.options.Options, webdriver.chrome.options.Options, - webdriver.edge.options.Options, webdriver.ie.options.Options - ]: +.. code-block:: python + + def switch(self, switch_type: str, switch_value: str = None) -> None: """ - :param webdriver_name: use name to open webdriver manager and download webdriver - :param key_and_vale_dict: set webdriver options capability - :return: webdriver + Switch context. Supported types: + active_element, default_content, frame, parent_frame, window, alert """ -* WebDriverWrapper +**Cookie Management:** + +.. code-block:: python + + def get_cookies(self) -> list: + def get_cookie(self, name: str) -> dict: + def add_cookie(self, cookie_dict: dict) -> None: + def delete_cookie(self, name: str) -> None: + def delete_all_cookies(self) -> None: + +**JavaScript:** + +.. code-block:: python + + def execute(self, script: str, *args) -> Any: + def execute_script(self, script: str, *args) -> Any: + def execute_async_script(self, script: str, *args) -> Any: + +**Mouse Actions:** + +.. code-block:: python + + def left_click(self) -> None: + def left_click_with_test_object(self, element_name: str) -> None: + def right_click(self) -> None: + def right_click_with_test_object(self, element_name: str) -> None: + def left_double_click(self) -> None: + def left_double_click_with_test_object(self, element_name: str) -> None: + def left_click_and_hold(self) -> None: + def left_click_and_hold_with_test_object(self, element_name: str) -> None: + def release(self) -> None: + def release_with_test_object(self, element_name: str) -> None: + def move_to_element(self, element: WebElement) -> None: + def move_to_element_with_test_object(self, element_name: str) -> None: + def move_to_element_with_offset(self, element, offset_x: int, offset_y: int) -> None: + def move_to_element_with_offset_and_test_object(self, element_name: str, offset_x: int, offset_y: int) -> None: + def drag_and_drop(self, source, target) -> None: + def drag_and_drop_with_test_object(self, source_name: str, target_name: str) -> None: + def drag_and_drop_offset(self, element, offset_x: int, offset_y: int) -> None: + def drag_and_drop_offset_with_test_object(self, element_name: str, offset_x: int, offset_y: int) -> None: + def move_by_offset(self, offset_x: int, offset_y: int) -> None: + +**Keyboard:** + +.. code-block:: python + + def press_key(self, key) -> None: + def press_key_with_test_object(self, key) -> None: + def release_key(self, key) -> None: + def release_key_with_test_object(self, key) -> None: + def send_keys(self, keys: str) -> None: + def send_keys_to_element(self, element, keys: str) -> None: + def send_keys_to_element_with_test_object(self, element_name: str, keys: str) -> None: + +**Action Chain:** + +.. code-block:: python + + def perform(self) -> None: + def reset_actions(self) -> None: + def pause(self, duration: int) -> None: + def scroll(self, offset_x: int, offset_y: int) -> None: + +**Window Management:** + +.. code-block:: python + + def maximize_window(self) -> None: + def fullscreen_window(self) -> None: + def minimize_window(self) -> None: + def set_window_size(self, width: int, height: int) -> None: + def set_window_position(self, x: int, y: int) -> None: + def get_window_position(self) -> dict: + def get_window_rect(self) -> dict: + def set_window_rect(self, x=None, y=None, width=None, height=None) -> None: + +**Screenshots & Logging:** + +.. code-block:: python + + def get_screenshot_as_png(self) -> bytes: + def get_screenshot_as_base64(self) -> str: + def get_log(self, log_type: str) -> list: + +**Validation & Quit:** + +.. code-block:: python + + def check_current_webdriver(self, check_dict: dict) -> None: + """Validate WebDriver properties against expected values.""" + + def quit(self) -> None: + """Quit the current single WebDriver.""" + +Global Instance +~~~~~~~~~~~~~~~ .. code-block:: python - def set_driver(self, webdriver_name: str, - webdriver_manager_option_dict: dict = None, **kwargs) -> \ - Union[ - webdriver.Chrome, - webdriver.Chrome, - webdriver.Firefox, - webdriver.Edge, - webdriver.Ie, - webdriver.Safari, - ]: - """ - :param webdriver_name: which webdriver we want to use - :param webdriver_manager_option_dict: if you want to set webdriver download manager - :param kwargs: used to catch var - :return: current use webdriver - """ - - def set_webdriver_options_capability(self, key_and_vale_dict: dict) -> \ - Union[ - webdriver.Chrome, - webdriver.Chrome, - webdriver.Firefox, - webdriver.Edge, - webdriver.Ie, - webdriver.Safari, - ]: - """ - :param key_and_vale_dict: use to set webdriver capability - :return: current webdriver - """ - - def find_element(self, test_object: TestObject) -> WebElement: - """ - :param test_object: use test object to find element - :return: fined web element - """ - - def find_elements(self, test_object: TestObject) -> List[WebElement]: - """ - :param test_object: use test object to find elements - :return: list include fined web element - """ - - def find_element_with_test_object_record(self, element_name: str) -> WebElement: - """ - this is executor use but still can normally use - :param element_name: test object name - :return: fined web element - """ - - def find_elements_with_test_object_record(self, element_name: str) -> List[WebElement]: - """ - this is executor use but still can normally use - :param element_name: test object name - :return: list include fined web element - """ - - def implicitly_wait(self, time_to_wait: int) -> None: - """ - selenium implicitly_wait - :param time_to_wait: how much time we want to wait - :return: None - """ - - def explict_wait(self, wait_time: int, method: typing.Callable, until_type: bool = True): - """ - selenium explict_wait - :param wait_time: how much time we want to wait if over-time will raise an exception - :param method: a program statement should be return True or False - :param until_type: what type until wait True is until False is until_not - :return: - """ - - def to_url(self, url: str) -> None: - """ - to url - :param url: what url we want redirect to - :return: None - """ - - def forward(self) -> None: - """ - forward current page - :return: None - """ - - def back(self) -> None: - """ - back current page - :return: None - """ - - def refresh(self) -> None: - """ - refresh current page - :return: None - """ - - def switch(self, switch_type: str, switch_target_name: str = None): - """ - switch to target element - :param switch_type: what type switch? one of [active_element, default_content, frame, - parent_frame, window, alert] - :param switch_target_name: what target we want to switch use name to search - :return: what we switch to - """ - - def set_script_timeout(self, time_to_wait: int) -> None: - """ - set max script execute time - :param time_to_wait: how much time we want to wait if over-time will raise an exception - :return: None - """ - - def set_page_load_timeout(self, time_to_wait: int) -> None: - """ - set page load max wait time - :param time_to_wait: how much time we want to wait if over-time will raise an exception - :return: None - """ - - def get_cookies(self) -> List[dict]: - """ - get current page cookies - :return: cookies as list - """ - - def get_cookie(self, name: str) -> dict: - """ - use to get current page cookie - :param name: use cookie name to find cookie - :return: {cookie_name: value} - """ - - def add_cookie(self, cookie_dict: dict) -> None: - """ - use to add cookie to current page - :param cookie_dict: {cookie_name: value} - :return: None - """ - - def delete_cookie(self, name) -> None: - """ - use to delete current page cookie - :param name: use name to find cookie - :return: None - """ - - def delete_all_cookies(self) -> None: - """ - delete current page all cookies - :return: None - """ - - def execute(self, driver_command: str, params: dict = None) -> dict: - """ - :param driver_command: webdriver command - :param params: webdriver command params - :return: after execute dict - """ - - def execute_script(self, script: str, *args) -> None: - """ - execute script - :param script: script to execute - :param args: script args - :return: None - """ - - def execute_async_script(self, script: str, *args): - """ - execute script async - :param script:script to execute - :param args: script args - :return: None - """ - - def move_to_element(self, targe_element: WebElement) -> None: - """ - move mouse to target web element - :param targe_element: target web element - :return: None - """ - - def move_to_element_with_test_object(self, element_name: str): - """ - move mouse to target web element use test object - :param element_name: test object name - :return: None - """ - - def move_to_element_with_offset(self, target_element: WebElement, offset_x: int, offset_y: int) -> None: - """ - move to target element with offset - :param target_element: what target web element we want to move to - :param offset_x: offset x - :param offset_y: offset y - :return: None - """ - - def move_to_element_with_offset_and_test_object(self, element_name: str, offset_x: int, offset_y: int) -> None: - """ - move to target element with offset use test object - :param element_name: test object name - :param offset_x: offset x - :param offset_y: offset y - :return: None - """ - - def drag_and_drop(self, web_element: WebElement, targe_element: WebElement) -> None: - """ - drag web element to target element then drop - :param web_element: which web element we want to drag and drop - :param targe_element: target web element to drop - :return: None - """ - - def drag_and_drop_with_test_object(self, element_name: str, target_element_name: str): - """ - drag web element to target element then drop use testobject - :param element_name: which web element we want to drag and drop use name to find - :param target_element_name: target web element to drop use name to find - :return: None - """ - - def drag_and_drop_offset(self, web_element: WebElement, target_x: int, target_y: int) -> None: - """ - drag web element to target element then drop with offset - :param web_element: which web element we want to drag and drop with offset - :param target_x: offset x - :param target_y: offset y - :return: None - """ - - def drag_and_drop_offset_with_test_object(self, element_name: str, offset_x: int, offset_y: int) -> None: - """ - drag web element to target element then drop with offset and test object - :param element_name: test object name - :param offset_x: offset x - :param offset_y: offset y - :return: None - """ - - def perform(self) -> None: - """ - perform actions - :return: None - """ - - def reset_actions(self) -> None: - """ - clear actions - :return: None - """ - - def left_click(self, on_element: WebElement = None) -> None: - """ - left click mouse on current mouse position or click on web element - :param on_element: can be None or web element - :return: None - """ - - def left_click_with_test_object(self, element_name: str = None) -> None: - """ - left click mouse on current mouse position or click on web element - find use test object name - :param element_name: test object name - :return: None - """ - - def left_click_and_hold(self, on_element: WebElement = None) -> None: - """ - left click and hold on current mouse position or left click and hold on web element - :param on_element: can be None or web element - :return: None - """ - - def left_click_and_hold_with_test_object(self, element_name: str = None) -> None: - """ - left click and hold on current mouse position or left click and hold on web element - find use test object name - :param element_name: test object name - :return: None - """ - - def right_click(self, on_element: WebElement = None) -> None: - """ - right click mouse on current mouse position or click on web element - :param on_element: can be None or web element - :return: None - """ - - def right_click_with_test_object(self, element_name: str = None) -> None: - """ - right click mouse on current mouse position or click on web element - find use test object name - :param element_name: test object name - :return: None - """ - - def left_double_click(self, on_element: WebElement = None) -> None: - """ - double left click mouse on current mouse position or double click on web element - :param on_element: can be None or web element - :return: None - """ - - def left_double_click_with_test_object(self, element_name: str = None) -> None: - """ - double left click mouse on current mouse position or double click on web element - find use test object name - :param element_name: test object name - :return: None - """ - - def release(self, on_element: WebElement = None) -> None: - """ - release mouse or web element - :param on_element: can be None or web element - :return: None - """ - - def release_with_test_object(self, element_name: str = None) -> None: - """ - release mouse or web element find use test object name - :param element_name: test object name - :return: None - """ - def press_key(self, keycode_on_key_class, on_element: WebElement = None) -> None: - """ - press key or press key on web element key should be in Key - :param keycode_on_key_class: which key code to press - :param on_element: can be None or web element - :return: None - """ - - def press_key_with_test_object(self, keycode_on_key_class, element_name: str = None) -> None: - """ - press key or press key on web element key should be in Key find web element use test object name - :param keycode_on_key_class: which key code to press - :param element_name: test object name - :return: None - """ - - def release_key(self, keycode_on_key_class, on_element: WebElement = None) -> None: - """ - release key or release key on web element key should be in Key - :param keycode_on_key_class: which key code to release - :param on_element: can be None or web element - :return: None - """ - - def release_key_with_test_object(self, keycode_on_key_class, element_name: str = None) -> None: - """ - release key or release key on web element key should be in Key - find use test object - :param keycode_on_key_class: which key code to release - :param element_name: test object name - :return: None - """ - - def move_by_offset(self, offset_x: int, offset_y: int) -> None: - """ - move mouse use offset - :param offset_x: offset x - :param offset_y: offset y - :return: None - """ - - def pause(self, seconds: int) -> None: - """ - pause seconds time (this many be let selenium raise some exception) - :param seconds: seconds to pause - :return: None - """ - - def send_keys(self, keys_to_send) -> None: - """ - send(press and release) keyboard key - :param keys_to_send: which key on keyboard we want to send - :return: None - """ - - def send_keys_to_element(self, element: WebElement, keys_to_send) -> None: - """ - :param element: which element we want send key to - :param keys_to_send: which key on keyboard we want to send - :return: None - """ - - def send_keys_to_element_with_test_object(self, element_name: str, keys_to_send) -> None: - """ - :param element_name: test object name - :param keys_to_send: which key on keyboard we want to send find use test object - :return: None - """ - - def scroll(self, scroll_x: int, scroll_y: int, delta_x: int, delta_y: int, - duration: int = 0, origin: str = "viewport") -> None: - """ - :param scroll_x: starting x coordinate - :param scroll_y: starting y coordinate - :param delta_x: the distance the mouse will scroll on the x axis - :param delta_y: the distance the mouse will scroll on the y axis - :param duration: delay to wheel - :param origin: what is origin to scroll - :return: - """ - - def maximize_window(self) -> None: - """ - maximize current window - :return: None - """ - - def full_screen_window(self) -> None: - """ - full-screen current window - :return: None - """ - - def minimize_window(self) -> None: - """ - minimize current window - :return: None - """ - - def set_window_size(self, width, height, window_handle='current') -> dict: - """ - :param width: window width (pixel) - :param height: window height (pixel) - :param window_handle: normally is "current" (w3c) if not "current" will make exception - :return: size - """ - - def set_window_position(self, x, y, window_handle='current') -> dict: - """ - :param x: position x - :param y: position y - :param window_handle: normally is "current" (w3c) if not "current" will make exception - :return: execute(Command.SET_WINDOW_RECT, - {"x": x, "y": y, "width": width, "height": height})['value'] - """ - - def get_window_position(self, window_handle='current') -> dict: - """ - :param window_handle: normally is "current" (w3c) if not "current" will make exception - :return: window position dict - """ - - def get_window_rect(self) -> dict: - """ - :return: execute(Command.GET_WINDOW_RECT)['value'] - """ - - def set_window_rect(self, x: int = None, y: int = None, width: int = None, height: int = None) -> dict: - """ - only supported for w3c compatible another browsers need use set_window_position or set_window_size - :param x: set x coordinates - :param y: set y coordinates - :param width: set window width - :param height: set window height - :return: execute(Command.SET_WINDOW_RECT, - {"x": x, "y": y, "width": width, "height": height})['value'] - """ - - def get_screenshot_as_png(self) -> bytes: - """ - get current page screenshot as png - :return: screenshot as bytes - """ - - def get_screenshot_as_base64(self) -> str: - """ - get current page screenshot as base64 str - :return: screenshot as str - """ - - def get_log(self, log_type: str): - """ - :param log_type: ["browser", "driver", client", "server] - :return: execute(Command.GET_LOG, {'type': log_type})['value'] - """ - - def check_current_webdriver(self, check_dict: dict) -> None: - """ - if check failure will raise an exception - :param check_dict: use to check current webdriver state - :return: None - """ - - def quit(self) -> None: - """ - quit this webdriver - :return: None - """ + webdriver_wrapper_instance = WebDriverWrapper() diff --git a/docs/source/Eng/doc/action_executor/action_executor_doc.rst b/docs/source/Eng/doc/action_executor/action_executor_doc.rst new file mode 100644 index 0000000..a2764f5 --- /dev/null +++ b/docs/source/Eng/doc/action_executor/action_executor_doc.rst @@ -0,0 +1,401 @@ +Action Executor +=============== + +Overview +-------- + +The Action Executor is a powerful engine that maps command strings to callable functions. +It allows you to define automation scripts as JSON action lists, enabling data-driven automation workflows. + +The executor also includes all Python built-in functions, so you can call ``print``, ``len``, etc. from action lists. + +Action Format +------------- + +Each action is a list with the command name and optional parameters: + +.. code-block:: python + + ["command_name"] # No parameters + ["command_name", {"key": "value"}] # Keyword arguments + ["command_name", [arg1, arg2]] # Positional arguments + +Basic Usage +----------- + +.. code-block:: python + + from je_web_runner import execute_action + + actions = [ + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_to_url", {"url": "https://www.google.com"}], + ["WR_implicitly_wait", {"time_to_wait": 2}], + ["WR_SaveTestObject", {"test_object_name": "q", "object_type": "name"}], + ["WR_find_element", {"element_name": "q"}], + ["WR_click_element"], + ["WR_input_to_element", {"input_value": "WebRunner"}], + ["WR_quit"] + ] + + result = execute_action(actions) + +The ``execute_action()`` function returns a dict mapping each action to its return value. + +Available Commands +------------------ + +**WebDriver Manager:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_get_webdriver_manager`` + - Create a new WebDriver (params: ``webdriver_name``, ``options``) + * - ``WR_change_index_of_webdriver`` + - Switch to a WebDriver by index + * - ``WR_quit`` + - Quit all WebDrivers + * - ``WR_single_quit`` + - Quit the current single WebDriver + +**Navigation:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_to_url`` + - Navigate to URL (param: ``url``) + * - ``WR_forward`` + - Go forward + * - ``WR_back`` + - Go back + * - ``WR_refresh`` + - Refresh the page + +**Element Finding:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_find_element`` + - Find single element by saved test object name (param: ``element_name``) + * - ``WR_find_elements`` + - Find multiple elements by saved test object name + +**Wait:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_implicitly_wait`` + - Set implicit wait (param: ``time_to_wait``) + * - ``WR_explict_wait`` + - Explicit wait with condition + * - ``WR_set_script_timeout`` + - Set script timeout + * - ``WR_set_page_load_timeout`` + - Set page load timeout + +**Mouse Actions:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_left_click`` + - Left click (on saved test object or current position) + * - ``WR_right_click`` + - Right click + * - ``WR_left_double_click`` + - Double click + * - ``WR_left_click_and_hold`` + - Click and hold + * - ``WR_release`` + - Release held button + * - ``WR_drag_and_drop`` + - Drag from source to target (test object names) + * - ``WR_drag_and_drop_offset`` + - Drag to offset + * - ``WR_move_to_element`` + - Hover over element (test object name) + * - ``WR_move_to_element_with_offset`` + - Hover with offset + * - ``WR_move_by_offset`` + - Move mouse by offset + +**Keyboard:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_press_key`` + - Press a key + * - ``WR_release_key`` + - Release a key + * - ``WR_send_keys`` + - Send keys globally + * - ``WR_send_keys_to_element`` + - Send keys to saved test object + +**Action Chain:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_perform`` + - Execute queued actions + * - ``WR_reset_actions`` + - Clear action queue + * - ``WR_pause`` + - Pause in chain + * - ``WR_scroll`` + - Scroll page + +**Cookies:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_get_cookies`` + - Get all cookies + * - ``WR_get_cookie`` + - Get a specific cookie + * - ``WR_add_cookie`` + - Add a cookie + * - ``WR_delete_cookie`` + - Delete a cookie + * - ``WR_delete_all_cookies`` + - Delete all cookies + +**JavaScript:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_execute`` + - Execute JavaScript + * - ``WR_execute_script`` + - Execute script + * - ``WR_execute_async_script`` + - Execute async script + +**Window:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_maximize_window`` + - Maximize window + * - ``WR_minimize_window`` + - Minimize window + * - ``WR_fullscreen_window`` + - Full screen + * - ``WR_set_window_size`` + - Set window size + * - ``WR_set_window_position`` + - Set window position + * - ``WR_get_window_position`` + - Get window position + * - ``WR_get_window_rect`` + - Get window rect + * - ``WR_set_window_rect`` + - Set window rect + +**Screenshot & Logging:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_get_screenshot_as_png`` + - Get screenshot as PNG bytes + * - ``WR_get_screenshot_as_base64`` + - Get screenshot as base64 + * - ``WR_get_log`` + - Get browser logs + +**Element Interaction:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_click_element`` + - Click the current element + * - ``WR_input_to_element`` + - Type text into element + * - ``WR_element_clear`` + - Clear element content + * - ``WR_element_submit`` + - Submit form + * - ``WR_element_get_attribute`` + - Get element attribute + * - ``WR_element_get_property`` + - Get element property + * - ``WR_element_get_dom_attribute`` + - Get DOM attribute + * - ``WR_element_is_displayed`` + - Check if element is visible + * - ``WR_element_is_enabled`` + - Check if element is enabled + * - ``WR_element_is_selected`` + - Check if element is selected + * - ``WR_element_value_of_css_property`` + - Get CSS property + * - ``WR_element_screenshot`` + - Take element screenshot + * - ``WR_element_change_web_element`` + - Switch active element + * - ``WR_element_check_current_web_element`` + - Validate element properties + * - ``WR_element_get_select`` + - Get Select for dropdown + +**Test Object:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_SaveTestObject`` + - Save a test object (params: ``test_object_name``, ``object_type``) + * - ``WR_CleanTestObject`` + - Clear all saved test objects + +**Report:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_generate_html`` + - Generate HTML report string + * - ``WR_generate_html_report`` + - Save HTML report to file + * - ``WR_generate_json`` + - Generate JSON report dicts + * - ``WR_generate_json_report`` + - Save JSON report to file + * - ``WR_generate_xml`` + - Generate XML report + * - ``WR_generate_xml_report`` + - Save XML report to file + +**Other:** + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Command + - Description + * - ``WR_switch`` + - Switch context (frame, window, alert) + * - ``WR_check_current_webdriver`` + - Validate WebDriver properties + * - ``WR_set_record_enable`` + - Enable/disable test recording + * - ``WR_execute_action`` + - Nested execute action list + * - ``WR_execute_files`` + - Execute from JSON files + * - ``WR_add_package_to_executor`` + - Add a Python package to executor + * - ``WR_add_package_to_callback_executor`` + - Add a Python package to callback executor + +Execute from JSON Files +----------------------- + +.. code-block:: python + + from je_web_runner import execute_files + + # Execute actions from multiple JSON files + results = execute_files(["actions1.json", "actions2.json"]) + +JSON file format: + +.. code-block:: json + + [ + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_to_url", {"url": "https://example.com"}], + ["WR_quit"] + ] + +Dict Format Input +----------------- + +The executor also accepts a dict with a ``"webdriver_wrapper"`` key: + +.. code-block:: python + + execute_action({ + "webdriver_wrapper": [ + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_quit"] + ] + }) + +Add Custom Commands +------------------- + +Register your own functions as executor commands: + +.. code-block:: python + + from je_web_runner import add_command_to_executor + + def my_custom_function(param1, param2): + print(f"Custom: {param1}, {param2}") + + add_command_to_executor({"my_command": my_custom_function}) + + # Now use it in action lists + execute_action([ + ["my_command", {"param1": "hello", "param2": "world"}] + ]) + +.. note:: + + Only ``types.MethodType`` and ``types.FunctionType`` are accepted. + Passing other callable types will raise ``WebRunnerAddCommandException``. diff --git a/docs/source/Eng/doc/assertion/assertion_doc.rst b/docs/source/Eng/doc/assertion/assertion_doc.rst new file mode 100644 index 0000000..a0ed9fe --- /dev/null +++ b/docs/source/Eng/doc/assertion/assertion_doc.rst @@ -0,0 +1,98 @@ +Assertion and Validation +======================== + +Overview +-------- + +WebRunner provides assertion utilities for validating WebDriver and WebElement states +during automation. If a check fails, a ``WebRunnerAssertException`` is raised. + +WebDriver Validation +-------------------- + +Validate properties of the current WebDriver: + +.. code-block:: python + + # Via WebDriverWrapper + wrapper.check_current_webdriver({"name": "chrome"}) + +.. code-block:: python + + # Via utility functions + from je_web_runner.utils.assert_value.result_check import ( + check_webdriver_value, + check_webdriver_values, + ) + + check_webdriver_value("name", "chrome", webdriver_instance) + check_webdriver_values({"name": "chrome"}, webdriver_instance) + +WebElement Validation +--------------------- + +Validate properties of the current WebElement: + +.. code-block:: python + + # Via WebElementWrapper + web_element_wrapper.check_current_web_element({ + "tag_name": "input", + "enabled": True + }) + +.. code-block:: python + + # Via utility functions + from je_web_runner.utils.assert_value.result_check import check_web_element_details + + check_web_element_details(element, { + "tag_name": "input", + "enabled": True + }) + +General Value Checking +---------------------- + +.. code-block:: python + + from je_web_runner.utils.assert_value.result_check import check_value, check_values + + # Check a single value against a result dictionary + check_value("element_name", "expected_value", result_check_dict) + + # Check multiple values + check_values({"name": "expected"}, result_check_dict) + +Via Action Executor +------------------- + +.. code-block:: python + + from je_web_runner import execute_action + + execute_action([ + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_check_current_webdriver", {"check_dict": {"name": "chrome"}}], + ["WR_quit"], + ]) + +Functions Reference +------------------- + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Function + - Description + * - ``check_value(element_name, element_value, result_check_dict)`` + - Check a single value against a result dictionary + * - ``check_values(check_dict, result_check_dict)`` + - Check multiple key-value pairs + * - ``check_webdriver_value(element_name, element_value, webdriver)`` + - Check a single WebDriver property + * - ``check_webdriver_values(check_dict, webdriver)`` + - Check multiple WebDriver properties (alias: ``check_webdriver_details``) + * - ``check_web_element_details(element, check_dict)`` + - Validate WebElement properties against expected values diff --git a/docs/source/Eng/doc/callback_function/callback_function_doc.rst b/docs/source/Eng/doc/callback_function/callback_function_doc.rst index e13e792..986256a 100644 --- a/docs/source/Eng/doc/callback_function/callback_function_doc.rst +++ b/docs/source/Eng/doc/callback_function/callback_function_doc.rst @@ -1,57 +1,109 @@ -Callback Function ----- +Callback Executor +================= -In AutoControl, callback functions are supported by the Callback Executor. -Below is a simple example of using the Callback Executor: +Overview +-------- + +The Callback Executor allows you to execute automation commands with callback functions +triggered on completion. It wraps the standard executor's event dictionary and provides +an event-driven execution model. + +The global instance ``callback_executor`` is imported from ``je_web_runner``. + +Basic Usage +----------- .. code-block:: python from je_web_runner import callback_executor - # trigger_function will first to execute, but return value need to wait everything done - # so this test will first print("test") then print(size_function_return_value) + + def on_complete(): + print("Navigation complete!") + callback_executor.callback_function( - trigger_function_name="WR_get_webdriver_manager", - callback_function=print, - callback_param_method="args", - callback_function_param={"": "open driver"}, - **{ - "webdriver_name": "edge" - } + trigger_function_name="WR_to_url", + callback_function=on_complete, + url="https://example.com" ) -* Note that if the "name: function" pair in the callback_executor event_dict is different from the executor, it is a bug. -* Of course, like the executor, it can be expanded by adding external functions. Please see the example below. +The trigger function (``WR_to_url``) executes first, then the callback (``on_complete``) runs. -In this example, we use callback_executor to execute the "size" function defined in AutoControl. -After executing the "size" function, the function passed to callback_function will be executed. -The delivery method can be determined by the callback_param_method parameter. -If it is "args", please pass in {"value1", "value2", ...}. -Here, the ellipsis (...) represents multiple inputs. - If it is "kwargs", please pass in {"actually_param_name": value, ...}. -Here, the ellipsis (...) again represents multiple inputs. - If you want to use the return value, -since the return value will only be returned after all functions are executed, -you will actually see the "print" statement -before the "print(size_function_return_value)" statement in this example, -even though the order of size -> print is correct. -This is because the "size" function only returns the value itself without printing it. +Callback with kwargs +-------------------- -This code will load all built-in functions, methods, and classes of the time module into the callback executor. -To use the loaded functions, we need to use the package_function name, -for example, time.sleep will become time_sleep. +Pass keyword arguments to the callback using ``callback_param_method="kwargs"``: -If we want to add functions in the callback_executor, we can use the following code: +.. code-block:: python -This code will add all the functions of the time module to the executor (interpreter). + def on_element_found(result=None): + print(f"Element found: {result}") + + callback_executor.callback_function( + trigger_function_name="WR_find_element", + callback_function=on_element_found, + callback_function_param={"result": "search_box"}, + callback_param_method="kwargs", + element_name="search_box" + ) + +Callback with args +------------------ + +Pass positional arguments using ``callback_param_method="args"``: .. code-block:: python - from je_web_runner import package_manager - package_manager.add_package_to_callback_executor("time") + def on_done(msg): + print(f"Done: {msg}") -If you need to check the updated event_dict, you can use: + callback_executor.callback_function( + trigger_function_name="WR_quit", + callback_function=on_done, + callback_function_param=["All browsers closed"], + callback_param_method="args" + ) + +Parameters +---------- + +.. list-table:: + :header-rows: 1 + :widths: 25 20 55 + + * - Parameter + - Type + - Description + * - ``trigger_function_name`` + - ``str`` + - Name of the function to trigger (must exist in ``event_dict``) + * - ``callback_function`` + - ``Callable`` + - The callback function to execute after the trigger + * - ``callback_function_param`` + - ``dict | list | None`` + - Parameters to pass to the callback + * - ``callback_param_method`` + - ``str`` + - How to pass callback params: ``"kwargs"`` or ``"args"`` + * - ``**kwargs`` + - + - Parameters passed to the trigger function + +Return Value +------------ + +The ``callback_function()`` method returns the return value of the **trigger function** +(not the callback). + +Adding Packages +--------------- + +You can add external packages to the callback executor: .. code-block:: python - from je_web_runner import callback_executor - print(callback_executor.event_dict) \ No newline at end of file + from je_web_runner.utils.package_manager.package_manager_class import package_manager + + package_manager.add_package_to_callback_executor("time") + + # Now "time_sleep" is available as a trigger function name diff --git a/docs/source/Eng/doc/cli/cli_doc.rst b/docs/source/Eng/doc/cli/cli_doc.rst index ccc4a48..0968598 100644 --- a/docs/source/Eng/doc/cli/cli_doc.rst +++ b/docs/source/Eng/doc/cli/cli_doc.rst @@ -1,19 +1,74 @@ -CLI ----- +CLI Usage +========= -We can use the CLI mode to execute the keyword. -json file or execute the folder containing the Keyword.json files. +Overview +-------- -The following example is to execute the specified path of the keyword JSON file. +WebRunner can be executed directly from the command line using the ``je_web_runner`` module. -.. code-block:: +Commands +-------- - python je_web_runner --execute_file "your_file_path" +**Execute a single JSON action file:** +.. code-block:: bash + python -m je_web_runner -e actions.json + python -m je_web_runner --execute_file actions.json -The following example is to run all keyword JSON files in a specified folder: +**Execute all JSON files in a directory:** -.. code-block:: +.. code-block:: bash - python je_web_runner --execute_dir "your_dir_path" \ No newline at end of file + python -m je_web_runner -d ./actions/ + python -m je_web_runner --execute_dir ./actions/ + +**Execute a JSON action string directly:** + +.. code-block:: bash + + python -m je_web_runner --execute_str '[["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], ["WR_quit"]]' + +Command Reference +----------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 15 60 + + * - Flag + - Short + - Description + * - ``--execute_file`` + - ``-e`` + - Execute a single JSON action file + * - ``--execute_dir`` + - ``-d`` + - Execute all JSON files in a directory + * - ``--execute_str`` + - + - Execute a JSON action string directly + +JSON File Format +---------------- + +The JSON file should contain an array of action lists: + +.. code-block:: json + + [ + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_to_url", {"url": "https://example.com"}], + ["WR_quit"] + ] + +.. note:: + + On Windows, the ``--execute_str`` option may require double JSON parsing + due to shell escaping. WebRunner handles this automatically. + +Error Handling +-------------- + +If no arguments are provided, WebRunner raises a ``WebRunnerExecuteException``. +All errors are printed to stderr and the process exits with code 1. diff --git a/docs/source/Eng/doc/create_project/create_project_doc.rst b/docs/source/Eng/doc/create_project/create_project_doc.rst index 002b4f7..361834b 100644 --- a/docs/source/Eng/doc/create_project/create_project_doc.rst +++ b/docs/source/Eng/doc/create_project/create_project_doc.rst @@ -1,23 +1,80 @@ Create Project ----- +============== -In WebRunner, you can create a project which will automatically generate sample files once the project is created. -These sample files include a Python executor file and a keyword.json file. +Overview +-------- -To create a project, you can use the following method: +WebRunner can generate a quick-start project structure with sample files, +including example JSON keyword files and Python executor scripts. + +Usage +----- .. code-block:: python from je_web_runner import create_project_dir - # create on current workdir + + # Create on current working directory create_project_dir() - # create project on project_path - create_project_dir("project_path") - # create project on project_path and dir name is My First Project - create_project_dir("project_path", "My First Project") -Or using CLI, this will generate a project at the project_path location. + # Create at a specific path + create_project_dir(project_path="./my_project") + + # Create with a custom parent name + create_project_dir(project_path="./my_project", parent_name="MyTest") + +CLI Method +---------- + +.. code-block:: bash + + python -m je_web_runner --create_project ./my_project + +Generated Structure +------------------- + +.. code-block:: text + + my_project/WebRunner/ + ├── keyword/ + │ ├── keyword1.json # Sample action file (success case) + │ ├── keyword2.json # Sample action file (success case) + │ └── bad_keyword_1.json # Sample action file (failure case) + └── executor/ + ├── executor_one_file.py # Execute a single JSON file + ├── executor_folder.py # Execute all JSON files in a folder + └── executor_bad_file.py # Execute failure case file + +Template Details +---------------- + +**keyword1.json / keyword2.json**: Sample action lists that demonstrate correct usage +of WebRunner commands (launching a browser, navigating, quitting). + +**bad_keyword_1.json**: An intentionally broken action list to demonstrate error handling. + +**executor_one_file.py**: Reads and executes a single keyword JSON file using +``execute_action(read_action_json(path))``. + +**executor_folder.py**: Uses ``execute_files(get_dir_files_as_list(path))`` to +execute all ``.json`` files in the ``keyword/`` directory. + +**executor_bad_file.py**: Executes the bad keyword file to demonstrate +how errors are captured and reported. + +Parameters +---------- -.. code-block:: console +.. list-table:: + :header-rows: 1 + :widths: 25 25 50 - python -m je_web_runner --create_project project_path \ No newline at end of file + * - Parameter + - Default + - Description + * - ``project_path`` + - Current working directory + - Path where the project will be created + * - ``parent_name`` + - ``"WebRunner"`` + - Name of the top-level project directory diff --git a/docs/source/Eng/doc/exception/exception_doc.rst b/docs/source/Eng/doc/exception/exception_doc.rst new file mode 100644 index 0000000..b3ecb37 --- /dev/null +++ b/docs/source/Eng/doc/exception/exception_doc.rst @@ -0,0 +1,84 @@ +Exception Handling +================== + +Overview +-------- + +WebRunner provides a hierarchy of custom exceptions for specific error scenarios. +All exceptions inherit from ``WebRunnerException``. + +Exception Hierarchy +------------------- + +.. code-block:: text + + WebRunnerException (base) + ├── WebRunnerWebDriverNotFoundException + ├── WebRunnerOptionsWrongTypeException + ├── WebRunnerArgumentWrongTypeException + ├── WebRunnerWebDriverIsNoneException + ├── WebRunnerExecuteException + ├── WebRunnerAssertException + ├── WebRunnerHTMLException + ├── WebRunnerAddCommandException + ├── WebRunnerJsonException + │ └── WebRunnerGenerateJsonReportException + ├── XMLException + │ └── XMLTypeException + └── CallbackExecutorException + +Exception Reference +------------------- + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Exception + - Description + * - ``WebRunnerException`` + - Base exception for all WebRunner errors + * - ``WebRunnerWebDriverNotFoundException`` + - WebDriver not found or browser name not supported + * - ``WebRunnerOptionsWrongTypeException`` + - Invalid options type provided (must be list or set) + * - ``WebRunnerArgumentWrongTypeException`` + - Invalid argument type provided + * - ``WebRunnerWebDriverIsNoneException`` + - WebDriver is None (not initialized) + * - ``WebRunnerExecuteException`` + - Error during action execution (unknown command, invalid format) + * - ``WebRunnerJsonException`` + - JSON processing error + * - ``WebRunnerGenerateJsonReportException`` + - JSON report generation error + * - ``WebRunnerAssertException`` + - Assertion validation failure + * - ``WebRunnerHTMLException`` + - HTML report generation error (e.g., no test records) + * - ``WebRunnerAddCommandException`` + - Error registering custom command (not a function/method) + * - ``XMLException`` + - XML processing error + * - ``XMLTypeException`` + - Invalid XML type specified (must be ``"string"`` or ``"file"``) + * - ``CallbackExecutorException`` + - Callback execution error (bad trigger function or method) + +Example +------- + +.. code-block:: python + + from je_web_runner import get_webdriver_manager + from je_web_runner.utils.exception.exceptions import ( + WebRunnerException, + WebRunnerWebDriverNotFoundException, + ) + + try: + manager = get_webdriver_manager("unsupported_browser") + except WebRunnerWebDriverNotFoundException as e: + print(f"Browser not supported: {e}") + except WebRunnerException as e: + print(f"WebRunner error: {e}") diff --git a/docs/source/Eng/doc/generate_report/generate_report_doc.rst b/docs/source/Eng/doc/generate_report/generate_report_doc.rst index 050a411..4603ca9 100644 --- a/docs/source/Eng/doc/generate_report/generate_report_doc.rst +++ b/docs/source/Eng/doc/generate_report/generate_report_doc.rst @@ -1,25 +1,122 @@ -Generate Report ----- +Report Generation +================= -* Generate Report can generate reports in the following formats: - * HTML - * JSON - * XML +Overview +-------- -* Generate Report is mainly used to record and confirm which steps were executed and whether they were successful or not. -* The following example is used with keywords and an executor. If you don't understand, please first take a look at the executor. +WebRunner can automatically record all actions and generate reports in three formats: +**HTML**, **JSON**, and **XML**. Reports include detailed information about each executed action, +including function name, parameters, timestamp, and any exceptions. -Here's an example of generating an HTML report. +.. note:: + + Test recording must be enabled before generating reports. + See the :doc:`../test_record/test_record_doc` section for details. + +Enable Recording +---------------- .. code-block:: python + from je_web_runner import test_record_instance + + test_record_instance.set_record_enable(True) + +HTML Report +----------- -Here's an example of generating an JSON report. +HTML reports include color-coded tables: + +* **Aqua** background for successful actions +* **Red** background for failed actions + +Each row shows: function name, parameters, timestamp, and exception (if any). .. code-block:: python + from je_web_runner import generate_html, generate_html_report + + # Generate HTML string (returns complete HTML document) + html_content = generate_html() + + # Save to file (creates test_results.html) + generate_html_report("test_results") + +JSON Report +----------- + +JSON reports produce separate files for success and failure records. + +.. code-block:: python + + from je_web_runner import generate_json, generate_json_report + + # Generate dicts (returns tuple of success_dict, failure_dict) + success_dict, failure_dict = generate_json() + + # Save to files: + # - test_results_success.json + # - test_results_failure.json + generate_json_report("test_results") -Here's an example of generating an XML report. +XML Report +---------- .. code-block:: python + from je_web_runner import generate_xml, generate_xml_report + + # Generate XML structures + success_xml, failure_xml = generate_xml() + + # Save to files: + # - test_results_success.xml + # - test_results_failure.xml + generate_xml_report("test_results") + +Report via Action Executor +-------------------------- + +Reports can also be generated from action lists: + +.. code-block:: python + + from je_web_runner import execute_action + + execute_action([ + ["WR_set_record_enable", {"set_enable": True}], + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_to_url", {"url": "https://example.com"}], + ["WR_quit"], + ["WR_generate_html_report", {"html_name": "my_report"}], + ]) + +Record Data Format +------------------ + +Each record in the report contains: + +.. list-table:: + :header-rows: 1 + :widths: 25 25 50 + + * - Field + - Type + - Description + * - ``function_name`` + - ``str`` + - Name of the executed function + * - ``local_param`` + - ``dict | None`` + - Parameters passed to the function + * - ``time`` + - ``str`` + - Timestamp of execution + * - ``program_exception`` + - ``str`` + - Exception message or ``"None"`` + +Thread Safety +------------- + +All report generation methods use ``threading.Lock`` to ensure thread-safe file writing. diff --git a/docs/source/Eng/doc/installation/installation_doc.rst b/docs/source/Eng/doc/installation/installation_doc.rst index 156f263..bd44a4e 100644 --- a/docs/source/Eng/doc/installation/installation_doc.rst +++ b/docs/source/Eng/doc/installation/installation_doc.rst @@ -1,15 +1,85 @@ Installation ----- +============ -.. code-block:: python +Install via pip +--------------- + +**Stable version:** + +.. code-block:: bash pip install je_web_runner -* Python & pip require version - * Python 3.7 & up - * pip 19.3 & up +**Development version:** + +.. code-block:: bash + + pip install je_web_runner_dev + +Requirements +------------ + +* Python **3.10** or later +* pip **19.3** or later + +Dependencies (installed automatically): + +* ``selenium>=4.0.0`` +* ``requests`` +* ``python-dotenv`` +* ``webdriver-manager`` + +Supported Platforms +------------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Platform + - Notes + * - Windows 11 + - Fully supported + * - macOS + - Fully supported + * - Ubuntu / Linux + - Fully supported + * - Raspberry Pi + - Fully supported + +Supported Browsers +------------------ + +.. list-table:: + :header-rows: 1 + :widths: 30 20 50 + + * - Browser + - Key + - Notes + * - Google Chrome + - ``chrome`` + - Most commonly used, auto-managed via webdriver-manager + * - Chromium + - ``chromium`` + - Open-source Chrome variant + * - Mozilla Firefox + - ``firefox`` + - Full support via GeckoDriver + * - Microsoft Edge + - ``edge`` + - Chromium-based Edge + * - Internet Explorer + - ``ie`` + - Legacy support + * - Apple Safari + - ``safari`` + - macOS only, no auto driver management + +Verify Installation +------------------- + +.. code-block:: python -* Dev env - * windows 11 - * osx 11 big sur - * ubuntu 20.0.4 + import je_web_runner + print(je_web_runner.__all__) diff --git a/docs/source/Eng/doc/logging/logging_doc.rst b/docs/source/Eng/doc/logging/logging_doc.rst new file mode 100644 index 0000000..32ba103 --- /dev/null +++ b/docs/source/Eng/doc/logging/logging_doc.rst @@ -0,0 +1,54 @@ +Logging +======= + +Overview +-------- + +WebRunner uses Python's ``logging`` module with a rotating file handler +for logging automation events, errors, and warnings. + +Configuration +------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Property + - Value + * - Log file + - ``WEBRunner.log`` + * - Log level + - ``WARNING`` and above + * - Max file size + - 1 GB (rotating) + * - Log format + - ``%(asctime)s | %(name)s | %(levelname)s | %(message)s`` + * - Handler + - ``RotatingFileHandler`` (custom ``WebRunnerLoggingHandler``) + +Log Output +---------- + +The log file is created in the current working directory as ``WEBRunner.log``. +When the file reaches 1 GB, it is rotated. + +Example log entries: + +.. code-block:: text + + 2025-01-01 12:00:00 | je_web_runner | WARNING | WebDriverWrapper find_element failed: ... + 2025-01-01 12:00:01 | je_web_runner | ERROR | WebdriverManager quit, failed: ... + +Logger Instance +--------------- + +The global logger is accessible as ``web_runner_logger``: + +.. code-block:: python + + from je_web_runner.utils.logging.loggin_instance import web_runner_logger + + web_runner_logger.warning("Custom warning message") + +All WebRunner components use this logger internally to record their operations. diff --git a/docs/source/Eng/doc/package_manager/package_manager_doc.rst b/docs/source/Eng/doc/package_manager/package_manager_doc.rst new file mode 100644 index 0000000..397af30 --- /dev/null +++ b/docs/source/Eng/doc/package_manager/package_manager_doc.rst @@ -0,0 +1,89 @@ +Package Manager +=============== + +Overview +-------- + +The Package Manager dynamically loads external Python packages into the executor at runtime. +This allows you to extend the executor's capabilities without modifying the source code. + +When a package is added, all its public functions and classes are extracted and +registered in the executor's event dictionary with the naming convention ``{package}_{function}``. + +Usage via Action Executor +------------------------- + +.. code-block:: python + + from je_web_runner import execute_action + + actions = [ + # Load the 'time' package into executor + ["WR_add_package_to_executor", {"package": "time"}], + + # Now you can use time.sleep as "time_sleep" + ["time_sleep", [2]], + ] + + execute_action(actions) + +Direct API Usage +---------------- + +.. code-block:: python + + from je_web_runner.utils.package_manager.package_manager_class import package_manager + + # Check if a package exists and import it + module = package_manager.check_package("os") + + # Add all functions from a package to the executor + package_manager.add_package_to_executor("math") + + # Add to callback executor instead + package_manager.add_package_to_callback_executor("time") + +Methods +------- + +.. list-table:: + :header-rows: 1 + :widths: 30 30 40 + + * - Method + - Parameters + - Description + * - ``check_package()`` + - ``package: str`` + - Check and import a package, returns the module or None + * - ``add_package_to_executor()`` + - ``package: str`` + - Add package members to the Executor's event_dict + * - ``add_package_to_callback_executor()`` + - ``package: str`` + - Add package members to the CallbackExecutor's event_dict + * - ``get_member()`` + - ``package, predicate, target`` + - Extract and add specific members from a package + * - ``add_package_to_target()`` + - ``package: str, target`` + - Add functions/classes to a target executor + +Naming Convention +----------------- + +When a package is added, functions are registered with the prefix ``{package_name}_``. + +For example, adding the ``time`` package registers: + +* ``time_sleep`` -> ``time.sleep`` +* ``time_time`` -> ``time.time`` +* ``time_monotonic`` -> ``time.monotonic`` +* etc. + +Package Caching +--------------- + +The ``installed_package_dict`` attribute caches imported packages to avoid +re-importing them. If a package has already been loaded, subsequent calls +to ``check_package()`` return the cached module. diff --git a/docs/source/Eng/doc/quick_start/quick_start_doc.rst b/docs/source/Eng/doc/quick_start/quick_start_doc.rst new file mode 100644 index 0000000..49fc883 --- /dev/null +++ b/docs/source/Eng/doc/quick_start/quick_start_doc.rst @@ -0,0 +1,101 @@ +Quick Start +=========== + +This guide demonstrates the three main ways to use WebRunner. + +Example 1: Direct Python API +----------------------------- + +Use WebRunner's classes and functions directly in Python code. + +.. code-block:: python + + from je_web_runner import TestObject + from je_web_runner import get_webdriver_manager + from je_web_runner import web_element_wrapper + + # Create a WebDriver manager (using Chrome) + manager = get_webdriver_manager("chrome") + + # Navigate to a URL + manager.webdriver_wrapper.to_url("https://www.google.com") + + # Set implicit wait + manager.webdriver_wrapper.implicitly_wait(2) + + # Create a test object to locate the search box by name + search_box = TestObject("q", "name") + + # Find the element + manager.webdriver_wrapper.find_element(search_box) + + # Click and type into the element + web_element_wrapper.click_element() + web_element_wrapper.input_to_element("WebRunner automation") + + # Close the browser + manager.quit() + +Example 2: JSON Action List +----------------------------- + +Define automation scripts as JSON action lists and execute them through the Action Executor. + +.. code-block:: python + + from je_web_runner import execute_action + + actions = [ + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_to_url", {"url": "https://www.google.com"}], + ["WR_implicitly_wait", {"time_to_wait": 2}], + ["WR_SaveTestObject", {"test_object_name": "q", "object_type": "name"}], + ["WR_find_element", {"element_name": "q"}], + ["WR_click_element"], + ["WR_input_to_element", {"input_value": "WebRunner automation"}], + ["WR_quit"] + ] + + result = execute_action(actions) + +Example 3: Execute from JSON File +----------------------------------- + +Create an ``actions.json`` file: + +.. code-block:: json + + [ + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_to_url", {"url": "https://example.com"}], + ["WR_quit"] + ] + +Execute it in Python: + +.. code-block:: python + + from je_web_runner import execute_files + + results = execute_files(["actions.json"]) + +Or via CLI: + +.. code-block:: bash + + python -m je_web_runner -e actions.json + +Example 4: Headless Mode +-------------------------- + +Run browsers without a visible GUI window using headless mode. + +.. code-block:: python + + from je_web_runner import get_webdriver_manager + + manager = get_webdriver_manager("chrome", options=["--headless", "--disable-gpu"]) + manager.webdriver_wrapper.to_url("https://example.com") + title = manager.webdriver_wrapper.execute_script("return document.title") + print(f"Page title: {title}") + manager.quit() diff --git a/docs/source/Eng/doc/scheduler/scheduler_doc.rst b/docs/source/Eng/doc/scheduler/scheduler_doc.rst deleted file mode 100644 index 13e3939..0000000 --- a/docs/source/Eng/doc/scheduler/scheduler_doc.rst +++ /dev/null @@ -1,19 +0,0 @@ -Scheduler ----- - -You can use scheduling to perform repetitive tasks, either by using a simple wrapper for APScheduler or by consulting the API documentation to use it yourself. - -.. code-block:: python - - from je_web_runner import SchedulerManager - - - def test_scheduler(): - print("Test Scheduler") - scheduler.remove_blocking_job(id="test") - scheduler.shutdown_blocking_scheduler() - - - scheduler = SchedulerManager() - scheduler.add_interval_blocking_secondly(function=test_scheduler, id="test") - scheduler.start_block_scheduler() diff --git a/docs/source/Eng/doc/socket_driver/socket_driver_doc.rst b/docs/source/Eng/doc/socket_driver/socket_driver_doc.rst index 29ec49f..7ee10ab 100644 --- a/docs/source/Eng/doc/socket_driver/socket_driver_doc.rst +++ b/docs/source/Eng/doc/socket_driver/socket_driver_doc.rst @@ -1,25 +1,96 @@ -Socket Driver ----- - -* This is an experimental feature. -* The Socket Server is mainly used to allow other programming languages to use AutoControl. -* It processes received strings and performs actions through the underlying executor. -* Tests can be performed remotely through this feature. -* Currently, Java and C# support are experimental. -* Return_Data_Over_JE should be transmitted at the end of each paragraph. -* UTF-8 encoding is used. -* Sending quit_server will shut down the server. +Remote Automation (Socket Server) +================================= + +Overview +-------- + +WebRunner includes a multi-threaded TCP socket server for remote automation control. +This enables cross-language support -- any language that supports TCP sockets +(Java, C#, Go, etc.) can send automation commands to WebRunner. + +Starting the Server +------------------- + +.. code-block:: python + + from je_web_runner import start_web_runner_socket_server + + server = start_web_runner_socket_server(host="localhost", port=9941) + +The server starts in a background daemon thread and is ready to accept connections immediately. + +You can also override the host and port via command-line arguments: + +.. code-block:: bash + + python your_script.py localhost 9941 + +Client Connection Example +------------------------- .. code-block:: python - import sys + import socket + import json + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(("localhost", 9941)) + + # Send actions as JSON (UTF-8 encoded) + actions = [ + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_to_url", {"url": "https://example.com"}], + ["WR_quit"] + ] + sock.send(json.dumps(actions).encode("utf-8")) + + # Receive results (ends with "Return_Data_Over_JE\n") + response = sock.recv(4096).decode("utf-8") + print(response) + + # Shutdown server + sock.send("quit_server".encode("utf-8")) + +Protocol +-------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Property + - Value + * - Default host + - ``localhost`` + * - Default port + - ``9941`` + * - Encoding + - UTF-8 + * - Message format + - JSON array of actions + * - Max receive buffer + - 8192 bytes + * - Response terminator + - ``Return_Data_Over_JE`` + * - Shutdown command + - ``quit_server`` + * - Threading model + - Multi-threaded (``socketserver.ThreadingMixIn``) + +Response Format +--------------- + +After executing actions, the server sends results back one by one. +Each result is followed by a newline (``\n``). +The final message is always ``Return_Data_Over_JE\n``. + +If an error occurs, the error message is sent to the client followed by the same terminator. + +Server Classes +-------------- - from je_web_runner import start_autocontrol_socket_server +**TCPServerHandler** (extends ``socketserver.BaseRequestHandler``): +Handles incoming requests, parses JSON, executes actions, and returns results. - try: - server = start_autocontrol_socket_server() - while not server.close_flag: - pass - sys.exit(0) - except Exception as error: - print(repr(error)) \ No newline at end of file +**TCPServer** (extends ``socketserver.ThreadingMixIn, socketserver.TCPServer``): +Multi-threaded TCP server with a ``close_flag`` attribute for shutdown control. diff --git a/docs/source/Eng/doc/test_object/test_object_doc.rst b/docs/source/Eng/doc/test_object/test_object_doc.rst new file mode 100644 index 0000000..540954c --- /dev/null +++ b/docs/source/Eng/doc/test_object/test_object_doc.rst @@ -0,0 +1,90 @@ +Test Object +=========== + +Overview +-------- + +``TestObject`` encapsulates element locator information (strategy + value) for reusable element definitions. +``TestObjectRecord`` stores ``TestObject`` instances by name for later retrieval by the ``_with_test_object`` methods. + +Creating Test Objects +--------------------- + +.. code-block:: python + + from je_web_runner import TestObject, create_test_object, get_test_object_type_list + + # Constructor: TestObject(test_object_name, test_object_type) + obj1 = TestObject("search", "name") + + # Factory function: create_test_object(object_type, test_object_name) + obj2 = create_test_object("id", "submit-btn") + +Available Locator Types +----------------------- + +.. code-block:: python + + print(get_test_object_type_list()) + # ['ID', 'NAME', 'XPATH', 'CSS_SELECTOR', 'CLASS_NAME', + # 'TAG_NAME', 'LINK_TEXT', 'PARTIAL_LINK_TEXT'] + +These map directly to Selenium's ``By`` class constants. + +TestObject Attributes +--------------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 30 40 + + * - Attribute + - Type + - Description + * - ``test_object_type`` + - ``str`` + - Locator strategy (e.g., ``"name"``, ``"xpath"``) + * - ``test_object_name`` + - ``str`` + - Locator value (e.g., ``"search"``, ``"//div[@id='main']"``) + +TestObjectRecord +---------------- + +``TestObjectRecord`` stores ``TestObject`` instances by name. This is used by the Action Executor +to reference elements by string names (e.g., ``WR_SaveTestObject`` and ``WR_find_element``). + +.. code-block:: python + + from je_web_runner.utils.test_object.test_object_record.test_object_record_class import test_object_record + + # Save a test object + test_object_record.save_test_object("search_box", "name") + + # Remove a test object + test_object_record.remove_test_object("search_box") + + # Clear all records + test_object_record.clean_record() + +Usage in Action Executor +------------------------ + +.. code-block:: python + + from je_web_runner import execute_action + + execute_action([ + # Save a test object with name "search" and locator type "name" + ["WR_SaveTestObject", {"test_object_name": "search", "object_type": "name"}], + + # Find the element by its saved name + ["WR_find_element", {"element_name": "search"}], + + # Interact with the found element + ["WR_click_element"], + ["WR_input_to_element", {"input_value": "hello"}], + + # Clean all saved test objects + ["WR_CleanTestObject"], + ]) diff --git a/docs/source/Eng/doc/test_record/test_record_doc.rst b/docs/source/Eng/doc/test_record/test_record_doc.rst new file mode 100644 index 0000000..2890797 --- /dev/null +++ b/docs/source/Eng/doc/test_record/test_record_doc.rst @@ -0,0 +1,107 @@ +Test Record +=========== + +Overview +-------- + +All WebRunner actions can be automatically recorded for audit trails and report generation. +The global instance ``test_record_instance`` manages recording state and stores records. + +Enable Recording +---------------- + +Recording is disabled by default. Enable it before running actions: + +.. code-block:: python + + from je_web_runner import test_record_instance + + test_record_instance.set_record_enable(True) + +Or via the action executor: + +.. code-block:: python + + from je_web_runner import execute_action + + execute_action([ + ["WR_set_record_enable", {"set_enable": True}], + # ... your actions ... + ]) + +Accessing Records +----------------- + +.. code-block:: python + + from je_web_runner import test_record_instance + + test_record_instance.set_record_enable(True) + + # ... perform automation ... + + # Access records + records = test_record_instance.test_record_list + + for record in records: + print(record) + +Record Format +------------- + +Each record is a dictionary with the following fields: + +.. list-table:: + :header-rows: 1 + :widths: 25 25 50 + + * - Field + - Type + - Description + * - ``function_name`` + - ``str`` + - Name of the executed function + * - ``local_param`` + - ``dict | None`` + - Parameters passed to the function + * - ``time`` + - ``str`` + - Timestamp of execution (e.g., ``"2025-01-01 12:00:00"``) + * - ``program_exception`` + - ``str`` + - Exception message or ``"None"`` if successful + +Example record: + +.. code-block:: python + + { + "function_name": "to_url", + "local_param": {"url": "https://example.com"}, + "time": "2025-01-01 12:00:00", + "program_exception": "None" + } + +Clearing Records +---------------- + +.. code-block:: python + + test_record_instance.clean_record() + +Attributes +---------- + +.. list-table:: + :header-rows: 1 + :widths: 25 25 50 + + * - Attribute + - Type + - Description + * - ``test_record_list`` + - ``list`` + - List of all recorded actions + * - ``init_record`` + - ``bool`` + - Whether recording is currently enabled diff --git a/docs/source/Eng/doc/web_element/web_element_doc.rst b/docs/source/Eng/doc/web_element/web_element_doc.rst new file mode 100644 index 0000000..6ab833e --- /dev/null +++ b/docs/source/Eng/doc/web_element/web_element_doc.rst @@ -0,0 +1,162 @@ +Web Element Wrapper +=================== + +Overview +-------- + +``WebElementWrapper`` provides methods for interacting with located elements. +It operates on the currently active element set by ``find_element()`` or ``find_elements()``. + +The global instance ``web_element_wrapper`` is imported from ``je_web_runner``. + +Basic Interactions +------------------ + +.. code-block:: python + + from je_web_runner import web_element_wrapper + + web_element_wrapper.click_element() # Click element + web_element_wrapper.input_to_element("Hello World") # Type text + web_element_wrapper.clear() # Clear content + web_element_wrapper.submit() # Submit form + +Attribute and Property Inspection +--------------------------------- + +.. code-block:: python + + web_element_wrapper.get_attribute("href") # Get HTML attribute + web_element_wrapper.get_property("checked") # Get JS property + web_element_wrapper.get_dom_attribute("data-id") # Get DOM attribute + +State Checks +------------ + +.. code-block:: python + + web_element_wrapper.is_displayed() # Check visibility + web_element_wrapper.is_enabled() # Check if enabled + web_element_wrapper.is_selected() # Check if selected (checkbox/radio) + +CSS Property +------------ + +.. code-block:: python + + web_element_wrapper.value_of_css_property("color") + +Dropdown (Select) Handling +-------------------------- + +.. code-block:: python + + select = web_element_wrapper.get_select() + # Now use Selenium's Select API: + # select.select_by_visible_text("Option 1") + # select.select_by_value("opt1") + # select.select_by_index(0) + +Element Screenshot +------------------ + +.. code-block:: python + + web_element_wrapper.screenshot("element") # Saves as element.png + +Switching Elements +------------------ + +When ``find_elements()`` returns multiple elements, use ``change_web_element()`` +to switch the active element: + +.. code-block:: python + + # After find_elements, switch to the 3rd element (index 2) + web_element_wrapper.change_web_element(2) + web_element_wrapper.click_element() + +Element Validation +------------------ + +Validate element properties against expected values: + +.. code-block:: python + + web_element_wrapper.check_current_web_element({ + "tag_name": "input", + "enabled": True + }) + +Key Attributes +-------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 30 40 + + * - Attribute + - Type + - Description + * - ``current_web_element`` + - ``WebElement | None`` + - Currently active element + * - ``current_web_element_list`` + - ``List[WebElement] | None`` + - List of found elements (from ``find_elements``) + +Methods Reference +----------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 35 40 + + * - Method + - Parameters + - Description + * - ``click_element()`` + - + - Click the current element + * - ``input_to_element()`` + - ``input_value: str`` + - Type text into the element + * - ``clear()`` + - + - Clear element content + * - ``submit()`` + - + - Submit the form + * - ``get_attribute()`` + - ``name: str`` + - Get HTML attribute value + * - ``get_property()`` + - ``name: str`` + - Get JavaScript property value + * - ``get_dom_attribute()`` + - ``name: str`` + - Get DOM attribute value + * - ``is_displayed()`` + - + - Check if element is visible + * - ``is_enabled()`` + - + - Check if element is enabled + * - ``is_selected()`` + - + - Check if element is selected + * - ``value_of_css_property()`` + - ``property_name: str`` + - Get CSS property value + * - ``screenshot()`` + - ``filename: str`` + - Take element screenshot + * - ``change_web_element()`` + - ``element_index: int`` + - Switch active element by index + * - ``check_current_web_element()`` + - ``check_dict: dict`` + - Validate element properties + * - ``get_select()`` + - + - Get Select object for dropdown diff --git a/docs/source/Eng/doc/webdriver_manager/webdriver_manager_doc.rst b/docs/source/Eng/doc/webdriver_manager/webdriver_manager_doc.rst new file mode 100644 index 0000000..c8abfcf --- /dev/null +++ b/docs/source/Eng/doc/webdriver_manager/webdriver_manager_doc.rst @@ -0,0 +1,102 @@ +WebDriver Manager +================= + +Overview +-------- + +``WebdriverManager`` manages multiple WebDriver instances for parallel browser automation. +It maintains a list of active WebDriver instances and provides methods to create, switch between, and close them. + +The global instance ``web_runner`` is used internally. Access it via the factory function ``get_webdriver_manager()``. + +Creating a Manager +------------------ + +.. code-block:: python + + from je_web_runner import get_webdriver_manager + + # Create with Chrome + manager = get_webdriver_manager("chrome") + + # Create with Firefox and options + manager = get_webdriver_manager("firefox", options=["--headless"]) + +The factory function creates a new WebDriver and returns the global ``WebdriverManager`` instance. + +Managing Multiple Browsers +-------------------------- + +.. code-block:: python + + manager = get_webdriver_manager("chrome") + + # Add a second browser instance + manager.new_driver("firefox") + + # Switch to Chrome (index 0) + manager.change_webdriver(0) + manager.webdriver_wrapper.to_url("https://example.com") + + # Switch to Firefox (index 1) + manager.change_webdriver(1) + manager.webdriver_wrapper.to_url("https://google.com") + + # Close Firefox only + manager.close_choose_webdriver(1) + + # Close the current browser + manager.close_current_webdriver() + + # Close and quit ALL browsers + manager.quit() + +Key Attributes +-------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 30 40 + + * - Attribute + - Type + - Description + * - ``webdriver_wrapper`` + - ``WebDriverWrapper`` + - Wrapper for WebDriver operations + * - ``webdriver_element`` + - ``WebElementWrapper`` + - Wrapper for element operations + * - ``current_webdriver`` + - ``WebDriver | None`` + - Currently active WebDriver instance + +Methods +------- + +.. list-table:: + :header-rows: 1 + :widths: 30 40 30 + + * - Method + - Parameters + - Description + * - ``new_driver()`` + - ``webdriver_name: str, options: List[str] = None, **kwargs`` + - Create a new WebDriver instance + * - ``change_webdriver()`` + - ``index_of_webdriver: int`` + - Switch to a WebDriver by index + * - ``close_current_webdriver()`` + - + - Close the current WebDriver + * - ``close_choose_webdriver()`` + - ``webdriver_index: int`` + - Close a WebDriver by index + * - ``quit()`` + - + - Close and quit all WebDrivers + +.. note:: + + When ``quit()`` is called, it also cleans up all saved ``TestObjectRecord`` entries. diff --git a/docs/source/Eng/doc/webdriver_options/webdriver_options_doc.rst b/docs/source/Eng/doc/webdriver_options/webdriver_options_doc.rst new file mode 100644 index 0000000..7472391 --- /dev/null +++ b/docs/source/Eng/doc/webdriver_options/webdriver_options_doc.rst @@ -0,0 +1,93 @@ +WebDriver Options Configuration +================================ + +Overview +-------- + +Configure browser options and capabilities before launching a WebDriver instance. +This is useful for headless mode, disabling GPU, setting window size, etc. + +Browser Arguments +----------------- + +.. code-block:: python + + from je_web_runner import set_webdriver_options_argument, get_webdriver_manager + + # Set browser arguments (returns Options object) + options = set_webdriver_options_argument("chrome", [ + "--headless", + "--disable-gpu", + "--no-sandbox", + "--window-size=1920,1080" + ]) + + # Or pass options directly when creating the manager + manager = get_webdriver_manager("chrome", options=["--headless", "--disable-gpu"]) + +Common Chrome Arguments +~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Argument + - Description + * - ``--headless`` + - Run without visible GUI + * - ``--disable-gpu`` + - Disable GPU hardware acceleration + * - ``--no-sandbox`` + - Disable sandbox (required in some Linux environments) + * - ``--window-size=W,H`` + - Set initial window size + * - ``--incognito`` + - Open in incognito mode + * - ``--disable-extensions`` + - Disable browser extensions + * - ``--start-maximized`` + - Start with maximized window + +DesiredCapabilities +------------------- + +.. code-block:: python + + from je_web_runner import get_desired_capabilities, get_desired_capabilities_keys + + # View available capability keys (browser names) + keys = get_desired_capabilities_keys() + # dict_keys(['CHROME', 'FIREFOX', 'EDGE', ...]) + + # Get capabilities for a specific browser + caps = get_desired_capabilities("CHROME") + +Setting Capabilities via Wrapper +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from je_web_runner.webdriver.webdriver_with_options import set_webdriver_options_capability_wrapper + + options = set_webdriver_options_capability_wrapper("chrome", { + "acceptInsecureCerts": True + }) + +Functions Reference +------------------- + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Function + - Description + * - ``set_webdriver_options_argument(webdriver_name, argument_iterable)`` + - Set browser startup arguments; returns ``Options`` object + * - ``set_webdriver_options_capability_wrapper(webdriver_name, key_and_vale_dict)`` + - Set browser capabilities; returns ``Options`` object + * - ``get_desired_capabilities_keys()`` + - Get available browser names + * - ``get_desired_capabilities(webdriver_name)`` + - Get DesiredCapabilities dict for a browser diff --git a/docs/source/Eng/doc/webdriver_wrapper/webdriver_wrapper_doc.rst b/docs/source/Eng/doc/webdriver_wrapper/webdriver_wrapper_doc.rst new file mode 100644 index 0000000..fa2c61b --- /dev/null +++ b/docs/source/Eng/doc/webdriver_wrapper/webdriver_wrapper_doc.rst @@ -0,0 +1,198 @@ +WebDriver Wrapper +================= + +Overview +-------- + +``WebDriverWrapper`` is the central component that wraps Selenium WebDriver with comprehensive methods. +It provides a unified interface for browser control, element finding, mouse/keyboard actions, +cookie management, JavaScript execution, window management, and more. + +The global instance ``webdriver_wrapper_instance`` is used internally by the manager and executor. + +Navigation +---------- + +.. code-block:: python + + wrapper = manager.webdriver_wrapper + + wrapper.to_url("https://example.com") # Navigate to URL + wrapper.forward() # Go forward + wrapper.back() # Go back + wrapper.refresh() # Refresh the page + +Element Finding +--------------- + +Find elements using ``TestObject`` locators: + +.. code-block:: python + + from je_web_runner import TestObject + + # Locator strategies: id, name, xpath, css selector, + # class name, tag name, link text, partial link text + element = TestObject("search-input", "id") + + wrapper.find_element(element) # Find single element + wrapper.find_elements(element) # Find multiple elements + +Find elements using saved ``TestObjectRecord`` names: + +.. code-block:: python + + # Using saved test objects (stored by name in TestObjectRecord) + wrapper.find_element_with_test_object_record("search-input") + wrapper.find_elements_with_test_object_record("search-input") + +Wait Methods +------------ + +.. code-block:: python + + wrapper.implicitly_wait(5) # Implicit wait (seconds) + wrapper.explict_wait( # Explicit WebDriverWait + wait_condition, + timeout=10, + poll_frequency=0.5 + ) + wrapper.set_script_timeout(30) # Async script timeout + wrapper.set_page_load_timeout(60) # Page load timeout + +Mouse Actions +------------- + +**Basic clicks (at current position):** + +.. code-block:: python + + wrapper.left_click() + wrapper.right_click() + wrapper.left_double_click() + wrapper.left_click_and_hold() + wrapper.release() + +**Clicks on saved test objects:** + +.. code-block:: python + + wrapper.left_click_with_test_object("button_name") + wrapper.right_click_with_test_object("button_name") + wrapper.left_double_click_with_test_object("button_name") + wrapper.left_click_and_hold_with_test_object("button_name") + wrapper.release_with_test_object("button_name") + +**Drag and drop:** + +.. code-block:: python + + wrapper.drag_and_drop(source_element, target_element) + wrapper.drag_and_drop_offset(element, x=100, y=50) + + # Using saved test objects + wrapper.drag_and_drop_with_test_object("source_name", "target_name") + wrapper.drag_and_drop_offset_with_test_object("element_name", offset_x=100, offset_y=50) + +**Mouse movement:** + +.. code-block:: python + + wrapper.move_to_element(element) # Hover over element + wrapper.move_to_element_with_test_object("element_name") + wrapper.move_to_element_with_offset(element, offset_x=10, offset_y=10) + wrapper.move_to_element_with_offset_and_test_object("name", offset_x=10, offset_y=10) + wrapper.move_by_offset(100, 200) # Move from current position + +Keyboard Actions +---------------- + +.. code-block:: python + + wrapper.press_key(keycode) + wrapper.press_key_with_test_object(keycode) + wrapper.release_key(keycode) + wrapper.release_key_with_test_object(keycode) + wrapper.send_keys("text") + wrapper.send_keys_to_element(element, "text") + wrapper.send_keys_to_element_with_test_object("element_name", "text") + +Action Chain Control +-------------------- + +.. code-block:: python + + wrapper.perform() # Execute queued actions + wrapper.reset_actions() # Clear action queue + wrapper.pause(2) # Pause in action chain (seconds) + wrapper.scroll(0, 500) # Scroll page by offset + +Cookie Management +----------------- + +.. code-block:: python + + wrapper.get_cookies() # Get all cookies + wrapper.get_cookie("session_id") # Get specific cookie + wrapper.add_cookie({"name": "key", "value": "val"}) + wrapper.delete_cookie("session_id") + wrapper.delete_all_cookies() + +JavaScript Execution +-------------------- + +.. code-block:: python + + wrapper.execute("document.title") + wrapper.execute_script("return document.title") + wrapper.execute_async_script("arguments[0]('done')", callback) + +Window Management +----------------- + +.. code-block:: python + + wrapper.maximize_window() + wrapper.minimize_window() + wrapper.fullscreen_window() + wrapper.set_window_size(1920, 1080) + wrapper.set_window_position(0, 0) + wrapper.get_window_position() # Returns dict + wrapper.get_window_rect() # Returns dict + wrapper.set_window_rect(x=0, y=0, width=1920, height=1080) + +Screenshots +----------- + +.. code-block:: python + + wrapper.get_screenshot_as_png() # Returns bytes + wrapper.get_screenshot_as_base64() # Returns base64 string + +Frame / Window / Alert Switching +-------------------------------- + +The ``switch()`` method supports the following context types: + +.. code-block:: python + + wrapper.switch("frame", "frame_name") + wrapper.switch("window", "window_handle") + wrapper.switch("default_content") + wrapper.switch("parent_frame") + wrapper.switch("active_element") + wrapper.switch("alert") + +Browser Logs +------------ + +.. code-block:: python + + wrapper.get_log("browser") + +WebDriver Validation +-------------------- + +.. code-block:: python + + wrapper.check_current_webdriver({"name": "chrome"}) diff --git a/docs/source/Eng/eng_index.rst b/docs/source/Eng/eng_index.rst index b737ef5..1fa7c80 100644 --- a/docs/source/Eng/eng_index.rst +++ b/docs/source/Eng/eng_index.rst @@ -6,9 +6,20 @@ WebRunner English Documentation :maxdepth: 4 doc/installation/installation_doc.rst + doc/quick_start/quick_start_doc.rst + doc/webdriver_manager/webdriver_manager_doc.rst + doc/webdriver_wrapper/webdriver_wrapper_doc.rst + doc/web_element/web_element_doc.rst + doc/test_object/test_object_doc.rst + doc/action_executor/action_executor_doc.rst doc/create_project/create_project_doc.rst doc/generate_report/generate_report_doc.rst doc/callback_function/callback_function_doc.rst doc/cli/cli_doc.rst - doc/scheduler/scheduler_doc.rst - doc/socket_driver/socket_driver_doc.rst \ No newline at end of file + doc/socket_driver/socket_driver_doc.rst + doc/package_manager/package_manager_doc.rst + doc/webdriver_options/webdriver_options_doc.rst + doc/assertion/assertion_doc.rst + doc/test_record/test_record_doc.rst + doc/exception/exception_doc.rst + doc/logging/logging_doc.rst diff --git a/docs/source/Zh/doc/action_executor/action_executor_doc.rst b/docs/source/Zh/doc/action_executor/action_executor_doc.rst new file mode 100644 index 0000000..a047c26 --- /dev/null +++ b/docs/source/Zh/doc/action_executor/action_executor_doc.rst @@ -0,0 +1,96 @@ +動作執行器 +========== + +概述 +---- + +動作執行器是一個強大的引擎,將指令字串對應到可呼叫的函式。 +它允許您將自動化腳本定義為 JSON 動作列表,實現資料驅動的自動化工作流程。 + +執行器還包含所有 Python 內建函式,因此可以在動作列表中呼叫 ``print``、``len`` 等。 + +動作格式 +-------- + +每個動作是一個包含指令名稱和可選參數的列表: + +.. code-block:: python + + ["command_name"] # 無參數 + ["command_name", {"key": "value"}] # 關鍵字參數 + ["command_name", [arg1, arg2]] # 位置參數 + +基本用法 +-------- + +.. code-block:: python + + from je_web_runner import execute_action + + actions = [ + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_to_url", {"url": "https://www.google.com"}], + ["WR_implicitly_wait", {"time_to_wait": 2}], + ["WR_SaveTestObject", {"test_object_name": "q", "object_type": "name"}], + ["WR_find_element", {"element_name": "q"}], + ["WR_click_element"], + ["WR_input_to_element", {"input_value": "WebRunner"}], + ["WR_quit"] + ] + + result = execute_action(actions) + +``execute_action()`` 回傳一個字典,將每個動作對應到其回傳值。 + +可用指令 +-------- + +完整的指令列表請參閱英文文件的 Action Executor 頁面。 + +主要指令分類: + +* **WebDriver 管理**:``WR_get_webdriver_manager``, ``WR_change_index_of_webdriver``, ``WR_quit`` +* **導航**:``WR_to_url``, ``WR_forward``, ``WR_back``, ``WR_refresh`` +* **元素尋找**:``WR_find_element``, ``WR_find_elements`` +* **等待**:``WR_implicitly_wait``, ``WR_explict_wait``, ``WR_set_script_timeout``, ``WR_set_page_load_timeout`` +* **滑鼠**:``WR_left_click``, ``WR_right_click``, ``WR_left_double_click``, ``WR_drag_and_drop`` +* **鍵盤**:``WR_press_key``, ``WR_release_key``, ``WR_send_keys``, ``WR_send_keys_to_element`` +* **Cookie**:``WR_get_cookies``, ``WR_add_cookie``, ``WR_delete_cookie``, ``WR_delete_all_cookies`` +* **JavaScript**:``WR_execute``, ``WR_execute_script``, ``WR_execute_async_script`` +* **視窗**:``WR_maximize_window``, ``WR_minimize_window``, ``WR_set_window_size`` +* **截圖**:``WR_get_screenshot_as_png``, ``WR_get_screenshot_as_base64`` +* **元素操作**:``WR_click_element``, ``WR_input_to_element``, ``WR_element_clear``, ``WR_element_submit`` +* **測試物件**:``WR_SaveTestObject``, ``WR_CleanTestObject`` +* **報告**:``WR_generate_html_report``, ``WR_generate_json_report``, ``WR_generate_xml_report`` +* **套件**:``WR_add_package_to_executor``, ``WR_add_package_to_callback_executor`` + +從 JSON 檔案執行 +----------------- + +.. code-block:: python + + from je_web_runner import execute_files + + results = execute_files(["actions1.json", "actions2.json"]) + +新增自訂指令 +------------ + +.. code-block:: python + + from je_web_runner import add_command_to_executor + + def my_custom_function(param1, param2): + print(f"自訂: {param1}, {param2}") + + add_command_to_executor({"my_command": my_custom_function}) + + # 在動作列表中使用 + execute_action([ + ["my_command", {"param1": "hello", "param2": "world"}] + ]) + +.. note:: + + 僅接受 ``types.MethodType`` 和 ``types.FunctionType``。 + 傳入其他可呼叫類型會引發 ``WebRunnerAddCommandException``。 diff --git a/docs/source/Zh/doc/assertion/assertion_doc.rst b/docs/source/Zh/doc/assertion/assertion_doc.rst new file mode 100644 index 0000000..fb902e1 --- /dev/null +++ b/docs/source/Zh/doc/assertion/assertion_doc.rst @@ -0,0 +1,61 @@ +斷言與驗證 +========== + +概述 +---- + +WebRunner 提供斷言工具,用於在自動化過程中驗證 WebDriver 和 WebElement 的狀態。 +驗證失敗時會引發 ``WebRunnerAssertException``。 + +WebDriver 驗證 +--------------- + +.. code-block:: python + + # 透過 WebDriverWrapper + wrapper.check_current_webdriver({"name": "chrome"}) + +.. code-block:: python + + # 透過工具函式 + from je_web_runner.utils.assert_value.result_check import ( + check_webdriver_value, + check_webdriver_values, + ) + + check_webdriver_value("name", "chrome", webdriver_instance) + check_webdriver_values({"name": "chrome"}, webdriver_instance) + +WebElement 驗證 +---------------- + +.. code-block:: python + + # 透過 WebElementWrapper + web_element_wrapper.check_current_web_element({ + "tag_name": "input", + "enabled": True + }) + +.. code-block:: python + + # 透過工具函式 + from je_web_runner.utils.assert_value.result_check import check_web_element_details + + check_web_element_details(element, { + "tag_name": "input", + "enabled": True + }) + +透過 Action Executor 使用 +-------------------------- + +.. code-block:: python + + from je_web_runner import execute_action + + execute_action([ + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_check_current_webdriver", {"check_dict": {"name": "chrome"}}], + ["WR_quit"], + ]) diff --git a/docs/source/Zh/doc/callback_function/callback_function_doc.rst b/docs/source/Zh/doc/callback_function/callback_function_doc.rst index e6fb018..4962e4e 100644 --- a/docs/source/Zh/doc/callback_function/callback_function_doc.rst +++ b/docs/source/Zh/doc/callback_function/callback_function_doc.rst @@ -1,51 +1,83 @@ -回調函數 +回調執行器 +========== + +概述 ---- -在 AutoControl 裡,Callback function 是由 Callback Executor 提供支援, -以下是簡易的使用 Callback Executor 的範例, +回調執行器允許您執行自動化指令,並在完成後觸發回調函式。 +它包裝了標準執行器的事件字典,提供事件驅動的執行模型。 + +全域實例 ``callback_executor`` 從 ``je_web_runner`` 匯入。 + +基本用法 +-------- .. code-block:: python from je_web_runner import callback_executor - # trigger_function will first to execute, but return value need to wait everything done - # so this test will first print("test") then print(size_function_return_value) + + def on_complete(): + print("導航完成!") + callback_executor.callback_function( - trigger_function_name="WR_get_webdriver_manager", - callback_function=print, - callback_param_method="args", - callback_function_param={"": "open driver"}, - **{ - "webdriver_name": "edge" - } + trigger_function_name="WR_to_url", + callback_function=on_complete, + url="https://example.com" ) -* ( 注意!,如果 callback_executor event_dict 裡面包含的 name: function 需與 executor 一樣,不一樣則是 Bug) -* (當然跟 executor 一樣可以藉由添加外部 function 來擴充,請看下面例子) +使用 kwargs 的回調 +------------------- -在這個範例裡,我們使用 callback_executor 執行定義在 AutoControl 的 size function, -然後執行完 size function 後,會去執行傳遞給 callback_function 的 function, -可以由 callback_param_method 參數來決定要使用的傳遞方法, -如果是 "args" 請傳入 {"value1", "value2", ...} 這裡 ... 代表可以複數傳入, -如果是 "kwargs" 請傳入 {"actually_param_name": value, ...} 這裡 ... 代表可以複數傳入, -然後如果要使用回傳值的話,由於回傳值會在所有 function 執行完後才回傳, -實際上 size -> print 順序沒錯,但此例會先看到 print 之後才是 print(size_function_return_value), -因為 size function 只有回傳值本身沒有 print 的動作。 +.. code-block:: python -如果我們想要在 callback_executor 裡面添加 function,可以使用如下: -這段程式碼會把所有 time module 的 builtin, function, method, class -載入到 callback executor,然後要使用被載入的 function 需要使用 package_function 名稱, -例如 time.sleep 會變成 time_sleep + def on_element_found(result=None): + print(f"找到元素: {result}") -這段程式碼會把 time 所有的 function 加入 executor(直譯器裡)。 + callback_executor.callback_function( + trigger_function_name="WR_find_element", + callback_function=on_element_found, + callback_function_param={"result": "search_box"}, + callback_param_method="kwargs", + element_name="search_box" + ) + +使用 args 的回調 +----------------- .. code-block:: python - from je_web_runner import package_manager - package_manager.add_package_to_callback_executor("time") + def on_done(msg): + print(f"完成: {msg}") -如果你需要查看被更新的 event_dict 可以使用 + callback_executor.callback_function( + trigger_function_name="WR_quit", + callback_function=on_done, + callback_function_param=["所有瀏覽器已關閉"], + callback_param_method="args" + ) -.. code-block:: python +參數 +---- - from je_web_runner import callback_executor - print(callback_executor.event_dict) \ No newline at end of file +.. list-table:: + :header-rows: 1 + :widths: 25 20 55 + + * - 參數 + - 型別 + - 說明 + * - ``trigger_function_name`` + - ``str`` + - 要觸發的函式名稱(必須存在於 ``event_dict``) + * - ``callback_function`` + - ``Callable`` + - 觸發後要執行的回調函式 + * - ``callback_function_param`` + - ``dict | list | None`` + - 傳遞給回調的參數 + * - ``callback_param_method`` + - ``str`` + - 回調參數傳遞方式:``"kwargs"`` 或 ``"args"`` + * - ``**kwargs`` + - + - 傳遞給觸發函式的參數 diff --git a/docs/source/Zh/doc/cli/cli_doc.rst b/docs/source/Zh/doc/cli/cli_doc.rst index d8b5b2e..64bac6c 100644 --- a/docs/source/Zh/doc/cli/cli_doc.rst +++ b/docs/source/Zh/doc/cli/cli_doc.rst @@ -1,17 +1,61 @@ 命令列介面 +========== + +概述 +---- + +WebRunner 可以直接從命令列使用 ``je_web_runner`` 模組執行。 + +指令 ---- -我們可以使用 CLI 模式去執行 keyword.json 檔案或執行包含 Keyword.json files 的資料夾, -以下這個範例是去執行指定路徑的關鍵字 json 檔 +**執行單一 JSON 動作檔案:** + +.. code-block:: bash + + python -m je_web_runner -e actions.json + python -m je_web_runner --execute_file actions.json + +**執行目錄中所有 JSON 檔案:** + +.. code-block:: bash + + python -m je_web_runner -d ./actions/ + python -m je_web_runner --execute_dir ./actions/ + +**直接執行 JSON 動作字串:** + +.. code-block:: bash + + python -m je_web_runner --execute_str '[["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], ["WR_quit"]]' -.. code-block:: +指令參考 +-------- - python je_web_runner --execute_file "your_file_path" +.. list-table:: + :header-rows: 1 + :widths: 25 15 60 + * - 旗標 + - 簡寫 + - 說明 + * - ``--execute_file`` + - ``-e`` + - 執行單一 JSON 動作檔案 + * - ``--execute_dir`` + - ``-d`` + - 執行目錄中所有 JSON 檔案 + * - ``--execute_str`` + - + - 直接執行 JSON 動作字串 +.. note:: -以下這個範例是去執行指定路徑資料夾下所有的 keyword json 檔 + 在 Windows 上,``--execute_str`` 選項可能需要雙重 JSON 解析(因 shell 跳脫字元)。 + WebRunner 會自動處理此情況。 -.. code-block:: +錯誤處理 +-------- - python je_web_runner --execute_dir "your_dir_path" \ No newline at end of file +若未提供任何參數,WebRunner 會引發 ``WebRunnerExecuteException``。 +所有錯誤會輸出到 stderr,程序以代碼 1 退出。 diff --git a/docs/source/Zh/doc/create_project/create_project_doc.rst b/docs/source/Zh/doc/create_project/create_project_doc.rst index b9023f9..be3cbcd 100644 --- a/docs/source/Zh/doc/create_project/create_project_doc.rst +++ b/docs/source/Zh/doc/create_project/create_project_doc.rst @@ -1,23 +1,62 @@ -創建專案 +建立專案 +======== + +概述 ---- -在 WebRunner 裡可以創建專案,創建專案後將會自動生成範例文件, -範例文件包含 python executor 檔案以及 keyword.json 檔案。 +WebRunner 可以產生快速啟動的專案結構,包含範例 JSON 關鍵字檔案和 Python 執行器腳本。 -要創建專案可以用以下方式: +用法 +---- .. code-block:: python from je_web_runner import create_project_dir - # create on current workdir + + # 在當前工作目錄建立 create_project_dir() - # create project on project_path - create_project_dir("project_path") - # create project on project_path and dir name is My First Project - create_project_dir("project_path", "My First Project") -或是這個方式將會在 project_path 路徑產生專案 + # 在指定路徑建立 + create_project_dir(project_path="./my_project") + + # 自訂父目錄名稱 + create_project_dir(project_path="./my_project", parent_name="MyTest") + +命令列方式 +---------- + +.. code-block:: bash + + python -m je_web_runner --create_project ./my_project + +產生的結構 +---------- + +.. code-block:: text + + my_project/WebRunner/ + ├── keyword/ + │ ├── keyword1.json # 範例動作檔案(成功案例) + │ ├── keyword2.json # 範例動作檔案(成功案例) + │ └── bad_keyword_1.json # 範例動作檔案(失敗案例) + └── executor/ + ├── executor_one_file.py # 執行單一 JSON 檔案 + ├── executor_folder.py # 執行資料夾中所有 JSON 檔案 + └── executor_bad_file.py # 執行失敗案例檔案 + +參數 +---- -.. code-block:: console +.. list-table:: + :header-rows: 1 + :widths: 25 25 50 - python -m je_web_runner --create_project project_path \ No newline at end of file + * - 參數 + - 預設值 + - 說明 + * - ``project_path`` + - 當前工作目錄 + - 專案建立路徑 + * - ``parent_name`` + - ``"WebRunner"`` + - 頂層專案目錄名稱 diff --git a/docs/source/Zh/doc/exception/exception_doc.rst b/docs/source/Zh/doc/exception/exception_doc.rst new file mode 100644 index 0000000..f840422 --- /dev/null +++ b/docs/source/Zh/doc/exception/exception_doc.rst @@ -0,0 +1,83 @@ +例外處理 +======== + +概述 +---- + +WebRunner 提供自訂例外階層結構。所有例外繼承自 ``WebRunnerException``。 + +例外階層 +-------- + +.. code-block:: text + + WebRunnerException(基底) + ├── WebRunnerWebDriverNotFoundException + ├── WebRunnerOptionsWrongTypeException + ├── WebRunnerArgumentWrongTypeException + ├── WebRunnerWebDriverIsNoneException + ├── WebRunnerExecuteException + ├── WebRunnerAssertException + ├── WebRunnerHTMLException + ├── WebRunnerAddCommandException + ├── WebRunnerJsonException + │ └── WebRunnerGenerateJsonReportException + ├── XMLException + │ └── XMLTypeException + └── CallbackExecutorException + +例外參考 +-------- + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - 例外 + - 說明 + * - ``WebRunnerException`` + - 所有 WebRunner 錯誤的基底例外 + * - ``WebRunnerWebDriverNotFoundException`` + - 找不到 WebDriver 或不支援的瀏覽器名稱 + * - ``WebRunnerOptionsWrongTypeException`` + - 提供了無效的選項型別 + * - ``WebRunnerArgumentWrongTypeException`` + - 提供了無效的參數型別 + * - ``WebRunnerWebDriverIsNoneException`` + - WebDriver 為 None(未初始化) + * - ``WebRunnerExecuteException`` + - 動作執行錯誤(未知指令、無效格式) + * - ``WebRunnerJsonException`` + - JSON 處理錯誤 + * - ``WebRunnerGenerateJsonReportException`` + - JSON 報告產生錯誤 + * - ``WebRunnerAssertException`` + - 斷言驗證失敗 + * - ``WebRunnerHTMLException`` + - HTML 報告產生錯誤 + * - ``WebRunnerAddCommandException`` + - 註冊自訂指令錯誤(非函式/方法) + * - ``XMLException`` + - XML 處理錯誤 + * - ``XMLTypeException`` + - 無效的 XML 類型 + * - ``CallbackExecutorException`` + - 回調執行錯誤 + +範例 +---- + +.. code-block:: python + + from je_web_runner import get_webdriver_manager + from je_web_runner.utils.exception.exceptions import ( + WebRunnerException, + WebRunnerWebDriverNotFoundException, + ) + + try: + manager = get_webdriver_manager("unsupported_browser") + except WebRunnerWebDriverNotFoundException as e: + print(f"不支援的瀏覽器: {e}") + except WebRunnerException as e: + print(f"WebRunner 錯誤: {e}") diff --git a/docs/source/Zh/doc/generate_report/generate_report_doc.rst b/docs/source/Zh/doc/generate_report/generate_report_doc.rst index f45eab4..e3daa36 100644 --- a/docs/source/Zh/doc/generate_report/generate_report_doc.rst +++ b/docs/source/Zh/doc/generate_report/generate_report_doc.rst @@ -1,23 +1,110 @@ 報告產生 +======== + +概述 ---- -Generate Report 可以生成以下格式的報告 +WebRunner 可以自動記錄所有動作,並產生三種格式的報告: +**HTML**、**JSON** 和 **XML**。報告包含每個執行動作的詳細資訊, +包括函式名稱、參數、時間戳記和例外資訊。 + +.. note:: + + 產生報告前必須先啟用測試記錄。 + +啟用記錄 +-------- + +.. code-block:: python + + from je_web_runner import test_record_instance + + test_record_instance.set_record_enable(True) + +HTML 報告 +--------- + +HTML 報告包含顏色編碼的表格: + +* **水藍色** 背景表示成功的動作 +* **紅色** 背景表示失敗的動作 + +.. code-block:: python + + from je_web_runner import generate_html, generate_html_report + + # 產生 HTML 字串 + html_content = generate_html() -* HTML -* JSON -* XML -* Generate Report 主要用來記錄與確認有哪些步驟執行,執行是否成功, -* 下面的範例有搭配 keyword and executor 如果看不懂可以先去看看 executor + # 儲存為檔案(建立 test_results.html) + generate_html_report("test_results") -以下是產生 HTML 的範例。 +JSON 報告 +--------- + +JSON 報告分別產生成功和失敗的檔案。 .. code-block:: python -以下是產生 JSON 的範例。 + from je_web_runner import generate_json, generate_json_report + + # 產生字典(回傳 success_dict, failure_dict 元組) + success_dict, failure_dict = generate_json() + + # 儲存為檔案: + # - test_results_success.json + # - test_results_failure.json + generate_json_report("test_results") + +XML 報告 +--------- .. code-block:: python -以下是產生 XML 的範例。 + from je_web_runner import generate_xml, generate_xml_report + + # 產生 XML 結構 + success_xml, failure_xml = generate_xml() + + # 儲存為檔案: + # - test_results_success.xml + # - test_results_failure.xml + generate_xml_report("test_results") + +透過 Action Executor 產生報告 +------------------------------ .. code-block:: python + from je_web_runner import execute_action + + execute_action([ + ["WR_set_record_enable", {"set_enable": True}], + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_to_url", {"url": "https://example.com"}], + ["WR_quit"], + ["WR_generate_html_report", {"html_name": "my_report"}], + ]) + +記錄資料格式 +------------ + +.. list-table:: + :header-rows: 1 + :widths: 25 25 50 + + * - 欄位 + - 型別 + - 說明 + * - ``function_name`` + - ``str`` + - 執行的函式名稱 + * - ``local_param`` + - ``dict | None`` + - 傳遞給函式的參數 + * - ``time`` + - ``str`` + - 執行時間戳記 + * - ``program_exception`` + - ``str`` + - 例外訊息或 ``"None"`` diff --git a/docs/source/Zh/doc/installation/installation_doc.rst b/docs/source/Zh/doc/installation/installation_doc.rst index 51c239e..f065618 100644 --- a/docs/source/Zh/doc/installation/installation_doc.rst +++ b/docs/source/Zh/doc/installation/installation_doc.rst @@ -1,15 +1,85 @@ 安裝 ----- +==== -.. code-block:: python +透過 pip 安裝 +-------------- + +**穩定版:** + +.. code-block:: bash pip install je_web_runner -* Python & pip require version - * Python 3.7 & up - * pip 19.3 & up +**開發版:** + +.. code-block:: bash + + pip install je_web_runner_dev + +系統需求 +-------- + +* Python **3.10** 或更新版本 +* pip **19.3** 或更新版本 + +相依套件(自動安裝): + +* ``selenium>=4.0.0`` +* ``requests`` +* ``python-dotenv`` +* ``webdriver-manager`` + +支援平台 +-------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - 平台 + - 備註 + * - Windows 11 + - 完整支援 + * - macOS + - 完整支援 + * - Ubuntu / Linux + - 完整支援 + * - Raspberry Pi + - 完整支援 + +支援瀏覽器 +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 20 50 + + * - 瀏覽器 + - 代碼 + - 備註 + * - Google Chrome + - ``chrome`` + - 最常使用,透過 webdriver-manager 自動管理 + * - Chromium + - ``chromium`` + - 開源 Chrome 變體 + * - Mozilla Firefox + - ``firefox`` + - 透過 GeckoDriver 完整支援 + * - Microsoft Edge + - ``edge`` + - 基於 Chromium 的 Edge + * - Internet Explorer + - ``ie`` + - 舊版支援 + * - Apple Safari + - ``safari`` + - 僅 macOS,無自動驅動程式管理 + +驗證安裝 +-------- + +.. code-block:: python -* Dev env - * windows 11 - * osx 11 big sur - * ubuntu 20.0.4 + import je_web_runner + print(je_web_runner.__all__) diff --git a/docs/source/Zh/doc/logging/logging_doc.rst b/docs/source/Zh/doc/logging/logging_doc.rst new file mode 100644 index 0000000..27e6259 --- /dev/null +++ b/docs/source/Zh/doc/logging/logging_doc.rst @@ -0,0 +1,51 @@ +日誌 +==== + +概述 +---- + +WebRunner 使用 Python 的 ``logging`` 模組搭配旋轉檔案處理器來記錄自動化事件、錯誤和警告。 + +設定 +---- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - 屬性 + - 值 + * - 日誌檔案 + - ``WEBRunner.log`` + * - 日誌等級 + - ``WARNING`` 及以上 + * - 最大檔案大小 + - 1 GB(旋轉) + * - 日誌格式 + - ``%(asctime)s | %(name)s | %(levelname)s | %(message)s`` + * - 處理器 + - ``RotatingFileHandler``(自訂 ``WebRunnerLoggingHandler``) + +日誌輸出 +-------- + +日誌檔案在當前工作目錄中建立為 ``WEBRunner.log``。 +當檔案達到 1 GB 時會進行旋轉。 + +日誌範例: + +.. code-block:: text + + 2025-01-01 12:00:00 | je_web_runner | WARNING | WebDriverWrapper find_element failed: ... + 2025-01-01 12:00:01 | je_web_runner | ERROR | WebdriverManager quit, failed: ... + +日誌實例 +-------- + +全域日誌可透過 ``web_runner_logger`` 存取: + +.. code-block:: python + + from je_web_runner.utils.logging.loggin_instance import web_runner_logger + + web_runner_logger.warning("自訂警告訊息") diff --git a/docs/source/Zh/doc/package_manager/package_manager_doc.rst b/docs/source/Zh/doc/package_manager/package_manager_doc.rst new file mode 100644 index 0000000..c7af6a3 --- /dev/null +++ b/docs/source/Zh/doc/package_manager/package_manager_doc.rst @@ -0,0 +1,61 @@ +套件管理器 +========== + +概述 +---- + +套件管理器可在執行期間動態載入外部 Python 套件到執行器中。 +當套件被加入後,其所有公開函式和類別會以 ``{套件名}_{函式名}`` 的命名慣例註冊到事件字典。 + +透過 Action Executor 使用 +-------------------------- + +.. code-block:: python + + from je_web_runner import execute_action + + actions = [ + # 將 'time' 套件載入執行器 + ["WR_add_package_to_executor", {"package": "time"}], + + # 現在可以使用 time.sleep(名稱為 "time_sleep") + ["time_sleep", [2]], + ] + + execute_action(actions) + +直接 API 使用 +-------------- + +.. code-block:: python + + from je_web_runner.utils.package_manager.package_manager_class import package_manager + + # 檢查套件是否存在並匯入 + module = package_manager.check_package("os") + + # 將套件的所有函式加入執行器 + package_manager.add_package_to_executor("math") + + # 加入回調執行器 + package_manager.add_package_to_callback_executor("time") + +方法 +---- + +.. list-table:: + :header-rows: 1 + :widths: 30 30 40 + + * - 方法 + - 參數 + - 說明 + * - ``check_package()`` + - ``package: str`` + - 檢查並匯入套件,回傳模組或 None + * - ``add_package_to_executor()`` + - ``package: str`` + - 將套件成員加入 Executor 的 event_dict + * - ``add_package_to_callback_executor()`` + - ``package: str`` + - 將套件成員加入 CallbackExecutor 的 event_dict diff --git a/docs/source/Zh/doc/quick_start/quick_start_doc.rst b/docs/source/Zh/doc/quick_start/quick_start_doc.rst new file mode 100644 index 0000000..fec6164 --- /dev/null +++ b/docs/source/Zh/doc/quick_start/quick_start_doc.rst @@ -0,0 +1,101 @@ +快速開始 +======== + +本指南示範使用 WebRunner 的三種主要方式。 + +範例一:直接使用 Python API +----------------------------- + +直接在 Python 程式碼中使用 WebRunner 的類別和函式。 + +.. code-block:: python + + from je_web_runner import TestObject + from je_web_runner import get_webdriver_manager + from je_web_runner import web_element_wrapper + + # 建立 WebDriver 管理器(使用 Chrome) + manager = get_webdriver_manager("chrome") + + # 導航到網址 + manager.webdriver_wrapper.to_url("https://www.google.com") + + # 設定隱式等待 + manager.webdriver_wrapper.implicitly_wait(2) + + # 建立測試物件來定位搜尋框 + search_box = TestObject("q", "name") + + # 尋找元素 + manager.webdriver_wrapper.find_element(search_box) + + # 點擊並輸入文字 + web_element_wrapper.click_element() + web_element_wrapper.input_to_element("WebRunner automation") + + # 關閉瀏覽器 + manager.quit() + +範例二:JSON 動作列表 +---------------------- + +將自動化腳本定義為 JSON 動作列表,透過 Action Executor 執行。 + +.. code-block:: python + + from je_web_runner import execute_action + + actions = [ + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_to_url", {"url": "https://www.google.com"}], + ["WR_implicitly_wait", {"time_to_wait": 2}], + ["WR_SaveTestObject", {"test_object_name": "q", "object_type": "name"}], + ["WR_find_element", {"element_name": "q"}], + ["WR_click_element"], + ["WR_input_to_element", {"input_value": "WebRunner automation"}], + ["WR_quit"] + ] + + result = execute_action(actions) + +範例三:從 JSON 檔案執行 +-------------------------- + +建立 ``actions.json`` 檔案: + +.. code-block:: json + + [ + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_to_url", {"url": "https://example.com"}], + ["WR_quit"] + ] + +在 Python 中執行: + +.. code-block:: python + + from je_web_runner import execute_files + + results = execute_files(["actions.json"]) + +或使用命令列: + +.. code-block:: bash + + python -m je_web_runner -e actions.json + +範例四:無頭模式 +----------------- + +使用無頭模式在沒有可見 GUI 視窗的情況下執行瀏覽器。 + +.. code-block:: python + + from je_web_runner import get_webdriver_manager + + manager = get_webdriver_manager("chrome", options=["--headless", "--disable-gpu"]) + manager.webdriver_wrapper.to_url("https://example.com") + title = manager.webdriver_wrapper.execute_script("return document.title") + print(f"頁面標題: {title}") + manager.quit() diff --git a/docs/source/Zh/doc/scheduler/scheduler_doc.rst b/docs/source/Zh/doc/scheduler/scheduler_doc.rst deleted file mode 100644 index 0213e54..0000000 --- a/docs/source/Zh/doc/scheduler/scheduler_doc.rst +++ /dev/null @@ -1,19 +0,0 @@ -Scheduler ----- - -可以使用排程來執行重複的任務,可以使用對 APScheduler 的簡易包裝或是觀看 API 文件自行使用 - -.. code-block:: python - - from je_web_runner import SchedulerManager - - - def test_scheduler(): - print("Test Scheduler") - scheduler.remove_blocking_job(id="test") - scheduler.shutdown_blocking_scheduler() - - - scheduler = SchedulerManager() - scheduler.add_interval_blocking_secondly(function=test_scheduler, id="test") - scheduler.start_block_scheduler() diff --git a/docs/source/Zh/doc/socket_driver/socket_driver_doc.rst b/docs/source/Zh/doc/socket_driver/socket_driver_doc.rst index 6c17aa1..37d6340 100644 --- a/docs/source/Zh/doc/socket_driver/socket_driver_doc.rst +++ b/docs/source/Zh/doc/socket_driver/socket_driver_doc.rst @@ -1,26 +1,72 @@ -Socket Driver +遠端自動化(Socket 伺服器) +============================ + +概述 ---- -* 實驗性的功能。 -* Socket Server 主要用來讓其他程式語言也可以使用 AutoControl。 -* 透過底層的 executor 處理接收到字串並進行執行動作。 -* 可以透過遠端來執行測試的動作。 +WebRunner 包含一個多執行緒 TCP Socket 伺服器,用於遠端自動化控制。 +這使得跨語言支援成為可能 -- 任何支援 TCP Socket 的語言 +(Java、C#、Go 等)都可以向 WebRunner 發送自動化指令。 + +啟動伺服器 +---------- + +.. code-block:: python + + from je_web_runner import start_web_runner_socket_server + + server = start_web_runner_socket_server(host="localhost", port=9941) + +伺服器在背景常駐執行緒中啟動,立即準備接受連線。 -* 目前有實驗性的 Java 與 C# 支援。 -* 每個段落結束都應該傳輸 Return_Data_Over_JE。 -* 使用 UTF-8 encoding。 -* 傳送 quit_server 將會關閉伺服器。 +客戶端連線範例 +-------------- .. code-block:: python - import sys + import socket + import json + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(("localhost", 9941)) + + # 發送 JSON 格式的動作(UTF-8 編碼) + actions = [ + ["WR_get_webdriver_manager", {"webdriver_name": "chrome"}], + ["WR_to_url", {"url": "https://example.com"}], + ["WR_quit"] + ] + sock.send(json.dumps(actions).encode("utf-8")) + + # 接收結果(以 "Return_Data_Over_JE\n" 結尾) + response = sock.recv(4096).decode("utf-8") + print(response) + + # 關閉伺服器 + sock.send("quit_server".encode("utf-8")) + +協定 +---- - from je_web_runner import start_autocontrol_socket_server +.. list-table:: + :header-rows: 1 + :widths: 30 70 - try: - server = start_autocontrol_socket_server() - while not server.close_flag: - pass - sys.exit(0) - except Exception as error: - print(repr(error)) \ No newline at end of file + * - 屬性 + - 值 + * - 預設主機 + - ``localhost`` + * - 預設埠號 + - ``9941`` + * - 編碼 + - UTF-8 + * - 訊息格式 + - JSON 動作陣列 + * - 最大接收緩衝 + - 8192 bytes + * - 回應終止符 + - ``Return_Data_Over_JE`` + * - 關閉指令 + - ``quit_server`` + * - 執行緒模型 + - 多執行緒(``socketserver.ThreadingMixIn``) diff --git a/docs/source/Zh/doc/test_object/test_object_doc.rst b/docs/source/Zh/doc/test_object/test_object_doc.rst new file mode 100644 index 0000000..32b3880 --- /dev/null +++ b/docs/source/Zh/doc/test_object/test_object_doc.rst @@ -0,0 +1,63 @@ +測試物件 +======== + +概述 +---- + +``TestObject`` 封裝元素定位資訊(策略+值),用於可重複使用的元素定義。 +``TestObjectRecord`` 以名稱儲存 ``TestObject`` 實例,供 ``_with_test_object`` 方法使用。 + +建立測試物件 +------------ + +.. code-block:: python + + from je_web_runner import TestObject, create_test_object, get_test_object_type_list + + # 建構子:TestObject(test_object_name, test_object_type) + obj1 = TestObject("search", "name") + + # 工廠函式:create_test_object(object_type, test_object_name) + obj2 = create_test_object("id", "submit-btn") + +可用定位類型 +------------ + +.. code-block:: python + + print(get_test_object_type_list()) + # ['ID', 'NAME', 'XPATH', 'CSS_SELECTOR', 'CLASS_NAME', + # 'TAG_NAME', 'LINK_TEXT', 'PARTIAL_LINK_TEXT'] + +這些直接對應 Selenium 的 ``By`` 類別常數。 + +TestObjectRecord +---------------- + +.. code-block:: python + + from je_web_runner.utils.test_object.test_object_record.test_object_record_class import test_object_record + + # 儲存測試物件 + test_object_record.save_test_object("search_box", "name") + + # 移除測試物件 + test_object_record.remove_test_object("search_box") + + # 清除所有紀錄 + test_object_record.clean_record() + +在 Action Executor 中使用 +-------------------------- + +.. code-block:: python + + from je_web_runner import execute_action + + execute_action([ + ["WR_SaveTestObject", {"test_object_name": "search", "object_type": "name"}], + ["WR_find_element", {"element_name": "search"}], + ["WR_click_element"], + ["WR_input_to_element", {"input_value": "hello"}], + ["WR_CleanTestObject"], + ]) diff --git a/docs/source/Zh/doc/test_record/test_record_doc.rst b/docs/source/Zh/doc/test_record/test_record_doc.rst new file mode 100644 index 0000000..fe3c176 --- /dev/null +++ b/docs/source/Zh/doc/test_record/test_record_doc.rst @@ -0,0 +1,71 @@ +測試記錄 +======== + +概述 +---- + +所有 WebRunner 動作都可以自動記錄,用於稽核追蹤和報告產生。 +全域實例 ``test_record_instance`` 管理記錄狀態和儲存記錄。 + +啟用記錄 +-------- + +記錄預設為停用。在執行動作前啟用: + +.. code-block:: python + + from je_web_runner import test_record_instance + + test_record_instance.set_record_enable(True) + +或透過動作執行器: + +.. code-block:: python + + from je_web_runner import execute_action + + execute_action([ + ["WR_set_record_enable", {"set_enable": True}], + ]) + +存取記錄 +-------- + +.. code-block:: python + + records = test_record_instance.test_record_list + + for record in records: + print(record) + +記錄格式 +-------- + +每筆記錄是一個字典,包含以下欄位: + +.. list-table:: + :header-rows: 1 + :widths: 25 25 50 + + * - 欄位 + - 型別 + - 說明 + * - ``function_name`` + - ``str`` + - 執行的函式名稱 + * - ``local_param`` + - ``dict | None`` + - 傳遞給函式的參數 + * - ``time`` + - ``str`` + - 執行時間戳記 + * - ``program_exception`` + - ``str`` + - 例外訊息或 ``"None"`` + +清除記錄 +-------- + +.. code-block:: python + + test_record_instance.clean_record() diff --git a/docs/source/Zh/doc/web_element/web_element_doc.rst b/docs/source/Zh/doc/web_element/web_element_doc.rst new file mode 100644 index 0000000..c8b0ed0 --- /dev/null +++ b/docs/source/Zh/doc/web_element/web_element_doc.rst @@ -0,0 +1,142 @@ +Web 元素包裝器 +=============== + +概述 +---- + +``WebElementWrapper`` 提供與已定位元素互動的方法。 +它操作由 ``find_element()`` 或 ``find_elements()`` 設定的當前活動元素。 + +全域實例 ``web_element_wrapper`` 從 ``je_web_runner`` 匯入。 + +基本互動 +-------- + +.. code-block:: python + + from je_web_runner import web_element_wrapper + + web_element_wrapper.click_element() # 點擊元素 + web_element_wrapper.input_to_element("Hello World") # 輸入文字 + web_element_wrapper.clear() # 清除內容 + web_element_wrapper.submit() # 提交表單 + +屬性檢查 +-------- + +.. code-block:: python + + web_element_wrapper.get_attribute("href") # 取得 HTML 屬性 + web_element_wrapper.get_property("checked") # 取得 JS 屬性 + web_element_wrapper.get_dom_attribute("data-id") # 取得 DOM 屬性 + +狀態檢查 +-------- + +.. code-block:: python + + web_element_wrapper.is_displayed() # 檢查是否可見 + web_element_wrapper.is_enabled() # 檢查是否可用 + web_element_wrapper.is_selected() # 檢查是否被選取(核取方塊/單選) + +CSS 屬性 +--------- + +.. code-block:: python + + web_element_wrapper.value_of_css_property("color") + +下拉選單處理 +------------ + +.. code-block:: python + + select = web_element_wrapper.get_select() + # 使用 Selenium 的 Select API: + # select.select_by_visible_text("選項一") + # select.select_by_value("opt1") + # select.select_by_index(0) + +元素截圖 +-------- + +.. code-block:: python + + web_element_wrapper.screenshot("element") # 儲存為 element.png + +切換元素 +-------- + +當 ``find_elements()`` 回傳多個元素時,使用 ``change_web_element()`` 切換活動元素: + +.. code-block:: python + + # 切換到第 3 個元素(索引 2) + web_element_wrapper.change_web_element(2) + web_element_wrapper.click_element() + +元素驗證 +-------- + +.. code-block:: python + + web_element_wrapper.check_current_web_element({ + "tag_name": "input", + "enabled": True + }) + +方法參考 +-------- + +.. list-table:: + :header-rows: 1 + :widths: 25 35 40 + + * - 方法 + - 參數 + - 說明 + * - ``click_element()`` + - + - 點擊當前元素 + * - ``input_to_element()`` + - ``input_value: str`` + - 輸入文字到元素 + * - ``clear()`` + - + - 清除元素內容 + * - ``submit()`` + - + - 提交表單 + * - ``get_attribute()`` + - ``name: str`` + - 取得 HTML 屬性值 + * - ``get_property()`` + - ``name: str`` + - 取得 JavaScript 屬性值 + * - ``get_dom_attribute()`` + - ``name: str`` + - 取得 DOM 屬性值 + * - ``is_displayed()`` + - + - 檢查元素是否可見 + * - ``is_enabled()`` + - + - 檢查元素是否可用 + * - ``is_selected()`` + - + - 檢查元素是否被選取 + * - ``value_of_css_property()`` + - ``property_name: str`` + - 取得 CSS 屬性值 + * - ``screenshot()`` + - ``filename: str`` + - 對元素截圖 + * - ``change_web_element()`` + - ``element_index: int`` + - 透過索引切換活動元素 + * - ``check_current_web_element()`` + - ``check_dict: dict`` + - 驗證元素屬性 + * - ``get_select()`` + - + - 取得下拉選單的 Select 物件 diff --git a/docs/source/Zh/doc/webdriver_manager/webdriver_manager_doc.rst b/docs/source/Zh/doc/webdriver_manager/webdriver_manager_doc.rst new file mode 100644 index 0000000..d3fa9aa --- /dev/null +++ b/docs/source/Zh/doc/webdriver_manager/webdriver_manager_doc.rst @@ -0,0 +1,100 @@ +WebDriver 管理器 +================= + +概述 +---- + +``WebdriverManager`` 管理多個 WebDriver 實例以支援並行瀏覽器自動化。 +它維護一個活動 WebDriver 清單,並提供建立、切換和關閉的方法。 + +透過工廠函式 ``get_webdriver_manager()`` 取得管理器。 + +建立管理器 +---------- + +.. code-block:: python + + from je_web_runner import get_webdriver_manager + + # 使用 Chrome 建立 + manager = get_webdriver_manager("chrome") + + # 使用 Firefox 搭配選項建立 + manager = get_webdriver_manager("firefox", options=["--headless"]) + +管理多個瀏覽器 +-------------- + +.. code-block:: python + + manager = get_webdriver_manager("chrome") + + # 新增第二個瀏覽器實例 + manager.new_driver("firefox") + + # 切換到 Chrome(索引 0) + manager.change_webdriver(0) + manager.webdriver_wrapper.to_url("https://example.com") + + # 切換到 Firefox(索引 1) + manager.change_webdriver(1) + manager.webdriver_wrapper.to_url("https://google.com") + + # 僅關閉 Firefox + manager.close_choose_webdriver(1) + + # 關閉當前瀏覽器 + manager.close_current_webdriver() + + # 關閉並退出所有瀏覽器 + manager.quit() + +關鍵屬性 +-------- + +.. list-table:: + :header-rows: 1 + :widths: 30 30 40 + + * - 屬性 + - 型別 + - 說明 + * - ``webdriver_wrapper`` + - ``WebDriverWrapper`` + - WebDriver 操作包裝器 + * - ``webdriver_element`` + - ``WebElementWrapper`` + - 元素操作包裝器 + * - ``current_webdriver`` + - ``WebDriver | None`` + - 當前活動的 WebDriver 實例 + +方法 +---- + +.. list-table:: + :header-rows: 1 + :widths: 30 40 30 + + * - 方法 + - 參數 + - 說明 + * - ``new_driver()`` + - ``webdriver_name: str, options: List[str] = None, **kwargs`` + - 建立新的 WebDriver 實例 + * - ``change_webdriver()`` + - ``index_of_webdriver: int`` + - 透過索引切換 WebDriver + * - ``close_current_webdriver()`` + - + - 關閉當前 WebDriver + * - ``close_choose_webdriver()`` + - ``webdriver_index: int`` + - 透過索引關閉指定 WebDriver + * - ``quit()`` + - + - 關閉並退出所有 WebDriver + +.. note:: + + 呼叫 ``quit()`` 時,會同時清除所有已儲存的 ``TestObjectRecord`` 項目。 diff --git a/docs/source/Zh/doc/webdriver_options/webdriver_options_doc.rst b/docs/source/Zh/doc/webdriver_options/webdriver_options_doc.rst new file mode 100644 index 0000000..626400e --- /dev/null +++ b/docs/source/Zh/doc/webdriver_options/webdriver_options_doc.rst @@ -0,0 +1,61 @@ +WebDriver 選項設定 +================== + +概述 +---- + +在啟動 WebDriver 之前設定瀏覽器選項和功能。 +適用於無頭模式、停用 GPU、設定視窗大小等。 + +瀏覽器參數 +---------- + +.. code-block:: python + + from je_web_runner import set_webdriver_options_argument, get_webdriver_manager + + # 設定瀏覽器參數(回傳 Options 物件) + options = set_webdriver_options_argument("chrome", [ + "--headless", + "--disable-gpu", + "--no-sandbox", + "--window-size=1920,1080" + ]) + + # 或在建立管理器時直接傳入 + manager = get_webdriver_manager("chrome", options=["--headless", "--disable-gpu"]) + +常用 Chrome 參數 +~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - 參數 + - 說明 + * - ``--headless`` + - 不顯示 GUI 執行 + * - ``--disable-gpu`` + - 停用 GPU 硬體加速 + * - ``--no-sandbox`` + - 停用沙箱(某些 Linux 環境需要) + * - ``--window-size=W,H`` + - 設定初始視窗大小 + * - ``--incognito`` + - 以無痕模式開啟 + * - ``--start-maximized`` + - 以最大化視窗啟動 + +DesiredCapabilities +------------------- + +.. code-block:: python + + from je_web_runner import get_desired_capabilities, get_desired_capabilities_keys + + # 檢視可用的功能鍵(瀏覽器名稱) + keys = get_desired_capabilities_keys() + + # 取得特定瀏覽器的功能 + caps = get_desired_capabilities("CHROME") diff --git a/docs/source/Zh/doc/webdriver_wrapper/webdriver_wrapper_doc.rst b/docs/source/Zh/doc/webdriver_wrapper/webdriver_wrapper_doc.rst new file mode 100644 index 0000000..0e49c96 --- /dev/null +++ b/docs/source/Zh/doc/webdriver_wrapper/webdriver_wrapper_doc.rst @@ -0,0 +1,192 @@ +WebDriver 包裝器 +================= + +概述 +---- + +``WebDriverWrapper`` 是核心元件,包裝了 Selenium WebDriver 並提供全面的方法。 +涵蓋瀏覽器控制、元素尋找、滑鼠/鍵盤操作、Cookie 管理、JavaScript 執行、視窗管理等功能。 + +導航 +---- + +.. code-block:: python + + wrapper = manager.webdriver_wrapper + + wrapper.to_url("https://example.com") # 導航至網址 + wrapper.forward() # 前進 + wrapper.back() # 後退 + wrapper.refresh() # 重新整理 + +元素尋找 +-------- + +使用 ``TestObject`` 定位器尋找元素: + +.. code-block:: python + + from je_web_runner import TestObject + + # 定位策略:id, name, xpath, css selector, + # class name, tag name, link text, partial link text + element = TestObject("search-input", "id") + + wrapper.find_element(element) # 尋找單一元素 + wrapper.find_elements(element) # 尋找多個元素 + +使用已儲存的 ``TestObjectRecord`` 名稱尋找: + +.. code-block:: python + + wrapper.find_element_with_test_object_record("search-input") + wrapper.find_elements_with_test_object_record("search-input") + +等待方法 +-------- + +.. code-block:: python + + wrapper.implicitly_wait(5) # 隱式等待(秒) + wrapper.explict_wait( # 顯式等待 + wait_condition, + timeout=10, + poll_frequency=0.5 + ) + wrapper.set_script_timeout(30) # 非同步腳本逾時 + wrapper.set_page_load_timeout(60) # 頁面載入逾時 + +滑鼠操作 +-------- + +**基本點擊(於當前位置):** + +.. code-block:: python + + wrapper.left_click() + wrapper.right_click() + wrapper.left_double_click() + wrapper.left_click_and_hold() + wrapper.release() + +**對已儲存的測試物件點擊:** + +.. code-block:: python + + wrapper.left_click_with_test_object("button_name") + wrapper.right_click_with_test_object("button_name") + wrapper.left_double_click_with_test_object("button_name") + wrapper.left_click_and_hold_with_test_object("button_name") + wrapper.release_with_test_object("button_name") + +**拖放:** + +.. code-block:: python + + wrapper.drag_and_drop(source_element, target_element) + wrapper.drag_and_drop_offset(element, x=100, y=50) + + # 使用已儲存的測試物件 + wrapper.drag_and_drop_with_test_object("source_name", "target_name") + wrapper.drag_and_drop_offset_with_test_object("element_name", offset_x=100, offset_y=50) + +**滑鼠移動:** + +.. code-block:: python + + wrapper.move_to_element(element) # 懸停 + wrapper.move_to_element_with_test_object("element_name") + wrapper.move_to_element_with_offset(element, offset_x=10, offset_y=10) + wrapper.move_to_element_with_offset_and_test_object("name", offset_x=10, offset_y=10) + wrapper.move_by_offset(100, 200) # 從當前位置移動 + +鍵盤操作 +-------- + +.. code-block:: python + + wrapper.press_key(keycode) + wrapper.press_key_with_test_object(keycode) + wrapper.release_key(keycode) + wrapper.release_key_with_test_object(keycode) + wrapper.send_keys("text") + wrapper.send_keys_to_element(element, "text") + wrapper.send_keys_to_element_with_test_object("element_name", "text") + +動作鏈控制 +---------- + +.. code-block:: python + + wrapper.perform() # 執行佇列中的動作 + wrapper.reset_actions() # 清除動作佇列 + wrapper.pause(2) # 在動作鏈中暫停(秒) + wrapper.scroll(0, 500) # 捲動頁面 + +Cookie 管理 +----------- + +.. code-block:: python + + wrapper.get_cookies() # 取得所有 Cookie + wrapper.get_cookie("session_id") # 取得指定 Cookie + wrapper.add_cookie({"name": "key", "value": "val"}) + wrapper.delete_cookie("session_id") + wrapper.delete_all_cookies() + +JavaScript 執行 +---------------- + +.. code-block:: python + + wrapper.execute("document.title") + wrapper.execute_script("return document.title") + wrapper.execute_async_script("arguments[0]('done')", callback) + +視窗管理 +-------- + +.. code-block:: python + + wrapper.maximize_window() + wrapper.minimize_window() + wrapper.fullscreen_window() + wrapper.set_window_size(1920, 1080) + wrapper.set_window_position(0, 0) + wrapper.get_window_position() # 回傳 dict + wrapper.get_window_rect() # 回傳 dict + wrapper.set_window_rect(x=0, y=0, width=1920, height=1080) + +截圖 +---- + +.. code-block:: python + + wrapper.get_screenshot_as_png() # 回傳 bytes + wrapper.get_screenshot_as_base64() # 回傳 base64 字串 + +Frame/視窗/Alert 切換 +------------------------ + +.. code-block:: python + + wrapper.switch("frame", "frame_name") + wrapper.switch("window", "window_handle") + wrapper.switch("default_content") + wrapper.switch("parent_frame") + wrapper.switch("active_element") + wrapper.switch("alert") + +瀏覽器日誌 +---------- + +.. code-block:: python + + wrapper.get_log("browser") + +WebDriver 驗證 +--------------- + +.. code-block:: python + + wrapper.check_current_webdriver({"name": "chrome"}) diff --git a/docs/source/Zh/zh_index.rst b/docs/source/Zh/zh_index.rst index faf0d78..77e4f81 100644 --- a/docs/source/Zh/zh_index.rst +++ b/docs/source/Zh/zh_index.rst @@ -1,14 +1,25 @@ ==================================== -WebRunner 繁體中文 文件 +WebRunner 繁體中文文件 ==================================== .. toctree:: :maxdepth: 4 doc/installation/installation_doc.rst + doc/quick_start/quick_start_doc.rst + doc/webdriver_manager/webdriver_manager_doc.rst + doc/webdriver_wrapper/webdriver_wrapper_doc.rst + doc/web_element/web_element_doc.rst + doc/test_object/test_object_doc.rst + doc/action_executor/action_executor_doc.rst doc/create_project/create_project_doc.rst doc/generate_report/generate_report_doc.rst doc/callback_function/callback_function_doc.rst doc/cli/cli_doc.rst - doc/scheduler/scheduler_doc.rst - doc/socket_driver/socket_driver_doc.rst \ No newline at end of file + doc/socket_driver/socket_driver_doc.rst + doc/package_manager/package_manager_doc.rst + doc/webdriver_options/webdriver_options_doc.rst + doc/assertion/assertion_doc.rst + doc/test_record/test_record_doc.rst + doc/exception/exception_doc.rst + doc/logging/logging_doc.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index 534b6df..ec36d7a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,15 +1,8 @@ # Configuration file for the Sphinx documentation builder. # -# This file only contains a selection of the most common options. For a full -# list see the documentation: +# For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# import os import sys @@ -18,34 +11,19 @@ # -- Project information ----------------------------------------------------- project = 'WebRunner' -copyright = '2020 ~ 2023, JE-Chen' +copyright = '2021 ~ 2025, JE-Chen' author = 'JE-Chen' # -- General configuration --------------------------------------------------- -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. extensions = [ "sphinx.ext.autosectionlabel" ] -# Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# html_theme = 'sphinx_rtd_theme' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] diff --git a/docs/source/index.rst b/docs/source/index.rst index 70e0b78..88a2fa1 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -2,8 +2,20 @@ WebRunner ================== +**A cross-platform web automation framework built on Selenium** + +WebRunner (``je_web_runner``) simplifies browser automation with multi-browser support, +parallel execution, automatic driver management, JSON-driven action scripts, and detailed report generation. + +* **PyPI**: https://pypi.org/project/je-web-runner/ +* **GitHub**: https://github.com/Intergration-Automation-Testing/WebRunner +* **License**: MIT + +---- + .. toctree:: :maxdepth: 4 + :caption: Documentation Eng/eng_index.rst Zh/zh_index.rst @@ -12,10 +24,6 @@ WebRunner ---- RoadMap +------- ----- - -* Project Kanban -* https://github.com/orgs/Integrated-Testing-Environment/projects/4/views/1 - ----- \ No newline at end of file +* Project Kanban: https://github.com/orgs/Integrated-Testing-Environment/projects/4/views/1