diff --git a/Drivers/Built_In_Driver.py b/Drivers/Built_In_Driver.py index dd2e894c5..dde1b15f6 100755 --- a/Drivers/Built_In_Driver.py +++ b/Drivers/Built_In_Driver.py @@ -9,14 +9,14 @@ from Framework.Built_In_Automation.Sequential_Actions import sequential_actions as sa -def sequential_actions( +async def sequential_actions( step_data, test_action_info, temp_q, debug_actions=None, ): try: - sTestStepReturnStatus = sa.Sequential_Actions( + sTestStepReturnStatus = await sa.Sequential_Actions( step_data, test_action_info, debug_actions, diff --git a/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py b/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py index 4ff0044c8..fd1a3f61f 100755 --- a/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py +++ b/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py @@ -177,8 +177,22 @@ def write_browser_logs(): try: if str(sr.Get_Shared_Variables("zeuz_collect_browser_log")).strip().lower() in ("false", "no", "off", "disable"): return + drivers = [] + if sr.Test_Shared_Variables("browser_sessions"): + browser_sessions = sr.Get_Shared_Variables("browser_sessions", log=False) + if isinstance(browser_sessions, dict): + drivers.extend( + session.get("selenium_driver") + for session in browser_sessions.values() + if isinstance(session, dict) and session.get("selenium_driver") + ) if sr.Test_Shared_Variables("selenium_driver"): - driver = sr.Get_Shared_Variables("selenium_driver") + drivers.append(sr.Get_Shared_Variables("selenium_driver")) + seen = set() + for driver in drivers: + if id(driver) in seen: + continue + seen.add(id(driver)) for browser_log in driver.get_log("browser"): CommonUtil.ExecLog(sModuleInfo, browser_log["message"], 6,print_Execlog=CommonUtil.show_browser_log) except Exception as e: @@ -279,7 +293,7 @@ def if_else_log_for_actions(left, next_level_step_data, statement="if"): return left + ".... condition matched\n" + "Running actions: " + log_actions -def If_else_action(step_data, data_set_no): +async def If_else_action(step_data, data_set_no): sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME try: data_set = step_data[data_set_no] @@ -526,7 +540,7 @@ def check_operators(): ) return "zeuz_failed" if data_set_index not in inner_skip: - result, skip = Run_Sequential_Actions( + result, skip = await Run_Sequential_Actions( [data_set_index] ) # Running inner_skip = list(set(inner_skip+skip)) @@ -559,7 +573,7 @@ def sanitize_deprecated_dataset(value): return value -def for_loop_action(step_data, data_set_no): +async def for_loop_action(step_data, data_set_no): sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME try: data_set = step_data[data_set_no] @@ -748,7 +762,7 @@ def for_loop_action(step_data, data_set_no): sr.Set_Shared_Variables(CommonUtil.dont_prettify_on_server[0], step_data, protected=True, pretty=False) sr.test_action_info = CommonUtil.all_action_info[step_index] return "zeuz_failed", outer_skip - result, skip = Run_Sequential_Actions([data_set_index]) + result, skip = await Run_Sequential_Actions([data_set_index]) inner_skip = list(set(inner_skip + skip)) outer_skip = list(set(outer_skip + inner_skip)) @@ -848,7 +862,7 @@ def for_loop_action(step_data, data_set_no): return CommonUtil.Exception_Handler(sys.exc_info()), [] -def While_Loop_Action(step_data, data_set_no): +async def While_Loop_Action(step_data, data_set_no): sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME try: data_set = step_data[data_set_no] @@ -947,7 +961,7 @@ def While_Loop_Action(step_data, data_set_no): 3 ) return "zeuz_failed", outer_skip - result, skip = Run_Sequential_Actions( + result, skip = await Run_Sequential_Actions( [data_set_index] ) # new edit: full step data is passed. [step_data[data_set_index]]) # Recursively call this function until all called data sets are complete @@ -1049,7 +1063,7 @@ def ticker_linear_shape(seconds, callable, *args, **kwargs): seconds -= 1 -def Sequential_Actions( +async def Sequential_Actions( step_data, test_action_info, debug_actions=None, @@ -1068,13 +1082,13 @@ def Sequential_Actions( # sr.Set_Shared_Variables("test_action_info", test_action_info, protected=True, print_variable=False) sr.test_action_info = test_action_info - result, skip_for_loop = Run_Sequential_Actions([], debug_actions) + result, skip_for_loop = await Run_Sequential_Actions([], debug_actions) # empty list means run all, instead of step data we want to send the dataset no's of the step data to run write_browser_logs() return result -def Run_Sequential_Actions( +async def Run_Sequential_Actions( data_set_list=None, debug_actions=None ): # data_set_no will used in recursive conditional action call if data_set_list is None: @@ -1100,7 +1114,7 @@ def Run_Sequential_Actions( data_set_list.append(i) if len(data_set_list) == 0 and CommonUtil.debug_status and not sr.Test_Shared_Variables("selenium_driver") and ConfigModule.get_config_value("Inspector", "ai_plugin").strip().lower() in CommonUtil.affirmative_words: - return Action_Handler([["browser", "selenium action", "browser"]], ["browser", "selenium action", "browser"]), [] + return await Action_Handler([["browser", "selenium action", "browser"]], ["browser", "selenium action", "browser"]), [] for dataset_cnt in data_set_list: # For each data set within step data data_set = step_data[dataset_cnt] # Save data set to variable @@ -1196,7 +1210,7 @@ def Run_Sequential_Actions( # If middle column = action, call action handler, but always return a pass elif "optional action" in action_name: - result = Action_Handler(data_set, row) # Pass data set, and action_name to action handler + result = await Action_Handler(data_set, row) # Pass data set, and action_name to action handler if result == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Optional action failed. Returning pass anyway", 2) result = "passed" @@ -1204,7 +1218,7 @@ def Run_Sequential_Actions( # If middle column = conditional action, evaluate data set elif "conditional action" in action_name or "if else" in action_name: if action_name.lower().strip() == "windows conditional action": - result, to_skip = Conditional_Action_Handler(step_data, dataset_cnt) + result, to_skip = await Conditional_Action_Handler(step_data, dataset_cnt) skip += to_skip skip_for_loop += to_skip if result in failed_tag_list: @@ -1215,7 +1229,7 @@ def Run_Sequential_Actions( elif action_name.lower().strip() != "conditional action" and action_name.lower().strip() != "if else": # old style conditional action - result, to_skip = Conditional_Action_Handler(step_data, dataset_cnt) + result, to_skip = await Conditional_Action_Handler(step_data, dataset_cnt) skip += to_skip skip_for_loop += to_skip if result in failed_tag_list: @@ -1224,7 +1238,7 @@ def Run_Sequential_Actions( break else: - result, to_skip = If_else_action(step_data, dataset_cnt) + result, to_skip = await If_else_action(step_data, dataset_cnt) skip += to_skip skip_for_loop += to_skip if result in failed_tag_list: @@ -1235,7 +1249,7 @@ def Run_Sequential_Actions( # Simulate a while/for loop with the specified data sets elif "loop action" in action_name: if action_name.lower().strip() == "for loop action": - result, skip_for_loop = for_loop_action(step_data, dataset_cnt) + result, skip_for_loop = await for_loop_action(step_data, dataset_cnt) skip = list(set(skip + skip_for_loop)) if result in failed_tag_list: return "zeuz_failed", skip_for_loop @@ -1243,7 +1257,7 @@ def Run_Sequential_Actions( elif action_name.lower().strip() not in ("while loop action", "for loop action"): # old style loop action # CommonUtil.ExecLog(sModuleInfo,"Old style loop action found. This will not be supported in 2020, please replace them with new loop actions",2) - result, skip_for_loop = Loop_Action_Handler(data_set, row, dataset_cnt) + result, skip_for_loop = await Loop_Action_Handler(data_set, row, dataset_cnt) skip = skip_for_loop position_of_loop_action = dataset_cnt @@ -1273,7 +1287,7 @@ def Run_Sequential_Actions( return "zeuz_failed", skip_for_loop elif "loop" in action_name: if "while" in action_name.lower(): - result, skip_for_loop = While_Loop_Action(step_data, dataset_cnt) + result, skip_for_loop = await While_Loop_Action(step_data, dataset_cnt) skip = list(set(skip + skip_for_loop)) if result in failed_tag_list: return "zeuz_failed", skip_for_loop @@ -1343,7 +1357,7 @@ def Run_Sequential_Actions( # If middle column = action, call action handler elif "action" in action_name: # Must be last, since it's a single word that also exists in other action types - result = Action_Handler(data_set, row) # Pass data set, and action_name to action handler + result = await Action_Handler(data_set, row) # Pass data set, and action_name to action handler if row[0].lower().strip() in ("step exit", "testcase exit"): global step_exit_fail_called, step_exit_pass_called CommonUtil.ExecLog(sModuleInfo, f"{row[0].lower().strip()} Exit called. Stopping Test Step.", 1) @@ -1373,12 +1387,12 @@ def Run_Sequential_Actions( continue CommonUtil.ExecLog(sModuleInfo, "Action failed. Trying bypass #%d" % (i + 1), 1) - result = Action_Handler(bypass_data_set[i], bypass_row[i]) + result = await Action_Handler(bypass_data_set[i], bypass_row[i]) if result in failed_tag_list: # This also failed, so chances are first failure was real continue # Try the next bypass, if any else: # Bypass passed, which indicates there was something blocking the element in the first place CommonUtil.ExecLog(sModuleInfo, "Bypass passed. Retrying original action", 1) - result = Action_Handler(data_set, row) # Retry failed original data set + result = await Action_Handler(data_set, row) # Retry failed original data set if result in failed_tag_list: # Still a failure, give up return "zeuz_failed", skip_for_loop break # No need to process more bypasses @@ -1404,7 +1418,7 @@ def Run_Sequential_Actions( return CommonUtil.Exception_Handler(sys.exc_info()) -def Loop_Action_Handler(data, row, dataset_cnt): +async def Loop_Action_Handler(data, row, dataset_cnt): """ Performs a sub-set of the data set in a loop, similar to a for or while loop """ sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME @@ -1903,7 +1917,7 @@ def build_subset(new_step_data): return CommonUtil.Exception_Handler(sys.exc_info()) -def Conditional_Action_Handler(step_data, dataset_cnt): +async def Conditional_Action_Handler(step_data, dataset_cnt): """ Process conditional actions, called only by Sequential_Actions() """ sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME @@ -1984,6 +1998,37 @@ def Conditional_Action_Handler(step_data, dataset_cnt): logic_decision = False log_msg += "Element is not found\n" + elif module == "playwright": + try: + from Framework.Built_In_Automation.Web.Playwright import locator as PlaywrightLocator + from Framework.Built_In_Automation.Web.Playwright.BuiltInFunctions import current_page + + wait = 10 + for left, mid, right in data_set: + mid = mid.lower() + left = left.lower() + if "optional parameter" in mid and "wait" in left: + wait = float(right.strip()) + + if current_page is None: + CommonUtil.ExecLog(sModuleInfo, "No browser open for Playwright conditional action", 3) + logic_decision = False + log_msg += "Browser not open\n" + else: + Element = await PlaywrightLocator.Get_Element(data_set, current_page, element_wait=wait) + if Element == "zeuz_failed": + CommonUtil.ExecLog(sModuleInfo, "Conditional Actions could not find the element", 3) + logic_decision = False + log_msg += "Element is not found\n" + else: + logic_decision = True + log_msg += "Element is found\n" + + except: # Element doesn't exist, proceed with the step data following the fail/false path + CommonUtil.ExecLog(sModuleInfo, "Conditional Actions could not find the element", 3) + logic_decision = False + log_msg += "Element is not found\n" + elif module == "windows": try: from Framework.Built_In_Automation.Desktop.Windows import BuiltInFunctions @@ -2186,7 +2231,7 @@ def Conditional_Action_Handler(step_data, dataset_cnt): 2 ) if data_set_index not in inner_skip: - result, skip = Run_Sequential_Actions( + result, skip = await Run_Sequential_Actions( [data_set_index] ) # Running inner_skip = list(set(inner_skip + skip)) @@ -2296,7 +2341,63 @@ def compare_variable_names(set, dataset): CommonUtil.compare_action_varnames = {"left": "Left", "right": "Right"} -def Action_Handler(_data_set, action_row, _bypass_bug=True): +def get_browser_driver_routing(action_subfield, data_set): + """ + Check if browser driver optional parameter is present and route to appropriate driver. + + Args: + action_subfield (str): The original action subfield (e.g., "selenium action", "playwright action") + data_set (list): The data set containing optional parameters + + Returns: + str: Updated action_subfield based on browser driver parameter + + This function checks if there is a "browser driver" optional parameter in the data set or a BROWSER_DRIVER in runtime parameters. + If any of them are present, it updates the action_subfield to the value specified. + If both are present, it uses the action-level optional parameter. + If neither are present, it returns the original action_subfield. + """ + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + + # If the action subfield is not for playwright or selenium, return as is + if action_subfield not in ("playwright action", "selenium action"): + return action_subfield + + # Initialize the updated action subfield with the original action subfield + updated_action_subfield = action_subfield + + # Get the runtime parameter for browser driver preference + browser_driver_runtime_parameter = sr.shared_variables.get("BROWSER_DRIVER") + + # If runtime parameter is present and valid, update the action subfield + if browser_driver_runtime_parameter and browser_driver_runtime_parameter.strip().lower() in ("playwright", "selenium"): + CommonUtil.ExecLog(sModuleInfo, "Runtime parameter for browser driver preference detected", 5) + updated_action_subfield = browser_driver_runtime_parameter.strip().lower() + " action" + + # Check if there is an optional parameter for browser driver in the data set + for left, mid, right in data_set: + # If optional parameter is present and valid, update the action subfield + if (mid.strip().lower().startswith("optional") + and left.strip().lower() == "browser driver" + and right.strip().lower() in ("playwright", "selenium")): + + # If runtime parameter is also present, action-level optional parameter will take precedence + if browser_driver_runtime_parameter: + # log a warning for browser driver preference in two places + CommonUtil.ExecLog(sModuleInfo, "Both runtime parameter and optional parameter for browser driver detected, using optional parameter", 2) + else: + CommonUtil.ExecLog(sModuleInfo, "Optional parameter for browser driver preference detected in action", 5) + updated_action_subfield = right.strip().lower() + " action" + break + + # If the action subfield has changed, log the change + if action_subfield != updated_action_subfield: + CommonUtil.ExecLog(sModuleInfo, "Browser action changed from %s to %s" % (action_subfield, updated_action_subfield), 1) + + return updated_action_subfield + + +async def Action_Handler(_data_set, action_row, _bypass_bug=True): """ Finds the appropriate function for the requested action in the step data and executes it """ sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME @@ -2306,6 +2407,9 @@ def Action_Handler(_data_set, action_row, _bypass_bug=True): action_name = action_row[0] action_subfield = action_row[1] + # Apply browser driver routing if applicable + action_subfield = get_browser_driver_routing(action_subfield, _data_set) + if str(action_name).startswith("%|"): # if shared variable action_name = sr.get_previous_response_variables_in_strings(action_name) @@ -2410,6 +2514,13 @@ def Action_Handler(_data_set, action_row, _bypass_bug=True): if result == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Can't find module for %s" % module, 3) return "zeuz_failed" + session_activator = getattr(eval(module), "_activate_browser_session_for_action", None) + if session_activator: + result = session_activator(data_set, function) + if inspect.iscoroutine(result): + result = await result + if result in failed_tag_list: + return result run_function = getattr(eval(module), function) # create a reference to the function start_time = time.perf_counter() if pre_sleep: @@ -2417,6 +2528,8 @@ def Action_Handler(_data_set, action_row, _bypass_bug=True): elif module in CommonUtil.global_sleep and "_all_" in CommonUtil.global_sleep[module]: time.sleep(CommonUtil.global_sleep[module]["_all_"]["pre"]) result = run_function(data_set) # Execute function, providing all rows in the data set + if inspect.iscoroutine(result): + result = await result if post_sleep: time.sleep(post_sleep) elif module in CommonUtil.global_sleep and "_all_" in CommonUtil.global_sleep[module]: @@ -2436,7 +2549,7 @@ def Action_Handler(_data_set, action_row, _bypass_bug=True): compare_variable_names(False, []) if performance_action.zeuz_cycle != -1: CommonUtil.action_perf[-1]['cycle'] = performance_action.zeuz_cycle - CommonUtil.TakeScreenShot(function) + await CommonUtil.TakeScreenShot(function) CommonUtil.previous_action_name = CommonUtil.current_action_name if _bypass_bug: CommonUtil.print_execlog = False @@ -2459,4 +2572,4 @@ def Action_Handler(_data_set, action_row, _bypass_bug=True): if any any passes if ay passes this-1199athis+-1,this+1 -''' \ No newline at end of file +''' diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index b13120e32..6652809f0 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -20,6 +20,7 @@ Author: Zeuz/AutomationSolutionz """ +import asyncio import sys import os import inspect @@ -27,8 +28,8 @@ import re from pathlib import Path -from playwright.sync_api import ( - sync_playwright, +from playwright.async_api import ( + async_playwright, Page, Browser, BrowserContext, @@ -44,6 +45,155 @@ ) from Framework.Utilities.CommonUtil import passed_tag_list, failed_tag_list from . import locator as PlaywrightLocator +from . import utils as PlaywrightUtils +from Framework.Built_In_Automation.Web.utils import ( + create_browser_session, + extract_session_name, + get_browser_session, + get_browser_sessions, + get_debug_port, + remove_browser_session, +) + +def _get_frame_locator(): + """Helper function to get current frame locator from shared variables.""" + try: + frame_locator = sr.Get_Shared_Variables("playwright_frame") + if frame_locator in failed_tag_list: + return None + return frame_locator + except: + # Variable doesn't exist yet + return None + + +def _set_active_playwright_session(session_name, session): + """Update module globals/shared variables for a selected Playwright session.""" + + global current_page, current_page_id, context, browser, playwright_instance + + current_page = session.get("playwright_page") + context = session.get("playwright_context") + browser = session.get("playwright_browser") + playwright_instance = session.get("playwright_instance") or playwright_instance + current_page_id = session_name + + sr.Set_Shared_Variables("playwright_page", current_page) + sr.Set_Shared_Variables("playwright_context", context) + sr.Set_Shared_Variables("playwright_browser", browser) + sr.Set_Shared_Variables("playwright_frame", session.get("playwright_frame")) + sr.Set_Shared_Variables("active_web_driver_type", "playwright") + if session.get("selenium_driver"): + sr.Set_Shared_Variables("selenium_driver", session["selenium_driver"]) + CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) + + +async def _ensure_playwright_session(session_name, existing_session): + """Activate an existing Playwright session or lazily attach to a Selenium one.""" + + global playwright_details + + if existing_session and existing_session.get("playwright_page"): + _set_active_playwright_session(session_name, existing_session) + return "passed" + + if not existing_session or not existing_session.get("selenium_driver"): + return "zeuz_failed" + + port = existing_session.get("remote_debugging_port") + if not port: + return "zeuz_failed" + + try: + from Framework.Built_In_Automation.Web.Selenium import BuiltInFunctions as SeleniumBuiltInFunctions + + playwright_instance, connected_browser, connected_context, connected_page = await SeleniumBuiltInFunctions.connect_playwright_to_selenium(port=port) + sessions = get_browser_sessions() + session = sessions.setdefault(session_name, existing_session) + session.update({ + "selenium_driver": existing_session.get("selenium_driver"), + "playwright_page": connected_page, + "playwright_browser": connected_browser, + "playwright_context": connected_context, + "playwright_frame": None, + "playwright_instance": playwright_instance, + "remote_debugging_port": port, + }) + sr.Set_Shared_Variables("browser_sessions", sessions) + playwright_details[session_name] = { + "page": connected_page, + "context": connected_context, + "browser": connected_browser, + "playwright": playwright_instance, + "remote-debugging-port": port, + } + _set_active_playwright_session(session_name, session) + CommonUtil.ExecLog("_ensure_playwright_session", f"Connected Playwright to Selenium session: {session_name}", 1) + return "passed" + except Exception as e: + CommonUtil.ExecLog("_ensure_playwright_session", f"Failed to connect Playwright to Selenium session '{session_name}': {e}", 3) + return "zeuz_failed" + + +def _save_current_playwright_frame(frame_locator): + if current_page_id: + sessions = get_browser_sessions() + if current_page_id in sessions: + sessions[current_page_id]["playwright_frame"] = frame_locator + sr.Set_Shared_Variables("browser_sessions", sessions) + + +async def _activate_browser_session_for_action(step_data, function_name=None): + """Select the requested browser session before running Playwright actions.""" + + session_name = extract_session_name(step_data) + create_or_cleanup_actions = { + "Open_Browser", + "Go_To_Link", + "Tear_Down_Playwright", + } + if function_name in create_or_cleanup_actions: + return "passed" + + if not session_name: + if current_page is None: + default_session = get_browser_session("default") + if default_session and default_session.get("selenium_driver"): + return await _ensure_playwright_session("default", default_session) + return "passed" + + existing_session = get_browser_session(session_name) + result = await _ensure_playwright_session(session_name, existing_session) + if result in failed_tag_list: + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + CommonUtil.ExecLog(sModuleInfo, f"Browser session '{session_name}' not found", 3) + return "zeuz_failed" + + return "passed" + + +def connect_selenium_to_playwright(port=9222): + """Connect Selenium to Playwright browser via CDP""" + try: + from selenium import webdriver + from selenium.webdriver.chrome.options import Options + + options = Options() + options.add_experimental_option("debuggerAddress", f"127.0.0.1:{port}") + + driver = webdriver.Chrome(options=options) + + from Framework.Built_In_Automation.Web.Selenium import BuiltInFunctions as SeleniumBuiltInFunctions + SeleniumBuiltInFunctions.selenium_driver = driver + + sr.Set_Shared_Variables("selenium_driver", driver) + + CommonUtil.ExecLog("connect_selenium_to_playwright", "Connected Selenium to Playwright", 1) + return driver + + except Exception as e: + CommonUtil.ExecLog("connect_selenium_to_playwright", f"Failed to connect Selenium to Playwright: {e}", 3) + return "zeuz_failed" ######################### # # @@ -74,8 +224,46 @@ # # ######################### +async def _handle_playwright_session(step_data): + """ + Helper function to handle session parameter for Playwright actions. + + Args: + step_data: The step data containing potential session parameter + + Returns: + tuple: (session_name, current_page, current_page_id, context, browser) + - session_name: The session name found or None + - current_page: The appropriate page instance + - current_page_id: The current page ID + - context: The browser context + - browser: The browser instance + """ + global current_page, current_page_id, context, browser + + session_name = extract_session_name(step_data) + + # If session parameter is provided, switch to that session + if session_name: + existing_session = get_browser_session(session_name) + + if existing_session and await _ensure_playwright_session(session_name, existing_session) not in failed_tag_list: + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + CommonUtil.ExecLog(sModuleInfo, f"Using existing browser session: {session_name}", 1) + else: + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + CommonUtil.ExecLog(sModuleInfo, f"Browser session '{session_name}' not found", 3) + raise ValueError(f"Browser session '{session_name}' not found") + elif current_page is None: + default_session = get_browser_session("default") + if default_session and default_session.get("selenium_driver"): + await _ensure_playwright_session("default", default_session) + + return session_name, current_page, current_page_id, context, browser + + @logger -def Open_Browser(step_data): +async def Open_Browser(step_data): """ Launch a new browser instance with Playwright. @@ -104,7 +292,7 @@ def Open_Browser(step_data): # Parse parameters url = None browser_name = "chromium" - headless = True + headless = False viewport = default_viewport.copy() args = [] timeout = default_timeout @@ -130,7 +318,7 @@ def Open_Browser(step_data): url = right_v elif left_l in ("browser", "browser name"): browser_name = right_v.lower() - elif left_l in ("driver id", "page id", "driver tag"): + elif left_l in ("driver id", "page id", "driver tag", "session"): page_id = right_v elif mid_l == "optional parameter": @@ -161,42 +349,59 @@ def Open_Browser(step_data): color_scheme = right_v elif left_l == "permission": permissions.append(right_v) + elif left_l in ("driver id", "page id", "driver tag", "session"): + page_id = right_v elif mid_l == "shared capability": # Handle Selenium-style capabilities where possible pass + # Ensure Playwright's managed browser is available in Zeuz's persistent cache. + success = PlaywrightUtils.ensure_playwright_browser_installed(sModuleInfo, browser_name) + if not success: + return "zeuz_failed" + # Launch Playwright CommonUtil.ExecLog(sModuleInfo, f"Launching Playwright with {browser_name} browser", 1) - playwright_instance = sync_playwright().start() + playwright_instance = await async_playwright().start() # Browser launch options launch_options = { "headless": headless, "slow_mo": slow_mo, - "devtools": devtools, } - if args: - launch_options["args"] = args + + # Add remote debugging port for CDP connection with unique port per session + unique_port = get_debug_port(page_id) + all_args = args + [f"--remote-debugging-port={unique_port}"] + if devtools: + all_args.append("--auto-open-devtools-for-tabs") + CommonUtil.ExecLog(sModuleInfo, f"Using remote debugging port {unique_port} for session '{page_id}'", 1) + if all_args: + launch_options["args"] = all_args if downloads_path: launch_options["downloads_path"] = downloads_path + + selenium_cdp_supported = True # Select and launch browser if browser_name in ("chrome", "chromium"): - browser = playwright_instance.chromium.launch(**launch_options) + browser = await playwright_instance.chromium.launch(**launch_options) elif browser_name == "firefox": - browser = playwright_instance.firefox.launch(**launch_options) + selenium_cdp_supported = False + browser = await playwright_instance.firefox.launch(**launch_options) elif browser_name in ("webkit", "safari"): - browser = playwright_instance.webkit.launch(**launch_options) + selenium_cdp_supported = False + browser = await playwright_instance.webkit.launch(**launch_options) elif browser_name in ("edge", "msedge", "microsoft edge"): launch_options["channel"] = "msedge" - browser = playwright_instance.chromium.launch(**launch_options) + browser = await playwright_instance.chromium.launch(**launch_options) elif browser_name == "chrome-beta": launch_options["channel"] = "chrome-beta" - browser = playwright_instance.chromium.launch(**launch_options) + browser = await playwright_instance.chromium.launch(**launch_options) else: CommonUtil.ExecLog(sModuleInfo, f"Unknown browser '{browser_name}', using chromium", 2) - browser = playwright_instance.chromium.launch(**launch_options) + browser = await playwright_instance.chromium.launch(**launch_options) # Context options context_options = {"viewport": viewport} @@ -214,9 +419,9 @@ def Open_Browser(step_data): context_options["color_scheme"] = color_scheme # Create context and page - context = browser.new_context(**context_options) + context = await browser.new_context(**context_options) context.set_default_timeout(timeout) - current_page = context.new_page() + current_page = await context.new_page() current_page_id = page_id # Store in details @@ -225,11 +430,12 @@ def Open_Browser(step_data): "context": context, "browser": browser, "playwright": playwright_instance, + "remote-debugging-port": unique_port, } # Navigate if URL provided if url: - current_page.goto(url, wait_until="domcontentloaded") + await current_page.goto(url, wait_until="domcontentloaded") CommonUtil.ExecLog(sModuleInfo, f"Navigated to: {url}", 1) # Save to shared variables for compatibility @@ -237,6 +443,25 @@ def Open_Browser(step_data): sr.Set_Shared_Variables("playwright_context", context) sr.Set_Shared_Variables("playwright_browser", browser) sr.Set_Shared_Variables("element_wait", timeout / 1000) # In seconds + sr.Set_Shared_Variables("active_web_driver_type", "playwright") + + # Set screenshot variables for CommonUtil.TakeScreenShot() + CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) + + # Create browser session + session = create_browser_session( + session_name=page_id, + selenium_driver=None, + playwright_page=current_page, + playwright_browser=browser, + playwright_context=context, + playwright_frame=None, + playwright_instance=playwright_instance, + remote_debugging_port=unique_port, + ) + session["selenium_cdp_supported"] = selenium_cdp_supported + sr.Set_Shared_Variables("browser_sessions", get_browser_sessions()) + CommonUtil.ExecLog(sModuleInfo, f"Created browser session: {page_id}", 5) CommonUtil.ExecLog(sModuleInfo, f"Browser opened successfully (page_id: {page_id})", 1) return "passed" @@ -246,7 +471,7 @@ def Open_Browser(step_data): @logger -def Go_To_Link(step_data): +async def Go_To_Link(step_data): """ Navigate to a URL. @@ -262,9 +487,50 @@ def Go_To_Link(step_data): global current_page try: - if current_page is None: - CommonUtil.ExecLog(sModuleInfo, "No browser open. Use 'open browser' first.", 3) - return "zeuz_failed" + # Parse session parameter first + session_name = None + for left, mid, right in step_data: + left_l = left.strip().lower() + mid_l = mid.strip().lower() + right_v = right.strip() + + if mid_l == "optional parameter" and left_l == "session": + session_name = right_v + break + + # Check if session exists and use it + if session_name: + existing_session = get_browser_session(session_name) + + if existing_session and await _ensure_playwright_session(session_name, existing_session) not in failed_tag_list: + CommonUtil.ExecLog(sModuleInfo, f"Using existing browser session: {session_name}", 1) + else: + # Session doesn't exist, open new browser with session name + CommonUtil.ExecLog(sModuleInfo, f"Session '{session_name}' not found. Opening new browser.", 2) + + # Add session parameter to step_data for Open_Browser + step_data_with_session = step_data.copy() + if not any(left.strip().lower() == "session" and mid.strip().lower() == "optional parameter" for left, mid, right in step_data_with_session): + step_data_with_session.append(("session", "optional parameter", session_name)) + + result = await Open_Browser(step_data_with_session) + if result == "zeuz_failed": + CommonUtil.ExecLog(sModuleInfo, "Failed to open browser for new session", 3) + return "zeuz_failed" + + elif current_page is None: + default_session = get_browser_session("default") + if default_session and default_session.get("selenium_driver"): + result = await _ensure_playwright_session("default", default_session) + if result in failed_tag_list: + return result + else: + # No session specified and no browser open + CommonUtil.ExecLog(sModuleInfo, "No browser open. Opening browser with default settings.", 2) + result = await Open_Browser(step_data) + if result == "zeuz_failed": + CommonUtil.ExecLog(sModuleInfo, "Failed to open browser automatically", 3) + return "zeuz_failed" url = None wait_until = "domcontentloaded" @@ -275,11 +541,13 @@ def Go_To_Link(step_data): mid_l = mid.strip().lower() right_v = right.strip() - if mid_l == "input parameter": - if left_l in ("go to link", "url", "link"): + if left_l in ("go to link", "url", "link"): url = right_v elif mid_l == "optional parameter": - if left_l in ("wait until", "wait_until", "waituntil"): + if left_l == "session": + # Skip session parameter - already processed above + continue + elif left_l in ("wait until", "wait_until", "waituntil", "wait time"): wait_until = right_v.lower() elif left_l == "timeout": timeout = int(float(right_v) * 1000) @@ -292,7 +560,12 @@ def Go_To_Link(step_data): if timeout: goto_options["timeout"] = timeout - current_page.goto(url, **goto_options) + await current_page.goto(url, **goto_options) + + # Reset frame context when navigating to a new URL + sr.Set_Shared_Variables("playwright_frame", None) + _save_current_playwright_frame(None) + CommonUtil.ExecLog(sModuleInfo, f"Navigated to: {url}", 1) return "passed" @@ -301,63 +574,170 @@ def Go_To_Link(step_data): @logger -def Tear_Down_Playwright(step_data=None): +async def Tear_Down_Playwright(step_data=None): """ Close browser and clean up Playwright resources. Example: Field Sub Field Value tear down playwright action tear down + + Example with session: + Field Sub Field Value + session optional parameter my_session + tear down playwright action tear down """ sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME global playwright_instance, browser, context, current_page global playwright_details, current_page_id try: - # Close all tracked pages/contexts - for page_id, details in playwright_details.items(): + # Parse session parameter + session_name = None + if step_data: + for left, mid, right in step_data: + left_l = left.strip().lower() + mid_l = mid.strip().lower() + right_v = right.strip() + + if mid_l == "optional parameter" and left_l == "session": + session_name = right_v + break + + # Handle session-specific teardown + if session_name: + existing_session = get_browser_session(session_name) + + if existing_session and existing_session.get("playwright_page"): + try: + # Close the specific session's page and context + session_page = existing_session["playwright_page"] + session_context = existing_session["playwright_context"] + session_browser = existing_session["playwright_browser"] + session_playwright = existing_session.get("playwright_instance") + session_selenium = existing_session.get("selenium_driver") + + if session_page: + await session_page.close() + if session_context: + await session_context.close() + if session_browser: + await session_browser.close() + if session_playwright: + await session_playwright.stop() + if session_selenium and session_selenium != "zeuz_failed": + try: + session_selenium.quit() + except Exception: + pass + + CommonUtil.ExecLog(sModuleInfo, f"Teared down session '{session_name}'", 1) + except Exception as e: + errMsg = f"Unable to tear down session '{session_name}'. may already been killed" + CommonUtil.ExecLog(sModuleInfo, errMsg, 2) + + remove_browser_session(session_name) + + # Remove from playwright_details if present + if session_name in playwright_details: + del playwright_details[session_name] + + # If this was the current session, clear globals + if current_page_id == session_name: + current_page = None + context = None + browser = None + current_page_id = None + + # Try to switch to another available session + if playwright_details: + for page_id, details in playwright_details.items(): + current_page = details["page"] + context = details["context"] + browser = details["browser"] + current_page_id = page_id + + # Update shared variables + sr.Set_Shared_Variables("playwright_page", current_page) + sr.Set_Shared_Variables("playwright_context", context) + sr.Set_Shared_Variables("playwright_browser", browser) + + CommonUtil.ExecLog(sModuleInfo, f"Switched to session '{page_id}'", 1) + break + else: + CommonUtil.ExecLog(sModuleInfo, f"Session '{session_name}' not found. Nothing to tear down.", 2) + return "passed" + + # Handle full teardown (backwards compatibility) + else: + for session in get_browser_sessions().values(): + if not (isinstance(session, dict) and session.get("playwright_page")): + continue + try: + if session.get("selenium_driver") and session.get("selenium_driver") != "zeuz_failed": + session["selenium_driver"].quit() + except Exception: + pass + + # Close all tracked pages/contexts + for page_id, details in list(playwright_details.items()): + try: + if details.get("page"): + await details["page"].close() + if details.get("context"): + await details["context"].close() + if details.get("browser"): + await details["browser"].close() + if details.get("playwright"): + await details["playwright"].stop() + except Exception: + pass + + # Close main instances try: - if details.get("page"): - details["page"].close() - if details.get("context"): - details["context"].close() + if current_page and current_page not in [d.get("page") for d in playwright_details.values()]: + await current_page.close() except Exception: pass - # Close main instances - try: - if current_page and current_page not in [d.get("page") for d in playwright_details.values()]: - current_page.close() - except Exception: - pass + try: + if context: + await context.close() + except Exception: + pass - try: - if context: - context.close() - except Exception: - pass + try: + if browser: + await browser.close() + except Exception: + pass - try: - if browser: - browser.close() - except Exception: - pass + try: + if playwright_instance: + await playwright_instance.stop() + except Exception: + pass - try: - if playwright_instance: - playwright_instance.stop() - except Exception: - pass + # Reset all globals + current_page = None + context = None + browser = None + playwright_instance = None + playwright_details = {} + current_page_id = None + + # Clear Playwright-backed browser sessions without discarding Selenium-only sessions. + sessions = get_browser_sessions() + sessions = { + name: session + for name, session in sessions.items() + if not (isinstance(session, dict) and session.get("playwright_page")) + } + sr.Set_Shared_Variables("browser_sessions", sessions) - # Reset all globals - current_page = None - context = None - browser = None - playwright_instance = None - playwright_details = {} - current_page_id = None + CommonUtil.ExecLog(sModuleInfo, "Browser closed successfully", 1) + return "passed" - CommonUtil.ExecLog(sModuleInfo, "Browser closed successfully", 1) return "passed" except Exception: @@ -365,7 +745,7 @@ def Tear_Down_Playwright(step_data=None): @logger -def Switch_Browser(step_data): +async def Switch_Browser(step_data): """ Switch between multiple browser instances/pages. @@ -375,7 +755,7 @@ def Switch_Browser(step_data): switch browser playwright action switch browser """ sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME - global current_page, current_page_id, context + global current_page, current_page_id, context, browser try: target_id = None @@ -385,14 +765,22 @@ def Switch_Browser(step_data): mid_l = mid.strip().lower() right_v = right.strip() - if mid_l == "input parameter": - if left_l in ("driver id", "page id", "driver tag"): + if mid_l in ("input parameter", "optional parameter"): + if left_l in ("driver id", "page id", "driver tag", "session"): target_id = right_v if not target_id: CommonUtil.ExecLog(sModuleInfo, "No driver/page ID provided", 3) return "zeuz_failed" + existing_session = get_browser_session(target_id) + if existing_session and existing_session.get("playwright_page"): + _set_active_playwright_session(target_id, existing_session) + if current_page: + await current_page.bring_to_front() + CommonUtil.ExecLog(sModuleInfo, f"Switched to page: {target_id}", 1) + return "passed" + if target_id not in playwright_details: CommonUtil.ExecLog(sModuleInfo, f"Page ID '{target_id}' not found", 3) return "zeuz_failed" @@ -400,12 +788,17 @@ def Switch_Browser(step_data): details = playwright_details[target_id] current_page = details["page"] context = details["context"] + browser = details["browser"] current_page_id = target_id - current_page.bring_to_front() + await current_page.bring_to_front() sr.Set_Shared_Variables("playwright_page", current_page) sr.Set_Shared_Variables("playwright_context", context) + sr.Set_Shared_Variables("playwright_browser", browser) + + # Set screenshot variables for CommonUtil.TakeScreenShot() + CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) CommonUtil.ExecLog(sModuleInfo, f"Switched to page: {target_id}", 1) return "passed" @@ -421,7 +814,7 @@ def Switch_Browser(step_data): ######################### @logger -def Click_Element(step_data): +async def Click_Element(step_data): """ Click an element. @@ -451,6 +844,9 @@ def Click_Element(step_data): global current_page try: + # Handle session parameter + session_name, current_page, current_page_id, context, browser = await _handle_playwright_session(step_data) + if current_page is None: CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) return "zeuz_failed" @@ -470,6 +866,10 @@ def Click_Element(step_data): mid_l = mid.strip().lower() right_v = right.strip() + # Skip session parameter - already handled above + if mid_l == "optional parameter" and left_l == "session": + continue + if mid_l == "optional parameter": if left_l == "use js": use_js = right_v.lower() in ("true", "yes", "1") @@ -492,7 +892,7 @@ def Click_Element(step_data): right_click = True # Get element - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" @@ -514,14 +914,14 @@ def Click_Element(step_data): # Perform click if double_click: - locator.dblclick(**{k: v for k, v in click_options.items() if k != "click_count"}) + await locator.dblclick(**{k: v for k, v in click_options.items() if k != "click_count"}) CommonUtil.ExecLog(sModuleInfo, "Double click performed", 1) elif right_click: click_options["button"] = "right" - locator.click(**click_options) + await locator.click(**click_options) CommonUtil.ExecLog(sModuleInfo, "Right click performed", 1) else: - locator.click(**click_options) + await locator.click(**click_options) CommonUtil.ExecLog(sModuleInfo, "Click performed", 1) return "passed" @@ -531,7 +931,7 @@ def Click_Element(step_data): @logger -def Double_Click_Element(step_data): +async def Double_Click_Element(step_data): """ Double-click an element. @@ -548,11 +948,11 @@ def Double_Click_Element(step_data): modified_step_data[i] = ("double click", mid, right) break - return Click_Element(modified_step_data) + return await Click_Element(modified_step_data) @logger -def Right_Click_Element(step_data): +async def Right_Click_Element(step_data): """ Right-click (context click) an element. @@ -567,11 +967,11 @@ def Right_Click_Element(step_data): modified_step_data[i] = ("right click", mid, right) break - return Click_Element(modified_step_data) + return await Click_Element(modified_step_data) @logger -def Hover_Over_Element(step_data): +async def Hover_Over_Element(step_data): """ Hover over an element. @@ -606,7 +1006,7 @@ def Hover_Over_Element(step_data): elif left_l == "timeout": timeout = int(float(right_v) * 1000) - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" @@ -619,7 +1019,7 @@ def Hover_Over_Element(step_data): if timeout: hover_options["timeout"] = timeout - locator.hover(**hover_options) + await locator.hover(**hover_options) CommonUtil.ExecLog(sModuleInfo, "Hover performed", 1) return "passed" @@ -634,7 +1034,7 @@ def Hover_Over_Element(step_data): ######################### @logger -def Enter_Text_In_Text_Box(step_data): +async def Enter_Text_In_Text_Box(step_data): """ Enter text in a text field. @@ -657,6 +1057,9 @@ def Enter_Text_In_Text_Box(step_data): global current_page try: + # Handle session parameter + session_name, current_page, current_page_id, context, browser = await _handle_playwright_session(step_data) + if current_page is None: CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) return "zeuz_failed" @@ -671,6 +1074,10 @@ def Enter_Text_In_Text_Box(step_data): left_l = left.strip().lower() mid_l = mid.strip().lower() + # Skip session parameter - already handled above + if mid_l == "optional parameter" and left_l == "session": + continue + if mid_l == "action": text_value = right # Don't strip - preserve whitespace elif mid_l == "optional parameter": @@ -683,7 +1090,7 @@ def Enter_Text_In_Text_Box(step_data): elif left_l == "timeout": timeout = int(float(right.strip()) * 1000) - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" @@ -691,17 +1098,17 @@ def Enter_Text_In_Text_Box(step_data): # Enter text based on options if use_js: # Use JavaScript to set value directly - locator.evaluate(f"el => {{ el.value = `{text_value}`; }}") + await locator.evaluate(f"el => {{ el.value = `{text_value}`; }}") # Trigger events - locator.dispatch_event("input") - locator.dispatch_event("change") + await locator.dispatch_event("input") + await locator.dispatch_event("change") CommonUtil.ExecLog(sModuleInfo, f"Text entered via JS: {text_value[:50]}{'...' if len(text_value) > 50 else ''}", 1) elif clear: # fill() clears and sets value - recommended approach fill_options = {} if timeout: fill_options["timeout"] = timeout - locator.fill(text_value, **fill_options) + await locator.fill(text_value, **fill_options) CommonUtil.ExecLog(sModuleInfo, f"Text filled: {text_value[:50]}{'...' if len(text_value) > 50 else ''}", 1) else: # type() appends to existing value @@ -710,7 +1117,7 @@ def Enter_Text_In_Text_Box(step_data): type_options["delay"] = int(delay * 1000) if timeout: type_options["timeout"] = timeout - locator.type(text_value, **type_options) + await locator.type(text_value, **type_options) CommonUtil.ExecLog(sModuleInfo, f"Text typed: {text_value[:50]}{'...' if len(text_value) > 50 else ''}", 1) return "passed" @@ -720,7 +1127,7 @@ def Enter_Text_In_Text_Box(step_data): @logger -def Keystroke_For_Element(step_data): +async def Keystroke_For_Element(step_data): """ Send keystrokes to an element or the page. @@ -827,18 +1234,18 @@ def Keystroke_For_Element(step_data): key = key_map.get(key, keystroke_value) if has_element: - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" for _ in range(key_count): - locator.press(key) + await locator.press(key) if delay > 0: time.sleep(delay) else: for _ in range(key_count): - current_page.keyboard.press(key) + await current_page.keyboard.press(key) if delay > 0: time.sleep(delay) @@ -850,13 +1257,13 @@ def Keystroke_For_Element(step_data): type_options["delay"] = int(delay * 1000) if has_element: - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" - locator.type(keystroke_value, **type_options) + await locator.type(keystroke_value, **type_options) else: - current_page.keyboard.type(keystroke_value, **type_options) + await current_page.keyboard.type(keystroke_value, **type_options) CommonUtil.ExecLog(sModuleInfo, f"Typed chars: {keystroke_value[:50]}{'...' if len(keystroke_value) > 50 else ''}", 1) @@ -873,7 +1280,7 @@ def Keystroke_For_Element(step_data): ######################### @logger -def Validate_Text(step_data): +async def Validate_Text(step_data): """ Validate that an element contains expected text. @@ -919,19 +1326,21 @@ def Validate_Text(step_data): case_insensitive = True elif left_l.startswith("*"): partial_match = True + elif "partial" in left_l: + partial_match = True expected_text = right_v elif mid_l == "optional parameter": if left_l == "timeout": timeout = int(float(right_v) * 1000) - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" # Get actual text - actual_text = locator.text_content() or "" + actual_text = await locator.text_content() or "" # Compare match = False @@ -962,7 +1371,7 @@ def Validate_Text(step_data): @logger -def if_element_exists(step_data): +async def if_element_exists(step_data): """ Check if an element exists on the page. @@ -990,14 +1399,14 @@ def if_element_exists(step_data): if mid_l == "optional parameter" and left_l == "timeout": timeout = int(float(right_v) * 1000) - locator = PlaywrightLocator.Get_Element(step_data, current_page, element_wait=timeout/1000) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, element_wait=timeout/1000, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element does not exist", 1) return "zeuz_failed" try: - count = locator.count() + count = await locator.count() if count > 0: CommonUtil.ExecLog(sModuleInfo, f"Element exists ({count} found)", 1) return "passed" @@ -1013,7 +1422,7 @@ def if_element_exists(step_data): @logger -def Save_Attribute(step_data): +async def Save_Attribute(step_data): """ Save an element's attribute value to a shared variable. @@ -1043,16 +1452,18 @@ def Save_Attribute(step_data): attribute_name = None save_variable = None + save_attribute = None for left, mid, right in step_data: left_l = left.strip().lower() mid_l = mid.strip().lower() right_v = right.strip() - if mid_l == "input parameter": + if mid_l in ["input parameter", "element parameter"]: attribute_name = left.strip() # Keep original case elif mid_l == "save parameter": - save_variable = left.strip() + save_variable = right_v + save_attribute = left_l if not attribute_name: CommonUtil.ExecLog(sModuleInfo, "No attribute name specified", 3) @@ -1062,7 +1473,7 @@ def Save_Attribute(step_data): CommonUtil.ExecLog(sModuleInfo, "No save variable specified", 3) return "zeuz_failed" - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" @@ -1070,30 +1481,30 @@ def Save_Attribute(step_data): # Get attribute value based on name attr_lower = attribute_name.lower() if attr_lower == "text": - value = locator.text_content() + value = await locator.text_content() elif attr_lower == "innertext": - value = locator.inner_text() + value = await locator.inner_text() elif attr_lower == "innerhtml": - value = locator.inner_html() + value = await locator.inner_html() elif attr_lower == "outerhtml": - value = locator.evaluate("el => el.outerHTML") + value = await locator.evaluate("el => el.outerHTML") elif attr_lower == "value": - value = locator.input_value() + value = await locator.input_value() elif attr_lower == "checked": - value = locator.is_checked() + value = await locator.is_checked() elif attr_lower == "selected": - value = locator.evaluate("el => el.selected") + value = await locator.evaluate("el => el.selected") elif attr_lower == "visible": - value = locator.is_visible() + value = await locator.is_visible() elif attr_lower == "enabled": - value = locator.is_enabled() + value = await locator.is_enabled() elif attr_lower == "disabled": - value = locator.is_disabled() + value = await locator.is_disabled() else: - value = locator.get_attribute(attribute_name) + value = await locator.get_attribute(save_attribute) sr.Set_Shared_Variables(save_variable, value) - CommonUtil.ExecLog(sModuleInfo, f"Saved '{attribute_name}' = '{value}' to '{save_variable}'", 1) + CommonUtil.ExecLog(sModuleInfo, f"Saved '{save_attribute}' = '{value}' to '{save_variable}'", 1) return "passed" except Exception: @@ -1101,7 +1512,7 @@ def Save_Attribute(step_data): @logger -def get_element_info(step_data): +async def get_element_info(step_data): """ Get detailed information about an element. @@ -1124,23 +1535,23 @@ def get_element_info(step_data): if mid.strip().lower() == "save parameter": save_variable = left.strip() - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" # Gather element info info = { - "tag_name": locator.evaluate("el => el.tagName"), - "text": locator.text_content(), - "inner_html": locator.inner_html(), - "visible": locator.is_visible(), - "enabled": locator.is_enabled(), - "bounding_box": locator.bounding_box(), + "tag_name": await locator.evaluate("el => el.tagName"), + "text": await locator.text_content(), + "inner_html": await locator.inner_html(), + "visible": await locator.is_visible(), + "enabled": await locator.is_enabled(), + "bounding_box": await locator.bounding_box(), } # Get all attributes - attributes = locator.evaluate("""el => { + attributes = await locator.evaluate("""el => { const attrs = {}; for (const attr of el.attributes) { attrs[attr.name] = attr.value; @@ -1168,7 +1579,7 @@ def get_element_info(step_data): ######################### @logger -def Navigate(step_data): +async def Navigate(step_data): """ Navigate browser (back, forward, refresh). @@ -1206,13 +1617,13 @@ def Navigate(step_data): nav_options["timeout"] = timeout if direction in ("back", "go back"): - current_page.go_back(**nav_options) + await current_page.go_back(**nav_options) CommonUtil.ExecLog(sModuleInfo, "Navigated back", 1) elif direction in ("forward", "go forward"): - current_page.go_forward(**nav_options) + await current_page.go_forward(**nav_options) CommonUtil.ExecLog(sModuleInfo, "Navigated forward", 1) elif direction in ("refresh", "reload"): - current_page.reload(**nav_options) + await current_page.reload(**nav_options) CommonUtil.ExecLog(sModuleInfo, "Page reloaded", 1) else: CommonUtil.ExecLog(sModuleInfo, f"Unknown navigation direction: {direction}", 3) @@ -1268,7 +1679,7 @@ def Get_Current_URL(step_data): ######################### @logger -def Scroll(step_data): +async def Scroll(step_data): """ Scroll the page in a direction. @@ -1315,7 +1726,7 @@ def Scroll(step_data): elif direction == "left": delta_x = -pixels - current_page.mouse.wheel(delta_x, delta_y) + await current_page.mouse.wheel(delta_x, delta_y) CommonUtil.ExecLog(sModuleInfo, f"Scrolled {direction} by {pixels}px", 1) return "passed" @@ -1324,7 +1735,7 @@ def Scroll(step_data): @logger -def scroll_to_element(step_data): +async def scroll_to_element(step_data): """ Scroll an element into view. @@ -1356,15 +1767,15 @@ def scroll_to_element(step_data): elif left_l == "align to top": align_to_top = right_v.lower() in ("true", "yes", "1") - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" if use_js: - locator.evaluate(f"el => el.scrollIntoView({str(align_to_top).lower()})") + await locator.evaluate(f"el => el.scrollIntoView({str(align_to_top).lower()})") else: - locator.scroll_into_view_if_needed() + await locator.scroll_into_view_if_needed() CommonUtil.ExecLog(sModuleInfo, "Scrolled element into view", 1) return "passed" @@ -1380,7 +1791,7 @@ def scroll_to_element(step_data): ######################### @logger -def Select_Deselect(step_data): +async def Select_Deselect(step_data): """ Select or deselect options in a dropdown/select element. @@ -1436,7 +1847,7 @@ def Select_Deselect(step_data): CommonUtil.ExecLog(sModuleInfo, "No selection value provided", 3) return "zeuz_failed" - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" @@ -1451,7 +1862,7 @@ def Select_Deselect(step_data): if is_deselect: # Playwright doesn't have direct deselect, use JavaScript - locator.evaluate(f"""el => {{ + await locator.evaluate(f"""el => {{ for (const opt of el.options) {{ if (opt.{'value' if select_type == 'value' else 'text'} === '{select_value}') {{ opt.selected = false; @@ -1461,7 +1872,7 @@ def Select_Deselect(step_data): }}""") CommonUtil.ExecLog(sModuleInfo, f"Deselected: {select_value}", 1) else: - locator.select_option(**option) + await locator.select_option(**option) CommonUtil.ExecLog(sModuleInfo, f"Selected: {select_value}", 1) return "passed" @@ -1477,7 +1888,7 @@ def Select_Deselect(step_data): ######################### @logger -def check_uncheck(step_data): +async def check_uncheck(step_data): """ Check or uncheck a checkbox/radio button. @@ -1514,7 +1925,7 @@ def check_uncheck(step_data): if left_l == "use js": use_js = right_v in ("true", "yes", "1") - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" @@ -1524,10 +1935,10 @@ def check_uncheck(step_data): options["force"] = True if action == "check": - locator.check(**options) + await locator.check(**options) CommonUtil.ExecLog(sModuleInfo, "Checkbox checked", 1) else: - locator.uncheck(**options) + await locator.uncheck(**options) CommonUtil.ExecLog(sModuleInfo, "Checkbox unchecked", 1) return "passed" @@ -1543,7 +1954,7 @@ def check_uncheck(step_data): ######################### @logger -def switch_window_or_tab(step_data): +async def switch_window_or_tab(step_data): """ Switch to a different window/tab. @@ -1592,19 +2003,21 @@ def switch_window_or_tab(step_data): if switch_by_title: for page in pages: - page_title = page.title() + page_title = await page.title() if partial_match: if switch_by_title.lower() in page_title.lower(): current_page = page - page.bring_to_front() + await page.bring_to_front() sr.Set_Shared_Variables("playwright_page", current_page) + CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) CommonUtil.ExecLog(sModuleInfo, f"Switched to tab: {page_title}", 1) return "passed" else: if switch_by_title.lower() == page_title.lower(): current_page = page - page.bring_to_front() + await page.bring_to_front() sr.Set_Shared_Variables("playwright_page", current_page) + CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) CommonUtil.ExecLog(sModuleInfo, f"Switched to tab: {page_title}", 1) return "passed" @@ -1614,9 +2027,10 @@ def switch_window_or_tab(step_data): elif switch_by_index is not None: if 0 <= switch_by_index < len(pages): current_page = pages[switch_by_index] - current_page.bring_to_front() + await current_page.bring_to_front() sr.Set_Shared_Variables("playwright_page", current_page) - CommonUtil.ExecLog(sModuleInfo, f"Switched to tab index {switch_by_index}: {current_page.title()}", 1) + CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) + CommonUtil.ExecLog(sModuleInfo, f"Switched to tab index {switch_by_index}: {await current_page.title()}", 1) return "passed" else: CommonUtil.ExecLog(sModuleInfo, f"Invalid tab index: {switch_by_index}", 3) @@ -1630,7 +2044,7 @@ def switch_window_or_tab(step_data): @logger -def open_new_tab(step_data): +async def open_new_tab(step_data): """ Open a new browser tab. @@ -1656,12 +2070,18 @@ def open_new_tab(step_data): if mid_l == "input parameter" and left_l in ("url", "link", "go to link"): url = right_v - new_page = context.new_page() + new_page = await context.new_page() current_page = new_page sr.Set_Shared_Variables("playwright_page", current_page) + if current_page_id: + sessions = get_browser_sessions() + if current_page_id in sessions: + sessions[current_page_id]["playwright_page"] = current_page + sr.Set_Shared_Variables("browser_sessions", sessions) + CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) if url: - new_page.goto(url) + await new_page.goto(url) CommonUtil.ExecLog(sModuleInfo, f"Opened new tab with URL: {url}", 1) else: CommonUtil.ExecLog(sModuleInfo, "Opened new blank tab", 1) @@ -1673,7 +2093,7 @@ def open_new_tab(step_data): @logger -def close_tab(step_data): +async def close_tab(step_data): """ Close a browser tab. @@ -1712,8 +2132,8 @@ def close_tab(step_data): if tab_title: for page in pages: - if tab_title.lower() in page.title().lower(): - page.close() + if tab_title.lower() in (await page.title()).lower(): + await page.close() CommonUtil.ExecLog(sModuleInfo, f"Closed tab: {tab_title}", 1) break else: @@ -1721,7 +2141,7 @@ def close_tab(step_data): return "zeuz_failed" elif tab_index is not None: if 0 <= tab_index < len(pages): - pages[tab_index].close() + await pages[tab_index].close() CommonUtil.ExecLog(sModuleInfo, f"Closed tab at index {tab_index}", 1) else: CommonUtil.ExecLog(sModuleInfo, f"Invalid tab index: {tab_index}", 3) @@ -1729,7 +2149,7 @@ def close_tab(step_data): else: # Close current tab if current_page: - current_page.close() + await current_page.close() CommonUtil.ExecLog(sModuleInfo, "Closed current tab", 1) # Switch to remaining tab if available @@ -1737,6 +2157,12 @@ def close_tab(step_data): if pages: current_page = pages[-1] sr.Set_Shared_Variables("playwright_page", current_page) + if current_page_id: + sessions = get_browser_sessions() + if current_page_id in sessions: + sessions[current_page_id]["playwright_page"] = current_page + sr.Set_Shared_Variables("browser_sessions", sessions) + CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) return "passed" @@ -1751,7 +2177,7 @@ def close_tab(step_data): ######################### @logger -def switch_iframe(step_data): +async def switch_iframe(step_data): """ Switch to an iframe or back to main content. @@ -1778,41 +2204,82 @@ def switch_iframe(step_data): CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) return "zeuz_failed" - iframe_index = None - iframe_selector = None + default_aliases = ("default content", "default", "main") + frame_targets = [] switch_to_default = False for left, mid, right in step_data: left_l = left.strip().lower() mid_l = mid.strip().lower() right_v = right.strip() - - if mid_l in ("iframe parameter", "frame parameter"): - iframe_selector = f"[{left}='{right_v}']" if left_l not in ("tag",) else right_v - elif mid_l == "input parameter": - if left_l == "index": - if right_v.lower() in ("default content", "default", "main"): - switch_to_default = True - else: - iframe_index = int(right_v) + right_l = right_v.lower() + + if "action" in mid_l and left_l == "switch iframe": + continue + + if left_l == "index" and right_l in default_aliases: + switch_to_default = True + continue + + if mid_l not in ("iframe parameter", "frame parameter", "input parameter"): + continue + + if left_l == "index": + frame_targets.append({"kind": "index", "mid": mid_l, "right": right_v}) + elif mid_l in ("iframe parameter", "frame parameter"): + frame_targets.append( + { + "kind": "selector", + "mid": mid_l, + "left": left_l, + "right": right_v, + } + ) if switch_to_default: # In Playwright, we work with the main page directly # Store a flag or reset frame locator + sr.Set_Shared_Variables("playwright_frame", None) + _save_current_playwright_frame(None) CommonUtil.ExecLog(sModuleInfo, "Switched to default content", 1) - return "passed" + if not frame_targets: + return "passed" - # Build frame locator - if iframe_selector: - frame_locator = current_page.frame_locator(iframe_selector) - elif iframe_index is not None: - frame_locator = current_page.frame_locator(f"iframe >> nth={iframe_index}") - else: + if not frame_targets: CommonUtil.ExecLog(sModuleInfo, "No iframe selector or index provided", 3) return "zeuz_failed" + frame_locator = None + for target in frame_targets: + base = frame_locator if frame_locator else current_page + tag_name = "frame" if target["mid"] == "frame parameter" else "iframe" + + if target["kind"] == "index": + try: + iframe_index = int(target["right"]) + except Exception: + CommonUtil.ExecLog( + sModuleInfo, + "Invalid %s index '%s'" % (tag_name, target["right"]), + 3, + ) + return "zeuz_failed" + frame_locator = base.frame_locator(tag_name).nth(iframe_index) + continue + + left_l = target["left"] + right_v = target["right"] + if left_l == "tag": + iframe_selector = right_v + elif left_l == "xpath": + iframe_selector = right_v if right_v.startswith("xpath=") else f"xpath={right_v}" + else: + iframe_selector = f"{tag_name}[{left_l}='{right_v}']" + frame_locator = base.frame_locator(iframe_selector) + # Store frame locator for subsequent actions sr.Set_Shared_Variables("playwright_frame", frame_locator) + _save_current_playwright_frame(frame_locator) CommonUtil.ExecLog(sModuleInfo, "Switched to iframe", 1) return "passed" @@ -1827,7 +2294,7 @@ def switch_iframe(step_data): ######################### @logger -def Handle_Browser_Alert(step_data): +async def Handle_Browser_Alert(step_data): """ Handle browser alerts/dialogs. @@ -1882,45 +2349,30 @@ def Handle_Browser_Alert(step_data): if left_l in ("timeout", "wait"): timeout = int(float(right_v) * 1000) - # Set up dialog handler - dialog_info = {"message": None, "handled": False} - - def handle_dialog(dialog): - dialog_info["message"] = dialog.message - dialog_info["type"] = dialog.type - - if action in ("accept", "ok", "yes"): - if prompt_text: - dialog.accept(prompt_text) - else: - dialog.accept() - elif action in ("dismiss", "cancel", "no"): - dialog.dismiss() - else: - dialog.accept() - - dialog_info["handled"] = True - - current_page.on("dialog", handle_dialog) - - # Wait for dialog try: - current_page.wait_for_event("dialog", timeout=timeout) + dialog = await current_page.wait_for_event("dialog", timeout=timeout) except PlaywrightTimeoutError: CommonUtil.ExecLog(sModuleInfo, "No alert appeared within timeout", 2) - current_page.remove_listener("dialog", handle_dialog) return "passed" # Not necessarily a failure - # Remove listener - current_page.remove_listener("dialog", handle_dialog) + message = dialog.message + if action in ("accept", "ok", "yes"): + if prompt_text: + await dialog.accept(prompt_text) + else: + await dialog.accept() + elif action in ("dismiss", "cancel", "no"): + await dialog.dismiss() + else: + await dialog.accept() # Save text if requested - if save_variable and dialog_info["message"]: - sr.Set_Shared_Variables(save_variable, dialog_info["message"]) + if save_variable and message: + sr.Set_Shared_Variables(save_variable, message) CommonUtil.ExecLog( sModuleInfo, - f"Alert handled ({action}): {dialog_info.get('message', 'N/A')}", + f"Alert handled ({action}): {message or 'N/A'}", 1 ) return "passed" @@ -1936,7 +2388,7 @@ def handle_dialog(dialog): ######################### @logger -def drag_and_drop(step_data): +async def drag_and_drop(step_data): """ Drag and drop an element to a target. @@ -1955,33 +2407,31 @@ def drag_and_drop(step_data): return "zeuz_failed" # Separate source and target parameters - source_params = [] - target_params = [] + source_param = None + target_param = None for left, mid, right in step_data: mid_l = mid.strip().lower() - if mid_l == "target parameter": - target_params.append((left, "element parameter", right)) - elif mid_l == "element parameter": - source_params.append((left, mid, right)) - else: - source_params.append((left, mid, right)) - target_params.append((left, mid, right)) + if "element parameter" in mid_l: + if mid_l.startswith("dst"): + target_param = (left, mid, right) + elif mid_l.startswith("src"): + source_param = (left, mid, right) # Get source element - source_locator = PlaywrightLocator.Get_Element(source_params, current_page) + source_locator = await PlaywrightLocator.Get_Element([source_param], current_page, frame_locator=_get_frame_locator()) if source_locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Source element not found", 3) return "zeuz_failed" # Get target element - target_locator = PlaywrightLocator.Get_Element(target_params, current_page) + target_locator = await PlaywrightLocator.Get_Element([target_param], current_page, frame_locator=_get_frame_locator()) if target_locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Target element not found", 3) return "zeuz_failed" # Perform drag and drop - source_locator.drag_to(target_locator) + await source_locator.drag_to(target_locator) CommonUtil.ExecLog(sModuleInfo, "Drag and drop completed", 1) return "passed" @@ -1996,7 +2446,7 @@ def drag_and_drop(step_data): ######################### @logger -def take_screenshot_playwright(step_data): +async def take_screenshot_playwright(step_data): """ Take a screenshot. @@ -2023,6 +2473,8 @@ def take_screenshot_playwright(step_data): save_variable = None custom_path = None has_element = False + image_type = "jpeg" + image_quality = CommonUtil.PLAYWRIGHT_AUTO_SCREENSHOT_QUALITY for left, mid, right in step_data: left_l = left.strip().lower() @@ -2036,25 +2488,43 @@ def take_screenshot_playwright(step_data): full_page = right_v.lower() in ("true", "yes", "1") elif left_l == "path": custom_path = right_v + elif left_l in ("format", "type", "image type"): + image_type = right_v.lower().replace("jpg", "jpeg") + elif left_l == "quality": + image_quality = int(right_v) elif mid_l == "save parameter": save_variable = left.strip() + if image_type not in ("jpeg", "png"): + CommonUtil.ExecLog(sModuleInfo, f"Unsupported screenshot format '{image_type}'. Use jpeg or png.", 3) + return "zeuz_failed" + # Generate filename if custom_path: screenshot_path = custom_path + suffix = Path(screenshot_path).suffix.lower() + if suffix in (".png", ".jpg", ".jpeg"): + image_type = "png" if suffix == ".png" else "jpeg" + else: + screenshot_path = str(Path(screenshot_path).with_suffix(".jpg" if image_type == "jpeg" else ".png")) else: timestamp = time.strftime("%Y_%m_%d_%H-%M-%S") - screenshot_path = f"screenshot_{timestamp}.png" + screenshot_path = f"screenshot_{timestamp}.{'jpg' if image_type == 'jpeg' else 'png'}" + + screenshot_options = {"path": screenshot_path, "type": image_type} + if image_type == "jpeg": + screenshot_options["quality"] = image_quality # Take screenshot if has_element: - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" - locator.screenshot(path=screenshot_path) + await locator.screenshot(**screenshot_options) else: - current_page.screenshot(path=screenshot_path, full_page=full_page) + screenshot_options["full_page"] = full_page + await current_page.screenshot(**screenshot_options) if save_variable: sr.Set_Shared_Variables(save_variable, screenshot_path) @@ -2073,7 +2543,7 @@ def take_screenshot_playwright(step_data): ######################### @logger -def execute_javascript(step_data): +async def execute_javascript(step_data): """ Execute JavaScript code in the browser. @@ -2118,13 +2588,13 @@ def execute_javascript(step_data): # Execute JS if has_element: - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" - result = locator.evaluate(js_code) + result = await locator.evaluate(js_code) else: - result = current_page.evaluate(js_code) + result = await current_page.evaluate(js_code) if save_variable: sr.Set_Shared_Variables(save_variable, result) @@ -2145,7 +2615,7 @@ def execute_javascript(step_data): ######################### @logger -def upload_file(step_data): +async def upload_file(step_data): """ Upload a file via file input. @@ -2188,12 +2658,12 @@ def upload_file(step_data): CommonUtil.ExecLog(sModuleInfo, f"File not found: {file_path}", 3) return "zeuz_failed" - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" - locator.set_input_files(file_path) + await locator.set_input_files(file_path) CommonUtil.ExecLog(sModuleInfo, f"File uploaded: {file_path}", 1) return "passed" @@ -2208,7 +2678,7 @@ def upload_file(step_data): ######################### @logger -def resize_window(step_data): +async def resize_window(step_data): """ Resize the browser viewport. @@ -2257,7 +2727,7 @@ def resize_window(step_data): width = width or current_size["width"] height = height or current_size["height"] - current_page.set_viewport_size({"width": width, "height": height}) + await current_page.set_viewport_size({"width": width, "height": height}) CommonUtil.ExecLog(sModuleInfo, f"Window resized to {width}x{height}", 1) return "passed" @@ -2272,7 +2742,7 @@ def resize_window(step_data): ######################### @logger -def Wait_For_Element(step_data): +async def Wait_For_Element(step_data): """ Wait for an element to appear/disappear. @@ -2309,11 +2779,13 @@ def Wait_For_Element(step_data): if mid_l == "input parameter": if left_l in ("wait", "state"): state = right_v.lower() - elif mid_l == "optional parameter": - if left_l == "timeout": - timeout = int(float(right_v) * 1000) + elif left_l == "wait for element": + timeout = int(right_v) - locator = PlaywrightLocator.Get_Element(step_data, current_page, element_wait=0.1) + if timeout: + await asyncio.sleep(timeout) + + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": # For hidden/detached states, element not found is actually success @@ -2323,11 +2795,11 @@ def Wait_For_Element(step_data): CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" - wait_options = {"state": state} - if timeout: - wait_options["timeout"] = timeout + # wait_options = {"state": state} + # if timeout: + # wait_options["timeout"] = timeout - locator.wait_for(**wait_options) + # locator.wait_for(**wait_options) CommonUtil.ExecLog(sModuleInfo, f"Element reached state: {state}", 1) return "passed" @@ -2345,7 +2817,7 @@ def Wait_For_Element(step_data): ######################### @logger -def Start_Tracing(step_data): +async def Start_Tracing(step_data): """ Start Playwright trace recording. @@ -2380,7 +2852,7 @@ def Start_Tracing(step_data): elif left_l == "sources": sources = right_v in ("true", "yes", "1") - context.tracing.start( + await context.tracing.start( screenshots=screenshots, snapshots=snapshots, sources=sources @@ -2393,7 +2865,7 @@ def Start_Tracing(step_data): @logger -def Stop_Tracing(step_data): +async def Stop_Tracing(step_data): """ Stop tracing and save trace file. @@ -2420,7 +2892,7 @@ def Stop_Tracing(step_data): if mid_l == "input parameter" and left_l == "path": trace_path = right_v - context.tracing.stop(path=trace_path) + await context.tracing.stop(path=trace_path) CommonUtil.ExecLog(sModuleInfo, f"Trace saved to: {trace_path}", 1) return "passed" @@ -2429,7 +2901,7 @@ def Stop_Tracing(step_data): @logger -def Intercept_Network(step_data): +async def Intercept_Network(step_data): """ Set up network request interception. @@ -2469,20 +2941,20 @@ def Intercept_Network(step_data): elif mid_l == "action": action = right_v.lower() - def handle_route(route): + async def handle_route(route): if action == "abort": - route.abort() + await route.abort() elif action == "fulfill": fulfill_options = {} if response_body: fulfill_options["body"] = response_body if response_status: fulfill_options["status"] = response_status - route.fulfill(**fulfill_options) + await route.fulfill(**fulfill_options) else: - route.continue_() + await route.continue_() - current_page.route(url_pattern, handle_route) + await current_page.route(url_pattern, handle_route) CommonUtil.ExecLog(sModuleInfo, f"Network interception set up for: {url_pattern}", 1) return "passed" @@ -2497,7 +2969,7 @@ def handle_route(route): ######################### @logger -def Extract_Table_Data(step_data): +async def Extract_Table_Data(step_data): """ Extract data from an HTML table. @@ -2532,13 +3004,13 @@ def Extract_Table_Data(step_data): elif left_l == "column": col_filter = right_v - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Table element not found", 3) return "zeuz_failed" # Extract table data using JavaScript - table_data = locator.evaluate("""table => { + table_data = await locator.evaluate("""table => { const data = []; const rows = table.querySelectorAll('tr'); rows.forEach(row => { diff --git a/Framework/Built_In_Automation/Web/Playwright/locator.py b/Framework/Built_In_Automation/Web/Playwright/locator.py index 5e7420d83..9acbe5b84 100644 --- a/Framework/Built_In_Automation/Web/Playwright/locator.py +++ b/Framework/Built_In_Automation/Web/Playwright/locator.py @@ -25,7 +25,7 @@ MODULE_NAME = inspect.getmodulename(__file__) -def Get_Element(step_data, page, return_all=False, element_wait=None): +async def Get_Element(step_data, page, return_all=False, element_wait=None, frame_locator=None): """ Get element using Playwright's native Locator API. @@ -38,6 +38,7 @@ def Get_Element(step_data, page, return_all=False, element_wait=None): page: Playwright Page object return_all: If True, return list of all matching ElementHandles element_wait: Override default wait timeout (in seconds) + frame_locator: Optional frame locator for iframe context Returns: Locator | List[ElementHandle] | "zeuz_failed" @@ -75,7 +76,7 @@ def Get_Element(step_data, page, return_all=False, element_wait=None): return "zeuz_failed" # Build the locator - locator = _build_locator(page, step_data, params) + locator = _build_locator(page, step_data, params, frame_locator) if locator is None: CommonUtil.ExecLog(sModuleInfo, "Could not build locator from step data", 3) @@ -119,7 +120,7 @@ def Get_Element(step_data, page, return_all=False, element_wait=None): # Return all elements if requested if return_all: try: - elements = locator.all() + elements = await locator.all() CommonUtil.ExecLog(sModuleInfo, f"Found {len(elements)} elements", 1) return elements except Exception as e: @@ -128,7 +129,7 @@ def Get_Element(step_data, page, return_all=False, element_wait=None): # Check if element exists (with timeout) try: - count = locator.count() + count = await locator.count() if count == 0: CommonUtil.ExecLog(sModuleInfo, "No elements found matching locator", 3) return "zeuz_failed" @@ -198,7 +199,7 @@ def _parse_element_params(step_data): params['wait'] = float(right_stripped) # Element parameters - elif mid_lower == "element parameter": + elif mid_lower == "element parameter" or "element parameter" in mid_lower: if left_lower == "index": try: params['index'] = int(right_stripped) @@ -230,7 +231,7 @@ def _parse_element_params(step_data): return params -def _build_locator(page, step_data, params): +def _build_locator(page, step_data, params, frame_locator=None): """ Build a Playwright Locator from step data. @@ -238,15 +239,24 @@ def _build_locator(page, step_data, params): 1. Playwright-native selectors (test-id, role, text, etc.) - fastest 2. Direct xpath/css if provided 3. Build xpath from element parameters using existing logic + + Args: + page: Playwright Page object + step_data: Step data for building xpath + params: Parsed element parameters + frame_locator: Optional frame locator for iframe context """ + # Use frame locator if provided, otherwise use page + base_locator = frame_locator if frame_locator else page + # Strategy 1: Check for Playwright-native selectors (fastest path) for left, right in params['element_params']: left_lower = left.lower() # Test ID selectors if left_lower in ("test-id", "testid", "data-testid", "data-test-id"): - return page.get_by_test_id(right) + return base_locator.get_by_test_id(right) # Role selector if left_lower == "role": @@ -257,54 +267,54 @@ def _build_locator(page, step_data, params): name = r break if name: - return page.get_by_role(right, name=name) - return page.get_by_role(right) + return base_locator.get_by_role(right, name=name) + return base_locator.get_by_role(right) # Text selectors if left_lower == "text": - return page.get_by_text(right, exact=True) + return base_locator.get_by_text(right, exact=True) if left_lower == "*text": - return page.get_by_text(right, exact=False) + return base_locator.get_by_text(right, exact=False) if left_lower == "**text": # Case-insensitive partial match - return page.get_by_text(re.compile(re.escape(right), re.IGNORECASE)) + return base_locator.get_by_text(re.compile(re.escape(right), re.IGNORECASE)) # Label selector if left_lower == "label": - return page.get_by_label(right) + return base_locator.get_by_label(right) # Placeholder selector if left_lower == "placeholder": - return page.get_by_placeholder(right) + return base_locator.get_by_placeholder(right) # Alt text selector if left_lower in ("alt", "alt text", "alt-text"): - return page.get_by_alt_text(right) + return base_locator.get_by_alt_text(right) # Title selector if left_lower == "title" and "parameter" not in params.get('mid', ''): - return page.get_by_title(right) + return base_locator.get_by_title(right) # Direct xpath if left_lower == "xpath": - return page.locator(f"xpath={right}") + return base_locator.locator(f"xpath={right}") # Direct CSS selector if left_lower in ("css", "css selector", "css_selector"): - return page.locator(right) + return base_locator.locator(right) - # Strategy 2: Check for unique parameters - for left, right in params['unique_params']: + # Strategy 2: Check for unique parameters and element parameters + for left, right in params['unique_params'] + params['element_params']: left_lower = left.lower() if left_lower == "id": - return page.locator(f"#{right}") + return base_locator.locator(f"#{right}") elif left_lower == "name": - return page.locator(f"[name='{right}']") + return base_locator.locator(f"[name='{right}']") elif left_lower == "class": - return page.locator(f".{right}") + return base_locator.locator(f".{right}") elif left_lower == "tag": - return page.locator(right) + return base_locator.locator(right) # Strategy 3: Build xpath from element/parent/child parameters xpath = _build_xpath_from_params(step_data, params) @@ -314,7 +324,7 @@ def _build_locator(page, step_data, params): f"Built xpath from parameters: {xpath}", 5 ) - return page.locator(f"xpath={xpath}") + return base_locator.locator(f"xpath={xpath}") # Strategy 4: Simple element parameters as xpath if params['element_params']: @@ -348,9 +358,9 @@ def _build_locator(page, step_data, params): if xpath_parts: xpath = f"//{tag}[{' and '.join(xpath_parts)}]" - return page.locator(f"xpath={xpath}") + return base_locator.locator(f"xpath={xpath}") elif tag != "*": - return page.locator(tag) + return base_locator.locator(tag) return None @@ -471,7 +481,7 @@ def _extract_sr_index(mid_value): return 1 -def wait_for_element(step_data, page, state="visible", timeout=None): +async def wait_for_element(step_data, page, state="visible", timeout=None): """ Wait for element to reach a specific state. @@ -487,7 +497,7 @@ def wait_for_element(step_data, page, state="visible", timeout=None): sModuleInfo = "wait_for_element" try: - locator = Get_Element(step_data, page) + locator = await Get_Element(step_data, page) if locator == "zeuz_failed": return "zeuz_failed" diff --git a/Framework/Built_In_Automation/Web/Playwright/utils.py b/Framework/Built_In_Automation/Web/Playwright/utils.py new file mode 100644 index 000000000..f6e531f7f --- /dev/null +++ b/Framework/Built_In_Automation/Web/Playwright/utils.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +""" +Playwright Utility Functions for Zeuz Node + +This module provides utility functions for Playwright automation, +including browser download and setup functionality. + +Author: Zeuz/AutomationSolutionz +""" + +import os +import subprocess +import sys +from pathlib import Path + +from filelock import FileLock + +from Framework.Utilities import CommonUtil +from settings import ZEUZ_NODE_DOWNLOADS_DIR + +PLAYWRIGHT_BROWSERS_DIR = ZEUZ_NODE_DOWNLOADS_DIR / "playwright_browsers" +PLAYWRIGHT_INSTALLABLE_BROWSERS = { + "chromium": "chromium", + "chrome": "chromium", + "firefox": "firefox", + "webkit": "webkit", + "safari": "webkit", +} +PLAYWRIGHT_SYSTEM_CHANNEL_BROWSERS = { + "edge", + "msedge", + "microsoft edge", + "chrome-beta", +} + + +def _set_playwright_browsers_path(): + """Use Zeuz's persistent downloads directory for Playwright browser binaries.""" + + PLAYWRIGHT_BROWSERS_DIR.mkdir(parents=True, exist_ok=True) + os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(PLAYWRIGHT_BROWSERS_DIR) + return PLAYWRIGHT_BROWSERS_DIR + + +def _get_playwright_browser_name(browser_name): + browser_name = (browser_name or "chromium").strip().lower() + return PLAYWRIGHT_INSTALLABLE_BROWSERS.get(browser_name) + + +def _get_playwright_executable_path(browser_name): + result = subprocess.run( + [ + sys.executable, + "-c", + ( + "from playwright.sync_api import sync_playwright\n" + "with sync_playwright() as p:\n" + f" print(p.{browser_name}.executable_path)\n" + ), + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=os.environ.copy(), + ) + + if result.returncode != 0: + return None + + executable_path = result.stdout.strip().splitlines()[-1] if result.stdout.strip() else "" + return Path(executable_path) if executable_path else None + + +def _is_playwright_browser_installed(browser_name): + executable_path = _get_playwright_executable_path(browser_name) + return bool(executable_path and executable_path.exists()) + + +def ensure_playwright_browser_installed(sModuleInfo, browser_name="chromium"): + """ + Ensure Playwright's managed browser is installed in Zeuz's persistent cache. + + Args: + sModuleInfo: Module information for logging + browser_name: Requested Playwright browser/channel name + + Returns: + bool: True if the browser is ready or no managed download is required + """ + try: + browsers_dir = _set_playwright_browsers_path() + requested_browser = (browser_name or "chromium").strip().lower() + install_browser = _get_playwright_browser_name(requested_browser) + + if requested_browser in PLAYWRIGHT_SYSTEM_CHANNEL_BROWSERS: + CommonUtil.ExecLog( + sModuleInfo, + f"Using Playwright browser cache: {browsers_dir}. Browser '{browser_name}' uses a system channel.", + 1, + ) + return True + + if not install_browser: + install_browser = "chromium" + CommonUtil.ExecLog( + sModuleInfo, + f"Unknown browser '{browser_name}', preparing Playwright chromium", + 2, + ) + + CommonUtil.ExecLog( + sModuleInfo, + f"Ensuring Playwright {install_browser} browser is installed in {browsers_dir}", + 1, + ) + + if _is_playwright_browser_installed(install_browser): + CommonUtil.ExecLog( + sModuleInfo, + f"Playwright {install_browser} browser already exists in {browsers_dir}", + 1, + ) + return True + + lock_path = browsers_dir / f"{install_browser}.install.lock" + with FileLock(str(lock_path)): + if _is_playwright_browser_installed(install_browser): + CommonUtil.ExecLog( + sModuleInfo, + f"Playwright {install_browser} browser already exists in {browsers_dir}", + 1, + ) + return True + + result = subprocess.run( + [sys.executable, "-m", "playwright", "install", "--with-deps", install_browser], + env=os.environ.copy(), + ) + + if result.returncode == 0: + CommonUtil.ExecLog( + sModuleInfo, + f"Playwright {install_browser} browser is ready", + 1, + ) + return True + + CommonUtil.ExecLog( + sModuleInfo, + f"Failed to install Playwright {install_browser} browser. See terminal output for details.", + 3, + ) + return False + + except Exception as e: + CommonUtil.ExecLog(sModuleInfo, f"Error setting up Playwright browser: {str(e)}", 3) + return False diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index 55797f587..17c822490 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -71,6 +71,16 @@ from Framework.AI.NLP import binary_classification from .utils import ChromeForTesting, ChromeExtensionDownloader +from playwright.async_api import async_playwright +from Framework.Built_In_Automation.Web.utils import ( + create_browser_session, + extract_session_name, + get_browser_session, + get_browser_sessions, + get_debug_port, + remove_browser_session, +) + ######################### # # # Global Variables # @@ -147,6 +157,166 @@ ReturnType = Literal["passed", "zeuz_failed"] +def _set_active_selenium_session(session_name, session): + """Update module globals/shared variables for a selected Selenium session.""" + + global selenium_driver, current_driver_id, selenium_details + + selenium_driver = session.get("selenium_driver") + current_driver_id = session_name + selenium_details.setdefault(session_name, {})["driver"] = selenium_driver + if session.get("remote_debugging_port"): + selenium_details[session_name]["remote-debugging-port"] = session["remote_debugging_port"] + Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) + Shared_Resources.Set_Shared_Variables("active_web_driver_type", "selenium") + if session.get("playwright_page"): + Shared_Resources.Set_Shared_Variables("playwright_page", session["playwright_page"]) + CommonUtil.set_screenshot_vars(Shared_Resources.Shared_Variable_Export()) + + +def _ensure_selenium_session(session_name, existing_session): + """Activate an existing Selenium session or lazily attach to a Playwright one.""" + + if existing_session and existing_session.get("selenium_driver"): + _set_active_selenium_session(session_name, existing_session) + return "passed" + + if not existing_session or not existing_session.get("playwright_page"): + return "zeuz_failed" + + if existing_session.get("selenium_cdp_supported") is False: + CommonUtil.ExecLog( + "_ensure_selenium_session", + f"Selenium attach is only supported for Chromium Playwright sessions: {session_name}", + 3, + ) + return "zeuz_failed" + + port = existing_session.get("remote_debugging_port") + if not port: + return "zeuz_failed" + + try: + from Framework.Built_In_Automation.Web.Playwright import BuiltInFunctions as PlaywrightBuiltInFunctions + + driver = PlaywrightBuiltInFunctions.connect_selenium_to_playwright(port=port) + if driver in failed_tag_list: + return "zeuz_failed" + + sessions = get_browser_sessions() + session = sessions.setdefault(session_name, existing_session) + session["selenium_driver"] = driver + session["remote_debugging_port"] = port + Shared_Resources.Set_Shared_Variables("browser_sessions", sessions) + _set_active_selenium_session(session_name, session) + CommonUtil.ExecLog("_ensure_selenium_session", f"Connected Selenium to Playwright session: {session_name}", 1) + return "passed" + except Exception as e: + CommonUtil.ExecLog("_ensure_selenium_session", f"Failed to connect Selenium to Playwright session '{session_name}': {e}", 3) + return "zeuz_failed" + + +def _handle_selenium_session(step_data): + """ + Helper function to handle session parameter for Selenium actions. + + Args: + step_data: The step data containing potential session parameter + + Returns: + tuple: (session_name, selenium_driver, current_driver_id) + - session_name: The session name found or None + - selenium_driver: The appropriate selenium driver instance + - current_driver_id: The current driver ID + """ + global selenium_driver, current_driver_id + + session_name = extract_session_name(step_data) + + if session_name: + existing_session = get_browser_session(session_name) + if _ensure_selenium_session(session_name, existing_session) not in failed_tag_list: + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + CommonUtil.ExecLog(sModuleInfo, f"Using existing browser session: {session_name}", 1) + else: + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + CommonUtil.ExecLog(sModuleInfo, f"Browser session '{session_name}' not found", 3) + raise ValueError(f"Browser session '{session_name}' not found") + elif selenium_driver is None: + default_session = get_browser_session("default") + if default_session and default_session.get("playwright_page"): + _ensure_selenium_session("default", default_session) + + return session_name, selenium_driver, current_driver_id + + +def _activate_browser_session_for_action(step_data, function_name=None): + """Select the requested browser session before running Selenium actions.""" + + global selenium_driver, current_driver_id, selenium_details + + session_name = extract_session_name(step_data) + + create_or_cleanup_actions = { + "Go_To_Link", + "Go_To_Link_V2", + "Open_Electron_App", + "Tear_Down_Selenium", + } + if function_name in create_or_cleanup_actions: + return "passed" + + if not session_name: + if selenium_driver is None: + default_session = get_browser_session("default") + if default_session and default_session.get("playwright_page"): + return _ensure_selenium_session("default", default_session) + return "passed" + + existing_session = get_browser_session(session_name) + if _ensure_selenium_session(session_name, existing_session) in failed_tag_list: + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + CommonUtil.ExecLog(sModuleInfo, f"Browser session '{session_name}' not found", 3) + return "zeuz_failed" + return "passed" + + +def _run_async_from_sync(awaitable): + """Run an awaitable from Selenium's sync actions, including inside an event loop.""" + + import asyncio + from concurrent.futures import ThreadPoolExecutor + + try: + loop = asyncio.get_running_loop() + if loop.is_running(): + def run_in_thread(): + new_loop = asyncio.new_event_loop() + asyncio.set_event_loop(new_loop) + try: + return new_loop.run_until_complete(awaitable) + finally: + new_loop.close() + + with ThreadPoolExecutor(max_workers=1) as executor: + return executor.submit(run_in_thread).result(timeout=30) + except RuntimeError: + pass + + return asyncio.run(awaitable) + + +def _close_maybe_async(resource, method_name="close"): + if not resource: + return + method = getattr(resource, method_name, None) + if not method: + return + result = method() + if inspect.iscoroutine(result): + _run_async_from_sync(result) + + class DefaultChromiumArguments(TypedDict): add_argument: list[str] add_experimental_option: dict[str, Any] @@ -426,7 +596,7 @@ def Open_Electron_App(data_set): desktop_app_path = "" driver_id = "" chrome_version = "" - for left, _, right in data_set: + for left, mid, right in data_set: left = left.replace(" ", "").replace("_", "").replace("-", "").lower() if "windows" in left and platform.system() == "Windows": desktop_app_path = right.strip() @@ -436,6 +606,8 @@ def Open_Electron_App(data_set): desktop_app_path = right.strip() elif left == "driverid": driver_id = right.strip() + elif left == "session" and mid.strip().lower() == "optional parameter": + driver_id = right.strip() elif left == "chrome:version": chrome_version = right.strip() @@ -464,7 +636,9 @@ def Open_Electron_App(data_set): opts = Options() opts.binary_location = desktop_app_path - opts.add_argument("--remote-debugging-port=9222") + electron_port = get_debug_port(driver_id or "electron", start=9230, stop=9320) + opts.add_argument(f"--remote-debugging-port={electron_port}") + CommonUtil.ExecLog(sModuleInfo, f"Using remote debugging port {electron_port} for Electron app", 1) # service = Service(executable_path=electron_chrome_path) arch = platform.machine().lower() if platform.system() == "Darwin" and arch == "arm64": @@ -481,15 +655,21 @@ def Open_Electron_App(data_set): selenium_driver.implicitly_wait(0.5) CommonUtil.ExecLog(sModuleInfo, "Started Electron App", 1) Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) + Shared_Resources.Set_Shared_Variables("active_web_driver_type", "selenium") CommonUtil.set_screenshot_vars(Shared_Resources.Shared_Variable_Export()) except Exception: return CommonUtil.Exception_Handler(sys.exc_info()) - if driver_id in selenium_details: - pass # we need to decide later based on the situation - else: - selenium_details[driver_id] = {"driver": selenium_driver} + selenium_details[driver_id] = { + "driver": selenium_driver, + "remote-debugging-port": electron_port, + } + create_browser_session( + session_name=driver_id, + selenium_driver=selenium_driver, + remote_debugging_port=electron_port, + ) current_driver_id = driver_id return "passed" except: @@ -656,8 +836,27 @@ def headless(): return options +async def connect_playwright_to_selenium(port=9222): + playwright_instance = await async_playwright().start() + browser = await playwright_instance.chromium.connect_over_cdp(f"http://localhost:{port}") + context = browser.contexts[0] + page = context.pages[0] + + from Framework.Built_In_Automation.Web.Playwright import BuiltInFunctions as PlaywrightBuiltInFunctions + PlaywrightBuiltInFunctions.playwright_instance = playwright_instance + PlaywrightBuiltInFunctions.browser = browser + PlaywrightBuiltInFunctions.context = context + PlaywrightBuiltInFunctions.current_page = page + + Shared_Resources.Set_Shared_Variables("playwright_context", context) + Shared_Resources.Set_Shared_Variables("playwright_browser", browser) + Shared_Resources.Set_Shared_Variables("playwright_page", page) + + return playwright_instance, browser, context, page + + @logger -def Open_Browser(browser, browser_options: BrowserOptions): +async def Open_Browser(browser, browser_options: BrowserOptions, session_name: str = "default"): """Launch browser from options and service object""" try: global selenium_driver @@ -687,6 +886,11 @@ def Open_Browser(browser, browser_options: BrowserOptions): options = generate_options(browser, browser_options) + # Enable remote debugging / CDP with a unique port per session. + unique_port = get_debug_port(session_name) + options.add_argument(f"--remote-debugging-port={unique_port}") + CommonUtil.ExecLog(sModuleInfo, f"Using remote debugging port {unique_port} for session '{session_name}'", 1) + if browser in ("android", "chrome", "chromeheadless"): from selenium.webdriver.chrome.service import Service @@ -756,8 +960,25 @@ def Open_Browser(browser, browser_options: BrowserOptions): ) return "zeuz_failed" + # If selenium_driver is of type Webdriver + from selenium.webdriver import Chrome, Firefox, Edge, Safari + if isinstance(selenium_driver, (Chrome, Firefox, Edge, Safari)): + # Create browser session + create_browser_session( + session_name=session_name, + selenium_driver=selenium_driver, + playwright_page=None, + playwright_browser=None, + playwright_context=None, + playwright_frame=None, + playwright_instance=None, + remote_debugging_port=unique_port, + ) + CommonUtil.ExecLog(sModuleInfo, f"Created browser session: {session_name=}", 5) + CommonUtil.ExecLog(sModuleInfo, f"Started {browser} browser", 1) Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) + Shared_Resources.Set_Shared_Variables("active_web_driver_type", "selenium") CommonUtil.set_screenshot_vars(Shared_Resources.Shared_Variable_Export()) return "passed" @@ -782,7 +1003,7 @@ def Go_To_Link_V2(step_data): options = Options() page_load_strategy = "normal" - for left, _, right in step_data: + for left, mid, right in step_data: left = left.strip().lower() if "add argument" == left: options.add_argument(right.strip()) @@ -809,6 +1030,8 @@ def Go_To_Link_V2(step_data): url = right.strip() if right.strip() != "" else None elif "driver tag" == left: driver_tag = right.strip() + elif left == "session" and mid.strip().lower() == "optional parameter": + driver_tag = right.strip() elif "wait for element" == left: Shared_Resources.Set_Shared_Variables("element_wait", float(right.strip())) elif "page load timeout" == left: @@ -817,7 +1040,10 @@ def Go_To_Link_V2(step_data): page_load_strategy = right.strip() options.page_load_strategy = page_load_strategy - if driver_tag in selenium_details.keys(): + existing_session = get_browser_session(driver_tag) + if existing_session and _ensure_selenium_session(driver_tag, existing_session) not in failed_tag_list: + pass + elif driver_tag in selenium_details.keys(): selenium_driver = selenium_details[driver_tag]["driver"] else: if Shared_Resources.Test_Shared_Variables("dependency"): @@ -830,16 +1056,28 @@ def Go_To_Link_V2(step_data): options.add_argument("--headless") CommonUtil.ExecLog(sModuleInfo, "Added headless argument", 1) + debug_port = get_debug_port(driver_tag) + options.add_argument(f"--remote-debugging-port={debug_port}") + CommonUtil.ExecLog(sModuleInfo, f"Using remote debugging port {debug_port} for session '{driver_tag}'", 1) + if "chrome" in dependency_browser: selenium_driver = webdriver.Chrome(options=options) elif "firefox" in dependency_browser: selenium_driver = webdriver.Firefox(options=options) selenium_driver.set_page_load_timeout(page_load_timeout_sec) - selenium_details[driver_tag] = dict() - selenium_details[driver_tag]["driver"] = selenium_driver - current_driver_id = selenium_driver + selenium_details[driver_tag] = { + "driver": selenium_driver, + "remote-debugging-port": debug_port, + } + create_browser_session( + session_name=driver_tag, + selenium_driver=selenium_driver, + remote_debugging_port=debug_port, + ) + current_driver_id = driver_tag Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) + Shared_Resources.Set_Shared_Variables("active_web_driver_type", "selenium") # Handle headless mode window maximize if ( @@ -853,6 +1091,8 @@ def Go_To_Link_V2(step_data): selenium_driver.maximize_window() Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) + current_driver_id = driver_tag + Shared_Resources.Set_Shared_Variables("active_web_driver_type", "selenium") CommonUtil.set_screenshot_vars(Shared_Resources.Shared_Variable_Export()) return "passed" @@ -915,7 +1155,7 @@ def parse_and_verify_datatype(left: str, right: str, chrome_version=None): @logger -def Go_To_Link(dataset: Dataset) -> ReturnType: +async def Go_To_Link(dataset: Dataset) -> ReturnType: try: sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME window_size_X = None @@ -990,13 +1230,14 @@ def Go_To_Link(dataset: Dataset) -> ReturnType: chrome_version = None chrome_channel = None debug_port = False + session_name = "default" for left, mid, right in dataset: left = left.replace(" ", "").replace("_", "").replace("-", "").lower() if left == "gotolink": web_link = right.strip() elif left == "driverid": - driver_id = right.strip() + session_name = driver_id = right.strip() elif left in ("waittimetoappearelement", "waitforelement"): Shared_Resources.Set_Shared_Variables( "element_wait", float(right.strip()) @@ -1009,6 +1250,8 @@ def Go_To_Link(dataset: Dataset) -> ReturnType: window_size_Y = int(resolution[1]) elif left == "chrome:version": chrome_version = right.strip() + elif left == "session": + session_name = driver_id = right.strip() # Capabilities are WebDriver attribute common across different browser elif mid.strip().lower() == "shared capability": @@ -1095,6 +1338,11 @@ def Go_To_Link(dataset: Dataset) -> ReturnType: driver_id = current_driver_id else: driver_id = list(selenium_details.keys())[0] + session_name = driver_id + + existing_session = get_browser_session(session_name) + if existing_session and _ensure_selenium_session(session_name, existing_session) not in failed_tag_list: + driver_id = session_name if ( driver_id not in selenium_details @@ -1117,9 +1365,12 @@ def Go_To_Link(dataset: Dataset) -> ReturnType: sModuleInfo, "Browser not previously opened, doing so now", 1 ) - if Open_Browser(dependency["Browser"], browser_options) == "zeuz_failed": + if await Open_Browser(dependency["Browser"], browser_options, session_name) == "zeuz_failed": return "zeuz_failed" + session_data = get_browser_session(session_name) + selenium_driver = session_data.get("selenium_driver", Shared_Resources.Get_Shared_Variables("selenium_driver")) + if ConfigModule.get_config_value( "RunDefinition", "window_size_x" ) and ConfigModule.get_config_value("RunDefinition", "window_size_y"): @@ -1139,13 +1390,18 @@ def Go_To_Link(dataset: Dataset) -> ReturnType: else: selenium_driver.set_window_size(window_size_X, window_size_Y) + if debug_port is None: + debug_port = session_data.get("remote_debugging_port") or get_debug_port(session_name) + selenium_details[driver_id] = { "driver": Shared_Resources.Get_Shared_Variables("selenium_driver"), "remote-debugging-port": debug_port } else: - selenium_driver = selenium_details[driver_id]["driver"] + session_data = get_browser_session(session_name) + selenium_driver = session_data.get("selenium_driver") or selenium_details[driver_id]["driver"] Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) + Shared_Resources.Set_Shared_Variables("active_web_driver_type", "selenium") current_driver_id = driver_id except Exception: ErrorMessage = "failed to open browser" @@ -1191,7 +1447,7 @@ def Go_To_Link(dataset: Dataset) -> ReturnType: # If the browser is closed but selenium instance is on, relaunch selenium_driver if Shared_Resources.Test_Shared_Variables("dependency"): dependency = Shared_Resources.Get_Shared_Variables("dependency") - result = Open_Browser(dependency["Browser"], browser_options) + result = await Open_Browser(dependency["Browser"], browser_options, session_name) else: result = "zeuz_failed" @@ -1584,6 +1840,10 @@ def Enter_Text_In_Text_Box(step_data): use_js = False clear = True global selenium_driver + + # Handle session parameter + session_name, selenium_driver, current_driver_id = _handle_selenium_session(step_data) + Element = LocateElement.Get_Element(step_data, selenium_driver) if Element == "zeuz_failed": CommonUtil.ExecLog( @@ -1593,6 +1853,9 @@ def Enter_Text_In_Text_Box(step_data): for left, mid, right in step_data: mid = mid.strip().lower() left = left.strip().lower() + # Skip session parameter - already handled above + if left == "session" and mid == "optional parameter": + continue if mid == "action": text_value = right elif left == "delay": @@ -2027,8 +2290,14 @@ def Click_Element(data_set, retry=0): global selenium_driver use_js = False # Use js to click on element? try: + # Handle session parameter + session_name, selenium_driver, current_driver_id = _handle_selenium_session(data_set) + location = "" for row in data_set: + # Skip session parameter - already handled above + if row[0].lower().replace(" ", "").replace("_", "").replace("-", "") == "session" and row[1].strip().lower() == "optional parameter": + continue if row[0] == "offset" and row[1] == "optional parameter": location = row[ 2 @@ -2974,6 +3243,9 @@ def Validate_Text(step_data): sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME global selenium_driver try: + # Handle session parameter + session_name, selenium_driver, current_driver_id = _handle_selenium_session(step_data) + Element = LocateElement.Get_Element(step_data, selenium_driver) ignore_case = False zeuz_ai = None @@ -2983,6 +3255,9 @@ def Validate_Text(step_data): ) return "zeuz_failed" for each_step_data_item in step_data: + # Skip session parameter - already handled above + if each_step_data_item[0].lower().replace(" ", "").replace("_", "").replace("-", "") == "session" and each_step_data_item[1].strip().lower() == "optional parameter": + continue if each_step_data_item[1] == "action": expected_text_data = each_step_data_item[2] validation_type = each_step_data_item[0] @@ -3088,6 +3363,9 @@ def Scroll(step_data): sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME global selenium_driver try: + # Handle session parameter + session_name, selenium_driver, current_driver_id = _handle_selenium_session(step_data) + Element = None get_element = False scroll_direction = "" @@ -3095,6 +3373,10 @@ def Scroll(step_data): pixel = 750 for left, mid, right in step_data: mid = mid.strip().lower() + left_clean = left.replace(" ", "").replace("_", "").replace("-", "").lower() + # Skip session parameter - already handled above + if left_clean == "session" and mid == "optional parameter": + continue if "action" in mid: scroll_direction = right.strip().lower() elif mid == "element parameter": @@ -3371,15 +3653,76 @@ def Tear_Down_Selenium(step_data=[]): global current_driver_id try: driver_id = "" + session_name = None + + # Parse both driverid (legacy) and session (new) parameters for left, mid, right in step_data: left = left.replace(" ", "").replace("_", "").replace("-", "").lower() - if left == "driverid": + if left == "session" and mid.strip().lower() == "optional parameter": + session_name = right.strip() + driver_id = session_name + elif left == "driverid": driver_id = right.strip() - if not driver_id: + # Handle session-specific teardown + if session_name: + existing_session = get_browser_session(session_name) + + if existing_session and existing_session.get("selenium_driver"): + try: + try: + _close_maybe_async(existing_session.get("playwright_context")) + _close_maybe_async(existing_session.get("playwright_browser")) + _close_maybe_async(existing_session.get("playwright_instance"), "stop") + except Exception: + pass + # Close the specific session's browser + session_driver = existing_session["selenium_driver"] + session_driver.quit() + CommonUtil.ExecLog(sModuleInfo, f"Teared down session '{session_name}'", 1) + except Exception as e: + errMsg = f"Unable to tear down session '{session_name}'. may already been killed" + CommonUtil.ExecLog(sModuleInfo, errMsg, 2) + CommonUtil.Exception_Handler(sys.exc_info(), None, errMsg) + + remove_browser_session(session_name) + + # Remove from selenium_details if present + if session_name in selenium_details: + del selenium_details[session_name] + + # If this was the current driver, switch to another or clear + if current_driver_id == session_name: + if selenium_details: + for driver in selenium_details: + selenium_driver = selenium_details[driver]["driver"] + Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) + CommonUtil.ExecLog(sModuleInfo, f"Current driver switched to driver_id='{driver}'", 1) + current_driver_id = driver + break + else: + Shared_Resources.Remove_From_Shared_Variables("selenium_driver") + selenium_driver = None + current_driver_id = None + else: + CommonUtil.ExecLog(sModuleInfo, f"Session '{session_name}' not found. Nothing to tear down.", 2) + + # Handle existing driver_id logic (backwards compatibility) + elif not driver_id: CommonUtil.Join_Thread_and_Return_Result( "screenshot" ) # Let the capturing screenshot end in thread + for session_name, session in get_browser_sessions().items(): + if not (isinstance(session, dict) and session.get("selenium_driver")): + continue + if session.get("playwright_page") and session_name not in selenium_details: + continue + try: + _close_maybe_async(session.get("playwright_context")) + _close_maybe_async(session.get("playwright_browser")) + _close_maybe_async(session.get("playwright_instance"), "stop") + except Exception: + pass for driver in selenium_details: try: selenium_details[driver]["driver"].quit() @@ -3394,8 +3737,20 @@ def Tear_Down_Selenium(step_data=[]): CommonUtil.ExecLog(sModuleInfo, errMsg, 2) CommonUtil.Exception_Handler(sys.exc_info(), None, errMsg) Shared_Resources.Remove_From_Shared_Variables("selenium_driver") + sessions = get_browser_sessions() + sessions = { + name: session + for name, session in sessions.items() + if not ( + isinstance(session, dict) + and session.get("selenium_driver") + and (not session.get("playwright_page") or name in selenium_details) + ) + } + Shared_Resources.Set_Shared_Variables("browser_sessions", sessions) selenium_details = {} selenium_driver = None + current_driver_id = None elif driver_id not in selenium_details: CommonUtil.ExecLog( @@ -3418,6 +3773,7 @@ def Tear_Down_Selenium(step_data=[]): 2, ) del selenium_details[driver_id] + remove_browser_session(driver_id) if selenium_details: for driver in selenium_details: selenium_driver = selenium_details[driver]["driver"] @@ -3459,13 +3815,19 @@ def Switch_Browser(step_data): driver_id = "" for left, mid, right in step_data: left = left.replace(" ", "").replace("_", "").replace("-", "").lower() - if left == "driverid": + if left in ("driverid", "session"): driver_id = right.strip() if not driver_id: driver_id = "default" - if driver_id not in selenium_details: + existing_session = get_browser_session(driver_id) + if existing_session and existing_session.get("selenium_driver"): + selenium_driver = existing_session["selenium_driver"] + selenium_details.setdefault(driver_id, {})["driver"] = selenium_driver + if existing_session.get("remote_debugging_port"): + selenium_details[driver_id]["remote-debugging-port"] = existing_session["remote_debugging_port"] + elif driver_id not in selenium_details: CommonUtil.ExecLog( sModuleInfo, "Driver_id='%s' not found. So could not Switch" % driver_id, @@ -3474,11 +3836,13 @@ def Switch_Browser(step_data): return "zeuz_failed" else: selenium_driver = selenium_details[driver_id]["driver"] - Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) - current_driver_id = driver_id - CommonUtil.ExecLog( - sModuleInfo, "Current driver is set to driver_id='%s'" % driver_id, 1 - ) + Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) + current_driver_id = driver_id + Shared_Resources.Set_Shared_Variables("active_web_driver_type", "selenium") + CommonUtil.set_screenshot_vars(Shared_Resources.Shared_Variable_Export()) + CommonUtil.ExecLog( + sModuleInfo, "Current driver is set to driver_id='%s'" % driver_id, 1 + ) return "passed" except Exception: @@ -4608,16 +4972,23 @@ def drag_and_drop(dataset): def playwright(dataset): sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME global selenium_driver + global selenium_details + global current_driver_id try: from playwright.sync_api import sync_playwright + # Get the correct remote debugging port for current driver + debug_port = 9222 # fallback + if current_driver_id and current_driver_id in selenium_details: + debug_port = selenium_details[current_driver_id].get("remote-debugging-port") or 9222 + devtools_url = ( selenium_driver.command_executor._url.replace("http://", "ws://") + "/devtools/browser" ) with sync_playwright() as p: # browser = p.chromium.connect(browserURL=devtools_url) - browser = p.chromium.connect_over_cdp("http://localhost:9222") + browser = p.chromium.connect_over_cdp(f"http://localhost:{debug_port}") page = browser.contexts[0].pages[0] # source = page.locator("//div[contains(text(), 'abcd')]") diff --git a/Framework/Built_In_Automation/Web/utils.py b/Framework/Built_In_Automation/Web/utils.py new file mode 100644 index 000000000..8af04245d --- /dev/null +++ b/Framework/Built_In_Automation/Web/utils.py @@ -0,0 +1,137 @@ +import hashlib +import socket + +from Framework.Built_In_Automation.Shared_Resources import ( + BuiltInFunctionSharedResources as sr, +) +from playwright.async_api import Browser, BrowserContext, Frame, Page +from selenium.webdriver import Chrome, Firefox, Edge, Safari + + + +def initialize_browser_sessions(): + """ + Checks if `browser_sessions` shared variable is already initialized. + If not, initializes it as an empty dictionary. + """ + + if sr.Test_Shared_Variables("browser_sessions") == False: + sr.Set_Shared_Variables("browser_sessions", {}) + + +def get_browser_sessions() -> dict: + """Return the browser session registry, initializing it when needed.""" + + if sr.Test_Shared_Variables("browser_sessions") == False: + initialize_browser_sessions() + + browser_sessions = sr.Get_Shared_Variables("browser_sessions", log=False) + if not isinstance(browser_sessions, dict): + browser_sessions = {} + sr.Set_Shared_Variables("browser_sessions", browser_sessions) + + return browser_sessions + + +def extract_session_name(step_data) -> str | None: + """Return the optional browser session name from Zeuz step data.""" + + if not step_data: + return None + + for left, mid, right in step_data: + left_l = left.replace(" ", "").replace("_", "").replace("-", "").lower() + if left_l == "session" and mid.strip().lower() == "optional parameter": + session_name = right.strip() + return session_name or None + + return None + + +def remove_browser_session(session_name: str) -> dict | None: + """Remove and return a browser session from the shared registry.""" + + browser_sessions = get_browser_sessions() + removed = browser_sessions.pop(session_name, None) + sr.Set_Shared_Variables("browser_sessions", browser_sessions) + return removed + + +def is_port_in_use(port: int) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(0.2) + return sock.connect_ex(("127.0.0.1", port)) == 0 + + +def get_debug_port(session_name: str, start: int = 9222, stop: int = 9322) -> int: + """Pick an available CDP port, preferring a stable session-name hash.""" + + used_ports = { + session.get("remote_debugging_port") + for session in get_browser_sessions().values() + if isinstance(session, dict) and session.get("remote_debugging_port") + } + + port_range = stop - start + 1 + port_hash = int(hashlib.md5(session_name.encode()).hexdigest(), 16) + first = start + (port_hash % port_range) + candidates = list(range(first, stop + 1)) + list(range(start, first)) + + for port in candidates: + if port not in used_ports and not is_port_in_use(port): + return port + + raise RuntimeError(f"No available remote debugging port in range {start}-{stop}") + + +def create_browser_session( + session_name: str = "default", + selenium_driver: Chrome | Firefox | Edge | Safari | None = None, + playwright_page: Page | None = None, + playwright_browser: Browser | None = None, + playwright_context: BrowserContext | None = None, + playwright_frame: Frame | None = None, + remote_debugging_port: int | None = None, + playwright_instance = None, +) -> dict: + """ + Creates a new browser session with the given parameters. + Replaces the session if it already exists with the given name. + + Args: + session_name (str): The name of the session. + selenium_driver (Chrome | Firefox | Edge | Safari): The Selenium WebDriver instance. + playwright_page (Page): The Playwright Page instance. + playwright_browser (Browser): The Playwright Browser instance. + playwright_context (BrowserContext): The Playwright BrowserContext instance. + playwright_frame (Frame): The Playwright Frame instance. + """ + + browser_sessions = get_browser_sessions() + browser_sessions[session_name] = { + "selenium_driver": selenium_driver, + "playwright_page": playwright_page, + "playwright_browser": playwright_browser, + "playwright_context": playwright_context, + "playwright_frame": playwright_frame, + "playwright_instance": playwright_instance, + "remote_debugging_port": remote_debugging_port, + } + sr.Set_Shared_Variables("browser_sessions", browser_sessions) + + return browser_sessions[session_name] + + +def get_browser_session(session_name: str) -> dict: + """ + Returns the browser session with the given name. + + Args: + session_name (str): The name of the session. + + Returns: + dict: The browser session with the given name. + """ + + browser_sessions = get_browser_sessions() + return browser_sessions.get(session_name, {}) diff --git a/Framework/MainDriverApi.py b/Framework/MainDriverApi.py index 35fd30652..e8509e71f 100644 --- a/Framework/MainDriverApi.py +++ b/Framework/MainDriverApi.py @@ -307,7 +307,7 @@ def terminate_thread(thread): # To kill running thread raise SystemError("PyThreadState_SetAsyncExc failed") # call the function of a test step that is in its driver file -def call_driver_function_of_test_step( +async def call_driver_function_of_test_step( sModuleInfo, all_step_info, StepSeq, @@ -338,6 +338,7 @@ def call_driver_function_of_test_step( try: # importing functions from driver functionTocall = getattr(module_name, step_name) + print(functionTocall) except Exception as e: CommonUtil.Exception_Handler( sys.exc_info(), @@ -405,12 +406,20 @@ def call_driver_function_of_test_step( CommonUtil.Exception_Handler(sys.exc_info()) else: # run sequentially - sStepResult = functionTocall( - test_steps_data, - test_action_info, - simple_queue, - debug_actions, - ) + if inspect.iscoroutinefunction(functionTocall): + sStepResult = await functionTocall( + test_steps_data, + test_action_info, + simple_queue, + debug_actions, + ) + else: + sStepResult = functionTocall( + test_steps_data, + test_action_info, + simple_queue, + debug_actions, + ) except: CommonUtil.Exception_Handler(sys.exc_info()) # handle exceptions sStepResult = "zeuz_failed" @@ -437,7 +446,7 @@ def call_driver_function_of_test_step( # runs all test steps of a test case -def run_all_test_steps_in_a_test_case( +async def run_all_test_steps_in_a_test_case( testcase_info, test_case, sModuleInfo, @@ -632,7 +641,7 @@ def run_all_test_steps_in_a_test_case( CommonUtil.ExecLog(sModuleInfo, "STEP-%s is skipped" % StepSeq, 2) sStepResult = "skipped" else: - sStepResult = call_driver_function_of_test_step( + sStepResult = await call_driver_function_of_test_step( sModuleInfo, all_step_info, StepSeq, @@ -797,13 +806,20 @@ def zip_and_delete_tc_folder( FL.DeleteFolder(path) -def cleanup_driver_instances(): # cleans up driver(selenium, appium) instances +async def cleanup_driver_instances(): # cleans up driver(selenium, playwright, appium) instances try: # if error happens. we don't care, main driver should not stop, pass in exception import Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions as Selenium + import Framework.Built_In_Automation.Web.Playwright.BuiltInFunctions as Playwright + try: + await Playwright.Tear_Down_Playwright() + except: + pass + try: Selenium.Tear_Down_Selenium() except: pass + if shared.Test_Shared_Variables("appium_details"): import Framework.Built_In_Automation.Mobile.CrossPlatform.Appium.BuiltInFunctions as Appium driver = shared.Remove_From_Shared_Variables("appium_details") @@ -962,7 +978,7 @@ def check_test_skip(run_id, tc_num, skip_remaining=True) -> bool: return False -def run_test_case( +async def run_test_case( TestCaseID, sModuleInfo, run_id, @@ -1020,7 +1036,7 @@ def run_test_case( if check_test_skip(run_id, tc_num): sTestStepResultList = ['SKIPPED' for i in range(len(testcase_info['steps']))] else: - sTestStepResultList = run_all_test_steps_in_a_test_case( + sTestStepResultList = await run_all_test_steps_in_a_test_case( testcase_info, test_case, sModuleInfo, @@ -1133,7 +1149,7 @@ def run_test_case( else: CommonUtil.Join_Thread_and_Return_Result("screenshot") if str(shared.Get_Shared_Variables("zeuz_auto_teardown")).strip().lower() not in ("off", "no", "false", "disable"): - cleanup_driver_instances() + await cleanup_driver_instances() shared.Clean_Up_Shared_Variables(run_id) if ConfigModule.get_config_value("RunDefinition", "local_run") == "False": @@ -1832,7 +1848,7 @@ def download_or_copy(attachment): # main function -def main(device_dict, all_run_id_info): +async def main(device_dict, all_run_id_info): try: # get module info sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME @@ -1945,7 +1961,7 @@ def main(device_dict, all_run_id_info): if "debug_step_actions" in run_id_info: debug_info["debug_step_actions"] = run_id_info["debug_step_actions"] if run_id_info["debug_clean"] == "YES": - cleanup_driver_instances() + await cleanup_driver_instances() shared.Clean_Up_Shared_Variables(run_id) driver_list = ["Not needed currently"] @@ -1966,7 +1982,7 @@ def main(device_dict, all_run_id_info): shared.Set_Shared_Variables("zeuz_auto_teardown", "on") if not CommonUtil.debug_status and str(shared.Get_Shared_Variables("zeuz_auto_teardown")).strip().lower() not in ("off", "no", "false", "disable"): - cleanup_driver_instances() + await cleanup_driver_instances() if not shared.Test_Shared_Variables("zeuz_collect_browser_log"): shared.Set_Shared_Variables("zeuz_collect_browser_log", "on") @@ -2119,7 +2135,7 @@ def kill(process): ) except Exception as e: CommonUtil.ExecLog(sModuleInfo, str(e), 3) - run_test_case( + await run_test_case( test_case_no, sModuleInfo, run_id, @@ -2137,7 +2153,7 @@ def kill(process): else: - run_test_case( + await run_test_case( test_case_no, sModuleInfo, run_id, diff --git a/Framework/Utilities/CommonUtil.py b/Framework/Utilities/CommonUtil.py index 28215917b..546a74216 100644 --- a/Framework/Utilities/CommonUtil.py +++ b/Framework/Utilities/CommonUtil.py @@ -166,6 +166,7 @@ def temp_config() -> Path: AUTO_SCREENSHOT_DEBUG_DELAY_SECONDS = 3 AUTO_SCREENSHOT_DEBUG_DELAY_POLL_SECONDS = 0.25 CANCELLED_RUN_STATUS = "Cancelled" +PLAYWRIGHT_AUTO_SCREENSHOT_QUALITY = 70 # Metrics variables browser_perf = {} @@ -842,14 +843,18 @@ def set_screenshot_vars(shared_variables): screen_capture_driver = appium_details[device_id][ "driver" ] # Driver for selected device - if screen_capture_type == "web": # Selenium driver object - if "selenium_driver" in shared_variables: + if screen_capture_type == "web": # Selenium or Playwright driver object + if shared_variables.get("active_web_driver_type") == "playwright" and "playwright_page" in shared_variables: + screen_capture_driver = shared_variables["playwright_page"] + elif "selenium_driver" in shared_variables: screen_capture_driver = shared_variables["selenium_driver"] + elif "playwright_page" in shared_variables: + screen_capture_driver = shared_variables["playwright_page"] except: ExecLog(sModuleInfo, "Error setting screenshot variables", 3) -def TakeScreenShot(function_name, local_run=False): +async def TakeScreenShot(function_name, local_run=False): """ Puts TakeScreenShot into a thread, so it doesn't block test case execution """ if not ws_ss_log or performance_testing: return try: @@ -898,8 +903,7 @@ def TakeScreenShot(function_name, local_run=False): break image_name = "Step#" + current_step_no + "_Action#" + current_action_no + "_" + filename - thread = executor.submit(Thread_ScreenShot, function_name, image_folder, Method, Driver, image_name) - SaveThread("screenshot", thread) + await Thread_ScreenShot(function_name, image_folder, Method, Driver, image_name) except: return Exception_Handler(sys.exc_info()) @@ -990,10 +994,11 @@ def _get_window_screenshot_bbox(): return None -def Thread_ScreenShot(function_name, image_folder, Method, Driver, image_name): - """ Capture screen of mobile or desktop """ - if performance_testing: return - sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME +def _is_playwright_page(driver): + return driver.__class__.__module__.startswith("playwright.") and hasattr(driver, "screenshot") + + +def _screenshot_path(image_folder, image_name, extension="png"): chars_to_remove = [ r"?", r"*", @@ -1004,15 +1009,22 @@ def Thread_ScreenShot(function_name, image_folder, Method, Driver, image_name): r"\\", r"\/", r":", - ] # Symbols that can't be used in filename + ] + trans_table = str.maketrans(dict.fromkeys("".join(chars_to_remove))) + safe_name = (image_name.translate(trans_table)).strip().replace(" ", "_") + return os.path.join(image_folder, safe_name + "." + extension.lstrip(".")) + + +async def Thread_ScreenShot(function_name, image_folder, Method, Driver, image_name): + """ Capture screen of mobile or desktop """ + if performance_testing: return + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME picture_quality = 100 # Quality of picture picture_size = 1920, 1080 # Size of image (for reduction in file size) + is_playwright_page = Method == "web" and Driver is not None and _is_playwright_page(Driver) # Adjust filename and create full path (remove invalid characters, convert spaces to underscore, remove leading and trailing spaces) - trans_table = str.maketrans( - dict.fromkeys("".join(chars_to_remove)) - ) # python3 version of translate - ImageName = os.path.join(image_folder, (image_name.translate(trans_table)).strip().replace(" ", "_") + ".png") + ImageName = _screenshot_path(image_folder, image_name, "jpg" if is_playwright_page else "png") ExecLog(sModuleInfo, "Capturing screen on %s, with driver: %s, and saving to %s" % (str(Method), str(Driver), ImageName), 0) try: should_delay_before_capture = Method == "desktop" and sys.platform in ("linux2", "win32", "darwin") @@ -1056,7 +1068,11 @@ def Thread_ScreenShot(function_name, image_folder, Method, Driver, image_name): # Capture screenshot of web browser elif Method == "web": - Driver.get_screenshot_as_file(ImageName) # Must be .png, otherwise an exception occurs + # Check if it's a Playwright page or Selenium driver + if is_playwright_page: + await Driver.screenshot(path=ImageName, type="jpeg", quality=PLAYWRIGHT_AUTO_SCREENSHOT_QUALITY) + else: # Selenium driver + Driver.get_screenshot_as_file(ImageName) # Must be .png, otherwise an exception occurs # Capture screenshot of mobile elif Method == "mobile": @@ -1070,14 +1086,15 @@ def Thread_ScreenShot(function_name, image_folder, Method, Driver, image_name): ) # Lower the picture quality if os.path.exists(ImageName): # Make sure image was saved - image = Image.open(ImageName) # Re-open in standard format - image.thumbnail(picture_size, Image.LANCZOS) # Resize picture to lower file size - image.save(ImageName, format="PNG", quality=picture_quality) # Change quality to reduce file size + if not is_playwright_page: + image = Image.open(ImageName) # Re-open in standard format + image.thumbnail(picture_size, Image.LANCZOS) # Resize picture to lower file size + image.save(ImageName, format="PNG", quality=picture_quality) # Change quality to reduce file size if debug_status: # Convert image to bytearray and send it to live_log_service for streaming. + image = Image.open(ImageName) # Re-open in standard format image_byte_array = pil_image_to_bytearray(image) - live_log_service.binary(image_byte_array) else: ExecLog( diff --git a/node_cli.py b/node_cli.py index 9e1d9dc25..a8204a778 100755 --- a/node_cli.py +++ b/node_cli.py @@ -440,7 +440,7 @@ async def response_callback(response: str): # 3. Call MainDriver device_info = All_Device_Info.get_all_connected_device_info() await install_handler.cancel_run() - MainDriverApi.main( + await MainDriverApi.main( device_dict=device_info, all_run_id_info=node_json, ) diff --git a/tests/test_browser_sessions.py b/tests/test_browser_sessions.py new file mode 100644 index 000000000..5ccc01a44 --- /dev/null +++ b/tests/test_browser_sessions.py @@ -0,0 +1,165 @@ +import asyncio +from unittest.mock import MagicMock + +from Framework.Built_In_Automation.Shared_Resources import ( + BuiltInFunctionSharedResources as sr, +) +from Framework.Built_In_Automation.Web import utils as browser_utils +from Framework.Built_In_Automation.Web.Playwright import BuiltInFunctions as playwright_bif + +sr.Set_Shared_Variables("dependency", {"Browser": "chrome"}) +from Framework.Built_In_Automation.Web.Selenium import BuiltInFunctions as selenium_bif # noqa: E402 + + +def setup_function(): + sr.shared_variables.clear() + selenium_bif.selenium_driver = None + selenium_bif.current_driver_id = None + selenium_bif.selenium_details = {} + playwright_bif.current_page = None + playwright_bif.current_page_id = None + playwright_bif.context = None + playwright_bif.browser = None + playwright_bif.playwright_details = {} + + +def test_selenium_session_activation_selects_driver(): + driver = MagicMock() + browser_utils.create_browser_session( + session_name="admin", + selenium_driver=driver, + remote_debugging_port=9231, + ) + + result = selenium_bif._activate_browser_session_for_action( + [("session", "optional parameter", "admin")], + "Click_Element", + ) + + assert result == "passed" + assert selenium_bif.selenium_driver is driver + assert selenium_bif.current_driver_id == "admin" + assert sr.Get_Shared_Variables("selenium_driver") is driver + assert sr.Get_Shared_Variables("active_web_driver_type") == "selenium" + assert selenium_bif.selenium_details["admin"]["remote-debugging-port"] == 9231 + + +def test_selenium_missing_explicit_session_fails_non_create_action(): + result = selenium_bif._activate_browser_session_for_action( + [("session", "optional parameter", "missing")], + "Click_Element", + ) + + assert result == "zeuz_failed" + + +def test_selenium_missing_explicit_session_allowed_for_browser_creation(): + result = selenium_bif._activate_browser_session_for_action( + [("session", "optional parameter", "new_user")], + "Go_To_Link", + ) + + assert result == "passed" + + +def test_playwright_session_activation_selects_page_and_frame(): + page = MagicMock() + context = MagicMock() + browser = MagicMock() + frame = MagicMock() + selenium_driver = MagicMock() + browser_utils.create_browser_session( + session_name="buyer", + selenium_driver=selenium_driver, + playwright_page=page, + playwright_context=context, + playwright_browser=browser, + playwright_frame=frame, + ) + + result = playwright_bif._activate_browser_session_for_action( + [("session", "optional parameter", "buyer")], + "Hover_Over_Element", + ) + + assert result == "passed" + assert playwright_bif.current_page is page + assert playwright_bif.context is context + assert playwright_bif.browser is browser + assert playwright_bif.current_page_id == "buyer" + assert sr.Get_Shared_Variables("playwright_frame") is frame + assert sr.Get_Shared_Variables("active_web_driver_type") == "playwright" + + +def test_playwright_missing_explicit_session_fails_non_create_action(): + result = playwright_bif._activate_browser_session_for_action( + [("session", "optional parameter", "missing")], + "Validate_Text", + ) + + assert result == "zeuz_failed" + + +def test_remove_browser_session_updates_registry(): + browser_utils.create_browser_session("temp", selenium_driver=MagicMock()) + + removed = browser_utils.remove_browser_session("temp") + + assert removed is not None + assert browser_utils.get_browser_session("temp") == {} + + +def test_selenium_global_teardown_preserves_playwright_owned_sessions(monkeypatch): + selenium_driver = MagicMock() + context = MagicMock() + browser = MagicMock() + browser_utils.create_browser_session( + session_name="playwright_session", + selenium_driver=selenium_driver, + playwright_page=MagicMock(), + playwright_context=context, + playwright_browser=browser, + ) + monkeypatch.setattr( + "Framework.Utilities.CommonUtil.Join_Thread_and_Return_Result", + lambda key: [], + ) + + result = selenium_bif.Tear_Down_Selenium([]) + + assert result == "passed" + assert browser_utils.get_browser_session("playwright_session") + context.close.assert_not_called() + browser.close.assert_not_called() + + +def test_playwright_switch_iframe_uses_index_parameter_after_default_reset(): + frame_locator = MagicMock() + indexed_frame_locator = MagicMock() + frame_locator.nth.return_value = indexed_frame_locator + page = MagicMock() + page.frame_locator.return_value = frame_locator + playwright_bif.current_page = page + playwright_bif.current_page_id = "default" + browser_utils.create_browser_session( + session_name="default", + playwright_page=page, + playwright_context=MagicMock(), + playwright_browser=MagicMock(), + ) + + result = asyncio.run( + playwright_bif.switch_iframe( + [ + ("index", "iframe parameter", "default content"), + ("index", "iframe parameter", "1"), + ("switch iframe", "playwright action", "switch iframe"), + ] + ) + ) + + assert result == "passed" + page.frame_locator.assert_called_once_with("iframe") + frame_locator.nth.assert_called_once_with(1) + assert sr.Get_Shared_Variables("playwright_frame") is indexed_frame_locator + assert browser_utils.get_browser_session("default")["playwright_frame"] is indexed_frame_locator diff --git a/uv.lock b/uv.lock index f448fba4a..cd801351a 100644 --- a/uv.lock +++ b/uv.lock @@ -3093,21 +3093,21 @@ wheels = [ [[package]] name = "playwright" -version = "1.56.0" +version = "1.60.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet" }, { name = "pyee" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/31/a5362cee43f844509f1f10d8a27c9cc0e2f7bdce5353d304d93b2151c1b1/playwright-1.56.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b33eb89c516cbc6723f2e3523bada4a4eb0984a9c411325c02d7016a5d625e9c", size = 40611424, upload-time = "2025-11-11T18:39:10.175Z" }, - { url = "https://files.pythonhosted.org/packages/ef/95/347eef596d8778fb53590dc326c344d427fa19ba3d42b646fce2a4572eb3/playwright-1.56.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b228b3395212b9472a4ee5f1afe40d376eef9568eb039fcb3e563de8f4f4657b", size = 39400228, upload-time = "2025-11-11T18:39:13.915Z" }, - { url = "https://files.pythonhosted.org/packages/b9/54/6ad97b08b2ca1dfcb4fbde4536c4f45c0d9d8b1857a2d20e7bbfdf43bf15/playwright-1.56.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:0ef7e6fd653267798a8a968ff7aa2dcac14398b7dd7440ef57524e01e0fbbd65", size = 40611424, upload-time = "2025-11-11T18:39:17.093Z" }, - { url = "https://files.pythonhosted.org/packages/e4/76/6d409e37e82cdd5dda3df1ab958130ae32b46e42458bd4fc93d7eb8749cb/playwright-1.56.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:404be089b49d94bc4c1fe0dfb07664bda5ffe87789034a03bffb884489bdfb5c", size = 46263122, upload-time = "2025-11-11T18:39:20.619Z" }, - { url = "https://files.pythonhosted.org/packages/4f/84/fb292cc5d45f3252e255ea39066cd1d2385c61c6c1596548dfbf59c88605/playwright-1.56.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64cda7cf4e51c0d35dab55190841bfcdfb5871685ec22cb722cd0ad2df183e34", size = 46110645, upload-time = "2025-11-11T18:39:24.005Z" }, - { url = "https://files.pythonhosted.org/packages/61/bd/8c02c3388ae14edc374ac9f22cbe4e14826c6a51b2d8eaf86e89fabee264/playwright-1.56.0-py3-none-win32.whl", hash = "sha256:d87b79bcb082092d916a332c27ec9732e0418c319755d235d93cc6be13bdd721", size = 35639837, upload-time = "2025-11-11T18:39:27.174Z" }, - { url = "https://files.pythonhosted.org/packages/64/27/f13b538fbc6b7a00152f4379054a49f6abc0bf55ac86f677ae54bc49fb82/playwright-1.56.0-py3-none-win_amd64.whl", hash = "sha256:3c7fc49bb9e673489bf2622855f9486d41c5101bbed964638552b864c4591f94", size = 35639843, upload-time = "2025-11-11T18:39:30.851Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c7/3ee8b556107995846576b4fe42a08ed49b8677619421f2afacf6ee421138/playwright-1.56.0-py3-none-win_arm64.whl", hash = "sha256:2745490ae8dd58d27e5ea4d9aa28402e8e2991eb84fb4b2fd5fbde2106716f6f", size = 31248959, upload-time = "2025-11-11T18:39:33.998Z" }, + { url = "https://files.pythonhosted.org/packages/21/f0/832bd9677194908da118064eef20082f2791e3d18215cc6d9391ee2c5a67/playwright-1.60.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:6a8cd0fec171fb3089e95e898c8bc8a6f35dea0b78b399e12fcc19427e91b1d7", size = 43474635, upload-time = "2026-05-18T12:00:31.969Z" }, + { url = "https://files.pythonhosted.org/packages/59/7b/e1d32ae8a3ed937ec2be3721c5f728b13d731a0b7c6442e0b3bec5094ac0/playwright-1.60.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:39b5420ba6145045b69ced4c5c47d4d9fe5bddfc8ff816c518913afcb25ec7a5", size = 42261327, upload-time = "2026-05-18T12:00:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/d7/bc/23de499ded6411c188a20c5a0dea6f0cd4ed5d2b3cc6042a5dbd3ed609aa/playwright-1.60.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:2581d0e6a3392c71f91b27460c7fd093356818dc430f48153896c8aeeaef7705", size = 43474636, upload-time = "2026-05-18T12:00:39.294Z" }, + { url = "https://files.pythonhosted.org/packages/22/7b/1d679f4fced4ea94efadd17103856d8c565384f68382a1681264e46f5925/playwright-1.60.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:1c2bfae7884fb3fb05b853290eab8f343d524e5016f2f1def702acbbdf14c93e", size = 47467220, upload-time = "2026-05-18T12:00:43.179Z" }, + { url = "https://files.pythonhosted.org/packages/84/c2/1528d267d4442bd2c6b8eaeab819dd52c2030bf80e89293f0ba1f687473b/playwright-1.60.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43e66564125ee31b07a58cefb21e256d62d67d8d1713e6858df7a3019d8ed353", size = 47154856, upload-time = "2026-05-18T12:00:46.715Z" }, + { url = "https://files.pythonhosted.org/packages/bb/4e/b008b6440a7a1624378041da94829956d4b8f7ab9ef5aad22d0dc3f2e26d/playwright-1.60.0-py3-none-win32.whl", hash = "sha256:ec94e416ea320711e0ad4bf185dcbf41833672961e90773e1885255d7db7b7e7", size = 37902157, upload-time = "2026-05-18T12:00:50.374Z" }, + { url = "https://files.pythonhosted.org/packages/55/f0/0541524133104f9cc20bf900870ff4a736b76a23483f3a55295ddfa58409/playwright-1.60.0-py3-none-win_amd64.whl", hash = "sha256:9566821ce6030a1f9e7146a24e19355ab0d98805fd0f9be50bb3d8fef1750c02", size = 37902159, upload-time = "2026-05-18T12:00:53.728Z" }, + { url = "https://files.pythonhosted.org/packages/80/c8/210f282d278e4709cdd71b12a31af45a30a22ab3207b387e29b37e478713/playwright-1.60.0-py3-none-win_arm64.whl", hash = "sha256:6e4f6700a4c2250efff8e690a81d66e3855754fb587b6b87cf5c784014f91537", size = 34037981, upload-time = "2026-05-18T12:00:57.584Z" }, ] [[package]]