From 30a38077b20b29c0d4e81a2e8e1b24234c6ecbbc Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Mon, 9 Feb 2026 12:59:41 +0600 Subject: [PATCH 01/41] Convert go to link Action to async --- .../Web/Playwright/BuiltInFunctions.py | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index b13120e3..09be50dc 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -27,8 +27,8 @@ import re from pathlib import Path -from playwright.sync_api import ( - sync_playwright, +from playwright.async_api import ( + async_playwright, Page, Browser, BrowserContext, @@ -75,7 +75,7 @@ ######################### @logger -def Open_Browser(step_data): +async def Open_Browser(step_data): """ Launch a new browser instance with Playwright. @@ -104,7 +104,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 @@ -168,7 +168,7 @@ def Open_Browser(step_data): # 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 = { @@ -183,20 +183,20 @@ def Open_Browser(step_data): # 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) + browser = await playwright_instance.firefox.launch(**launch_options) elif browser_name in ("webkit", "safari"): - browser = playwright_instance.webkit.launch(**launch_options) + 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 +214,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 @@ -229,7 +229,7 @@ def Open_Browser(step_data): # 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 @@ -246,7 +246,7 @@ def Open_Browser(step_data): @logger -def Go_To_Link(step_data): +async def Go_To_Link(step_data): """ Navigate to a URL. @@ -263,8 +263,11 @@ def Go_To_Link(step_data): try: if current_page is None: - CommonUtil.ExecLog(sModuleInfo, "No browser open. Use 'open browser' first.", 3) - return "zeuz_failed" + 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 +278,10 @@ 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 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 +294,7 @@ 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) CommonUtil.ExecLog(sModuleInfo, f"Navigated to: {url}", 1) return "passed" From 69608d008dab037319161fc5a17ad018ad26c06d Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Mon, 9 Feb 2026 13:00:29 +0600 Subject: [PATCH 02/41] Convert action handlers and `MainDriver` to async --- Drivers/Built_In_Driver.py | 4 +- .../Sequential_Actions/sequential_actions.py | 48 ++++++++++--------- Framework/MainDriverApi.py | 37 ++++++++------ node_cli.py | 2 +- 4 files changed, 51 insertions(+), 40 deletions(-) diff --git a/Drivers/Built_In_Driver.py b/Drivers/Built_In_Driver.py index dd2e894c..dde1b15f 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 4ff0044c..e590da74 100755 --- a/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py +++ b/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py @@ -279,7 +279,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 +526,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 +559,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 +748,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 +848,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 +947,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 +1049,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 +1068,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 +1100,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 +1196,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 +1204,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 +1215,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 +1224,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 +1235,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 +1243,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 +1273,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 +1343,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 +1373,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 +1404,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 +1903,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 @@ -2296,7 +2296,7 @@ def compare_variable_names(set, dataset): CommonUtil.compare_action_varnames = {"left": "Left", "right": "Right"} -def Action_Handler(_data_set, action_row, _bypass_bug=True): +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 @@ -2417,6 +2417,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]: diff --git a/Framework/MainDriverApi.py b/Framework/MainDriverApi.py index f7734a5f..9e32bce3 100644 --- a/Framework/MainDriverApi.py +++ b/Framework/MainDriverApi.py @@ -281,7 +281,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, @@ -312,6 +312,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(), @@ -379,12 +380,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" @@ -411,7 +420,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, @@ -606,7 +615,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, @@ -936,7 +945,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, @@ -994,7 +1003,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, @@ -1806,7 +1815,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 @@ -2093,7 +2102,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, @@ -2111,7 +2120,7 @@ def kill(process): else: - run_test_case( + await run_test_case( test_case_no, sModuleInfo, run_id, diff --git a/node_cli.py b/node_cli.py index d4e1ce89..e8b13301 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, ) From a9959ed1604058f930c74965f1f1b5723538429a Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Mon, 9 Feb 2026 13:12:41 +0600 Subject: [PATCH 03/41] Make all Playwright web actions compatible with Playwright Async API --- .../Web/Playwright/BuiltInFunctions.py | 172 +++++++++--------- .../Web/Playwright/locator.py | 10 +- 2 files changed, 91 insertions(+), 91 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 09be50dc..17fa91d9 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -423,7 +423,7 @@ def Switch_Browser(step_data): ######################### @logger -def Click_Element(step_data): +async def Click_Element(step_data): """ Click an element. @@ -494,7 +494,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) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" @@ -516,14 +516,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" @@ -533,7 +533,7 @@ def Click_Element(step_data): @logger -def Double_Click_Element(step_data): +async def Double_Click_Element(step_data): """ Double-click an element. @@ -550,11 +550,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. @@ -569,11 +569,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. @@ -608,7 +608,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) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" @@ -621,7 +621,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" @@ -636,7 +636,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. @@ -685,7 +685,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) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" @@ -693,17 +693,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 @@ -712,7 +712,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" @@ -722,7 +722,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. @@ -829,18 +829,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) 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) @@ -852,13 +852,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) 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) @@ -875,7 +875,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. @@ -927,13 +927,13 @@ def Validate_Text(step_data): 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) 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 @@ -964,7 +964,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. @@ -992,14 +992,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) 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" @@ -1015,7 +1015,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. @@ -1064,7 +1064,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) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" @@ -1072,27 +1072,27 @@ 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(attribute_name) sr.Set_Shared_Variables(save_variable, value) CommonUtil.ExecLog(sModuleInfo, f"Saved '{attribute_name}' = '{value}' to '{save_variable}'", 1) @@ -1103,7 +1103,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. @@ -1126,23 +1126,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) 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; @@ -1326,7 +1326,7 @@ def Scroll(step_data): @logger -def scroll_to_element(step_data): +async def scroll_to_element(step_data): """ Scroll an element into view. @@ -1358,15 +1358,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) 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" @@ -1382,7 +1382,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. @@ -1438,7 +1438,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) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" @@ -1453,7 +1453,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; @@ -1463,7 +1463,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" @@ -1479,7 +1479,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. @@ -1516,7 +1516,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) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" @@ -1526,10 +1526,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" @@ -1938,7 +1938,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. @@ -1971,19 +1971,19 @@ def drag_and_drop(step_data): target_params.append((left, mid, right)) # Get source element - source_locator = PlaywrightLocator.Get_Element(source_params, current_page) + source_locator = await PlaywrightLocator.Get_Element(source_params, current_page) 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_params, current_page) 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" @@ -1998,7 +1998,7 @@ def drag_and_drop(step_data): ######################### @logger -def take_screenshot_playwright(step_data): +async def take_screenshot_playwright(step_data): """ Take a screenshot. @@ -2050,13 +2050,13 @@ def take_screenshot_playwright(step_data): # Take screenshot if has_element: - locator = PlaywrightLocator.Get_Element(step_data, current_page) + locator = await PlaywrightLocator.Get_Element(step_data, current_page) if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" - locator.screenshot(path=screenshot_path) + await locator.screenshot(path=screenshot_path) else: - current_page.screenshot(path=screenshot_path, full_page=full_page) + await current_page.screenshot(path=screenshot_path, full_page=full_page) if save_variable: sr.Set_Shared_Variables(save_variable, screenshot_path) @@ -2075,7 +2075,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. @@ -2120,13 +2120,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) 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) @@ -2147,7 +2147,7 @@ def execute_javascript(step_data): ######################### @logger -def upload_file(step_data): +async def upload_file(step_data): """ Upload a file via file input. @@ -2190,12 +2190,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) 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" @@ -2274,7 +2274,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. @@ -2315,7 +2315,7 @@ def Wait_For_Element(step_data): if left_l == "timeout": timeout = int(float(right_v) * 1000) - locator = PlaywrightLocator.Get_Element(step_data, current_page, element_wait=0.1) + locator = await PlaywrightLocator.Get_Element(step_data, current_page, element_wait=0.1) if locator == "zeuz_failed": # For hidden/detached states, element not found is actually success @@ -2499,7 +2499,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. @@ -2534,13 +2534,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) 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 5e7420d8..0546330d 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): """ Get element using Playwright's native Locator API. @@ -119,7 +119,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 +128,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" @@ -471,7 +471,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 +487,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" From 6a5ee3a9ede227404c66ec6173a76f3e7ca9e53e Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Mon, 9 Feb 2026 13:23:25 +0600 Subject: [PATCH 04/41] Fix partial text match bug Partial match was not picking up action named "validate partial text" --- .../Built_In_Automation/Web/Playwright/BuiltInFunctions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 17fa91d9..4fdc6843 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -921,6 +921,8 @@ async 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": From 8471529b6fe199f119a000cd40da79d93982b04a Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Tue, 10 Feb 2026 18:13:56 +0600 Subject: [PATCH 05/41] Fix incorrect logic in Save Attribute Value action --- .../Web/Playwright/BuiltInFunctions.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 4fdc6843..5917cfb7 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -1047,16 +1047,18 @@ async 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) @@ -1094,10 +1096,10 @@ async def Save_Attribute(step_data): elif attr_lower == "disabled": value = await locator.is_disabled() else: - value = await 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: From 436369ded8e72cd7c429491ac9927b449cb90449 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Tue, 17 Feb 2026 14:54:55 +0600 Subject: [PATCH 06/41] Fix Playwright drag and drop action --- .../Web/Playwright/BuiltInFunctions.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 5917cfb7..d87933f4 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -1961,27 +1961,25 @@ async 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.strip() + elif mid_l.startswith("src"): + source_param = left.strip() # Get source element - source_locator = await PlaywrightLocator.Get_Element(source_params, current_page) + source_locator = await PlaywrightLocator.Get_Element(source_param, current_page) if source_locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Source element not found", 3) return "zeuz_failed" # Get target element - target_locator = await PlaywrightLocator.Get_Element(target_params, current_page) + target_locator = await PlaywrightLocator.Get_Element(target_param, current_page) if target_locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Target element not found", 3) return "zeuz_failed" From 3d2b45f05e2a9f8f1bd26a3edf199a1cc23e5d75 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Tue, 17 Feb 2026 15:12:45 +0600 Subject: [PATCH 07/41] Convert Tear Down Playwright action to async function --- .../Web/Playwright/BuiltInFunctions.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index d87933f4..2f7635f4 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -303,7 +303,7 @@ async 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. @@ -320,34 +320,34 @@ def Tear_Down_Playwright(step_data=None): for page_id, details in playwright_details.items(): try: if details.get("page"): - details["page"].close() + await details["page"].close() if details.get("context"): - details["context"].close() + await details["context"].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() + await current_page.close() except Exception: pass try: if context: - context.close() + await context.close() except Exception: pass try: if browser: - browser.close() + await browser.close() except Exception: pass try: if playwright_instance: - playwright_instance.stop() + await playwright_instance.stop() except Exception: pass From f37ad4fc34a8aac7afab88f6750f591bf2526df8 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Tue, 17 Feb 2026 15:50:54 +0600 Subject: [PATCH 08/41] Patch Playwright Wait For Element action with `sleep` --- .../Web/Playwright/BuiltInFunctions.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 2f7635f4..358ed545 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 @@ -2313,11 +2314,13 @@ async 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 = await 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) if locator == "zeuz_failed": # For hidden/detached states, element not found is actually success @@ -2327,11 +2330,11 @@ async 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" From c2f0744c7b22b62d1cd5f0bbcef61f4e49901972 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Tue, 17 Feb 2026 16:48:39 +0600 Subject: [PATCH 09/41] Added playwright conditional action handler --- .../Sequential_Actions/sequential_actions.py | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py b/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py index e590da74..5589450c 100755 --- a/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py +++ b/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py @@ -1984,6 +1984,37 @@ async 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 +2217,7 @@ async 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)) From 289ebf933433b255891b1519be4ea7625edec3f8 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Tue, 17 Feb 2026 17:18:39 +0600 Subject: [PATCH 10/41] Added support for iframe context switching in all Playwright actions --- .../Web/Playwright/BuiltInFunctions.py | 48 ++++++++++------- .../Web/Playwright/locator.py | 54 +++++++++++-------- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 358ed545..9c27916d 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -46,6 +46,13 @@ from Framework.Utilities.CommonUtil import passed_tag_list, failed_tag_list from . import locator as PlaywrightLocator +def _get_frame_locator(): + """Helper function to get current frame locator from shared variables.""" + frame_locator = sr.Get_Shared_Variables("playwright_frame") + if frame_locator in failed_tag_list: + return None + return frame_locator + ######################### # # # Global Variables # @@ -495,7 +502,7 @@ async def Click_Element(step_data): right_click = True # Get element - locator = await 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" @@ -609,7 +616,7 @@ async def Hover_Over_Element(step_data): elif left_l == "timeout": timeout = int(float(right_v) * 1000) - locator = await 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" @@ -686,7 +693,7 @@ async def Enter_Text_In_Text_Box(step_data): elif left_l == "timeout": timeout = int(float(right.strip()) * 1000) - locator = await 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" @@ -830,7 +837,7 @@ async def Keystroke_For_Element(step_data): key = key_map.get(key, keystroke_value) if has_element: - locator = await 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" @@ -853,7 +860,7 @@ async def Keystroke_For_Element(step_data): type_options["delay"] = int(delay * 1000) if has_element: - locator = await 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" @@ -930,7 +937,7 @@ async def Validate_Text(step_data): if left_l == "timeout": timeout = int(float(right_v) * 1000) - locator = await 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" @@ -995,7 +1002,7 @@ async def if_element_exists(step_data): if mid_l == "optional parameter" and left_l == "timeout": timeout = int(float(right_v) * 1000) - locator = await 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) @@ -1069,7 +1076,7 @@ async def Save_Attribute(step_data): CommonUtil.ExecLog(sModuleInfo, "No save variable specified", 3) return "zeuz_failed" - locator = await 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" @@ -1131,7 +1138,7 @@ async def get_element_info(step_data): if mid.strip().lower() == "save parameter": save_variable = left.strip() - locator = await 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" @@ -1363,7 +1370,7 @@ async def scroll_to_element(step_data): elif left_l == "align to top": align_to_top = right_v.lower() in ("true", "yes", "1") - locator = await 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" @@ -1443,7 +1450,7 @@ async def Select_Deselect(step_data): CommonUtil.ExecLog(sModuleInfo, "No selection value provided", 3) return "zeuz_failed" - locator = await 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" @@ -1521,7 +1528,7 @@ async def check_uncheck(step_data): if left_l == "use js": use_js = right_v in ("true", "yes", "1") - locator = await 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" @@ -1758,7 +1765,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. @@ -1806,6 +1813,7 @@ def switch_iframe(step_data): 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) CommonUtil.ExecLog(sModuleInfo, "Switched to default content", 1) return "passed" @@ -1974,13 +1982,13 @@ async def drag_and_drop(step_data): source_param = left.strip() # Get source element - source_locator = await PlaywrightLocator.Get_Element(source_param, 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 = await PlaywrightLocator.Get_Element(target_param, 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" @@ -2053,7 +2061,7 @@ async def take_screenshot_playwright(step_data): # Take screenshot if has_element: - locator = await 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" @@ -2123,7 +2131,7 @@ async def execute_javascript(step_data): # Execute JS if has_element: - locator = await 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" @@ -2193,7 +2201,7 @@ async def upload_file(step_data): CommonUtil.ExecLog(sModuleInfo, f"File not found: {file_path}", 3) return "zeuz_failed" - locator = await 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" @@ -2320,7 +2328,7 @@ async def Wait_For_Element(step_data): if timeout: await asyncio.sleep(timeout) - locator = await 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": # For hidden/detached states, element not found is actually success @@ -2539,7 +2547,7 @@ async def Extract_Table_Data(step_data): elif left_l == "column": col_filter = right_v - locator = await 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" diff --git a/Framework/Built_In_Automation/Web/Playwright/locator.py b/Framework/Built_In_Automation/Web/Playwright/locator.py index 0546330d..258b488f 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__) -async 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 @@ async 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 @@ async 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) @@ -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']: 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 From 1f0e02d91743d9b4a57e415c082b1173212963a4 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Wed, 18 Feb 2026 19:21:15 +0600 Subject: [PATCH 11/41] Fix drag and drop not passing correct step data to element locator --- .../Web/Playwright/BuiltInFunctions.py | 18 +++++++++++------- .../Web/Playwright/locator.py | 6 +++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 9c27916d..850dbe9c 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -48,10 +48,14 @@ def _get_frame_locator(): """Helper function to get current frame locator from shared variables.""" - frame_locator = sr.Get_Shared_Variables("playwright_frame") - if frame_locator in failed_tag_list: + 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 - return frame_locator ######################### # # @@ -1977,18 +1981,18 @@ async def drag_and_drop(step_data): mid_l = mid.strip().lower() if "element parameter" in mid_l: if mid_l.startswith("dst"): - target_param = left.strip() + target_param = (left, mid, right) elif mid_l.startswith("src"): - source_param = left.strip() + source_param = (left, mid, right) # Get source element - source_locator = await PlaywrightLocator.Get_Element(source_param, current_page, frame_locator=_get_frame_locator()) + 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 = await PlaywrightLocator.Get_Element(target_param, current_page, frame_locator=_get_frame_locator()) + 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" diff --git a/Framework/Built_In_Automation/Web/Playwright/locator.py b/Framework/Built_In_Automation/Web/Playwright/locator.py index 258b488f..9acbe5b8 100644 --- a/Framework/Built_In_Automation/Web/Playwright/locator.py +++ b/Framework/Built_In_Automation/Web/Playwright/locator.py @@ -199,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) @@ -303,8 +303,8 @@ def _build_locator(page, step_data, params, frame_locator=None): if left_lower in ("css", "css selector", "css_selector"): 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": From 2d0a01fe15c41745d1ec5bb013c96cfc2a5a2caa Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Sun, 22 Feb 2026 16:25:13 +0600 Subject: [PATCH 12/41] Convert the screenshot function to async Removes threading and uses async instead for `Thread_ScreenShot` --- .../Sequential_Actions/sequential_actions.py | 2 +- .../Web/Playwright/BuiltInFunctions.py | 11 +++++++++++ Framework/Utilities/CommonUtil.py | 17 +++++++++++------ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py b/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py index 5589450c..6cdb5916 100755 --- a/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py +++ b/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py @@ -2469,7 +2469,7 @@ async 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 diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 850dbe9c..7276a710 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -249,6 +249,9 @@ async 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 + + # Set screenshot variables for CommonUtil.TakeScreenShot() + CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) CommonUtil.ExecLog(sModuleInfo, f"Browser opened successfully (page_id: {page_id})", 1) return "passed" @@ -420,6 +423,9 @@ def Switch_Browser(step_data): sr.Set_Shared_Variables("playwright_page", current_page) sr.Set_Shared_Variables("playwright_context", context) + + # 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" @@ -1616,6 +1622,7 @@ def switch_window_or_tab(step_data): current_page = page 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: @@ -1623,6 +1630,7 @@ def switch_window_or_tab(step_data): current_page = page 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" @@ -1634,6 +1642,7 @@ def switch_window_or_tab(step_data): current_page = pages[switch_by_index] current_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 index {switch_by_index}: {current_page.title()}", 1) return "passed" else: @@ -1677,6 +1686,7 @@ def open_new_tab(step_data): new_page = context.new_page() current_page = new_page sr.Set_Shared_Variables("playwright_page", current_page) + CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) if url: new_page.goto(url) @@ -1755,6 +1765,7 @@ def close_tab(step_data): if pages: current_page = pages[-1] sr.Set_Shared_Variables("playwright_page", current_page) + CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) return "passed" diff --git a/Framework/Utilities/CommonUtil.py b/Framework/Utilities/CommonUtil.py index 2edf195f..de2847a4 100644 --- a/Framework/Utilities/CommonUtil.py +++ b/Framework/Utilities/CommonUtil.py @@ -869,14 +869,16 @@ 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 screen_capture_type == "web": # Selenium or Playwright driver object if "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: @@ -925,8 +927,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()) @@ -939,7 +940,7 @@ def pil_image_to_bytearray(img): return img_byte_array -def Thread_ScreenShot(function_name, image_folder, Method, Driver, image_name): +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 @@ -985,7 +986,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 hasattr(Driver, 'screenshot'): # Playwright page + await Driver.screenshot(path=ImageName, full_page=True) + else: # Selenium driver + Driver.get_screenshot_as_file(ImageName) # Must be .png, otherwise an exception occurs # Capture screenshot of mobile elif Method == "mobile": From 0624017bde377544f8e14814c60b7f9d3bb71a73 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Wed, 25 Feb 2026 16:19:41 +0600 Subject: [PATCH 13/41] Add automatic detection and downloading of Chromium in Playwright Browser Path --- .../Web/Playwright/BuiltInFunctions.py | 12 +++++ .../Web/Playwright/utils.py | 54 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 Framework/Built_In_Automation/Web/Playwright/utils.py diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 7276a710..4e2210df 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -45,6 +45,8 @@ ) from Framework.Utilities.CommonUtil import passed_tag_list, failed_tag_list from . import locator as PlaywrightLocator +from . import utils as PlaywrightUtils +from settings import ZEUZ_NODE_DOWNLOADS_DIR def _get_frame_locator(): """Helper function to get current frame locator from shared variables.""" @@ -178,6 +180,16 @@ async def Open_Browser(step_data): # Handle Selenium-style capabilities where possible pass + # Set playwright browser path environment variable + os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(PlaywrightUtils.PW_BROWSERS_DIR) + + # Check if pw-browsers folder exists, download Chromium if needed + if not PlaywrightUtils.check_playwright_browser_exists(): + CommonUtil.ExecLog(sModuleInfo, "Playwright browser not found, downloading Chromium...", 2) + if not PlaywrightUtils.download_playwright_browser(): + CommonUtil.ExecLog(sModuleInfo, "Failed to download Playwright browser", 2) + return "zeuz_failed" + # Launch Playwright CommonUtil.ExecLog(sModuleInfo, f"Launching Playwright with {browser_name} browser", 1) playwright_instance = await async_playwright().start() 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 00000000..53910f09 --- /dev/null +++ b/Framework/Built_In_Automation/Web/Playwright/utils.py @@ -0,0 +1,54 @@ +# -*- 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 subprocess +import os +import sys +from pathlib import Path +from Framework.Utilities import CommonUtil +from settings import ZEUZ_NODE_DOWNLOADS_DIR + +PW_BROWSERS_DIR = ZEUZ_NODE_DOWNLOADS_DIR / "pw-browsers" + +def check_playwright_browser_exists(): + if not PW_BROWSERS_DIR.exists(): + return False + + # Check if a folder name exists that starts with "chromium-" + for folder in PW_BROWSERS_DIR.iterdir(): + if folder.name.startswith("chromium-"): + # Check if a file named "INSTALLATION_COMPLETE" exist + if (folder / "INSTALLATION_COMPLETE").exists(): + return True + return False + + +def download_playwright_browser(brand="chromium", download_path=PW_BROWSERS_DIR): + """ + Download Playwright browser for the specified brand. + + Args: + brand (str): Browser brand to download (default: "chromium") + download_path (Path): Path to download the browser to (default: PW_BROWSERS_DIR) + """ + + os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(download_path) + + CommonUtil.ExecLog("", f"Downloading Playwright browser: {brand} to {os.environ['PLAYWRIGHT_BROWSERS_PATH']}", 2) + # Execute the command with the current Python instance (venv) + python_path = sys.executable + install = subprocess.run([python_path, "-m", "playwright", "install", "--no-shell", brand]) + + if install.returncode == 0: + CommonUtil.ExecLog("", f"Playwright browser downloaded successfully: {brand}", 2) + return True + else: + CommonUtil.ExecLog("", f"Failed to download Playwright browser: {brand}", 2) + return False From 585193eea4291aac73a6495484bf5c366be8241d Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Wed, 25 Feb 2026 16:36:03 +0600 Subject: [PATCH 14/41] Add Playwright tear down in cleanup driver function --- Framework/MainDriverApi.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Framework/MainDriverApi.py b/Framework/MainDriverApi.py index 9e32bce3..729368cb 100644 --- a/Framework/MainDriverApi.py +++ b/Framework/MainDriverApi.py @@ -780,13 +780,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: Selenium.Tear_Down_Selenium() except: pass + + try: + await Playwright.Tear_Down_Playwright() + 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") @@ -1116,7 +1123,7 @@ async 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": @@ -1928,7 +1935,7 @@ async 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"] @@ -1949,7 +1956,7 @@ async 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") From f7a50bad692eaa2d56ef0f88b9909fca207e75de Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Wed, 25 Feb 2026 16:55:43 +0600 Subject: [PATCH 15/41] Reset Playwright iframe context when performing Go_To_Link action --- .../Built_In_Automation/Web/Playwright/BuiltInFunctions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 4e2210df..47208bf3 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -322,6 +322,10 @@ async def Go_To_Link(step_data): goto_options["timeout"] = timeout await current_page.goto(url, **goto_options) + + # Reset frame context when navigating to a new URL + sr.Set_Shared_Variables("playwright_frame", None) + CommonUtil.ExecLog(sModuleInfo, f"Navigated to: {url}", 1) return "passed" From 54c2bf618a96b9d1e85051761a46e9bbfd620965 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Mon, 2 Mar 2026 10:31:48 +0600 Subject: [PATCH 16/41] Replace Playwright auto browser download with the Selenium CfT functions --- .../Web/Playwright/BuiltInFunctions.py | 18 +++--- .../Web/Playwright/utils.py | 59 ++++++++----------- 2 files changed, 34 insertions(+), 43 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 47208bf3..c5e5b90e 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -180,15 +180,10 @@ async def Open_Browser(step_data): # Handle Selenium-style capabilities where possible pass - # Set playwright browser path environment variable - os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(PlaywrightUtils.PW_BROWSERS_DIR) - - # Check if pw-browsers folder exists, download Chromium if needed - if not PlaywrightUtils.check_playwright_browser_exists(): - CommonUtil.ExecLog(sModuleInfo, "Playwright browser not found, downloading Chromium...", 2) - if not PlaywrightUtils.download_playwright_browser(): - CommonUtil.ExecLog(sModuleInfo, "Failed to download Playwright browser", 2) - return "zeuz_failed" + # Ensure Chrome for Testing is available + chrome_binary_path, success = PlaywrightUtils.ensure_chromium_downloads(sModuleInfo) + if not success: + return "zeuz_failed" # Launch Playwright CommonUtil.ExecLog(sModuleInfo, f"Launching Playwright with {browser_name} browser", 1) @@ -204,6 +199,11 @@ async def Open_Browser(step_data): launch_options["args"] = args if downloads_path: launch_options["downloads_path"] = downloads_path + + # Use Chrome for Testing binary if available + if chrome_binary_path and browser_name in ("chrome", "chromium"): + launch_options["executable_path"] = chrome_binary_path + CommonUtil.ExecLog(sModuleInfo, f"Using Chrome for Testing binary: {chrome_binary_path}", 1) # Select and launch browser if browser_name in ("chrome", "chromium"): diff --git a/Framework/Built_In_Automation/Web/Playwright/utils.py b/Framework/Built_In_Automation/Web/Playwright/utils.py index 53910f09..731b88fa 100644 --- a/Framework/Built_In_Automation/Web/Playwright/utils.py +++ b/Framework/Built_In_Automation/Web/Playwright/utils.py @@ -8,47 +8,38 @@ Author: Zeuz/AutomationSolutionz """ -import subprocess import os -import sys from pathlib import Path from Framework.Utilities import CommonUtil -from settings import ZEUZ_NODE_DOWNLOADS_DIR +from Framework.Built_In_Automation.Web.Selenium.utils import ChromeForTesting -PW_BROWSERS_DIR = ZEUZ_NODE_DOWNLOADS_DIR / "pw-browsers" +# Initialize Chrome for Testing instance +chrome_for_testing = ChromeForTesting() -def check_playwright_browser_exists(): - if not PW_BROWSERS_DIR.exists(): - return False - # Check if a folder name exists that starts with "chromium-" - for folder in PW_BROWSERS_DIR.iterdir(): - if folder.name.startswith("chromium-"): - # Check if a file named "INSTALLATION_COMPLETE" exist - if (folder / "INSTALLATION_COMPLETE").exists(): - return True - return False - - -def download_playwright_browser(brand="chromium", download_path=PW_BROWSERS_DIR): +def ensure_chromium_downloads(sModuleInfo): """ - Download Playwright browser for the specified brand. + Ensure Chrome for Testing is available for Playwright. Args: - brand (str): Browser brand to download (default: "chromium") - download_path (Path): Path to download the browser to (default: PW_BROWSERS_DIR) + sModuleInfo: Module information for logging + + Returns: + tuple: (chrome_binary_path, success_flag) where success_flag is True if successful """ - - os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(download_path) - - CommonUtil.ExecLog("", f"Downloading Playwright browser: {brand} to {os.environ['PLAYWRIGHT_BROWSERS_PATH']}", 2) - # Execute the command with the current Python instance (venv) - python_path = sys.executable - install = subprocess.run([python_path, "-m", "playwright", "install", "--no-shell", brand]) - - if install.returncode == 0: - CommonUtil.ExecLog("", f"Playwright browser downloaded successfully: {brand}", 2) - return True - else: - CommonUtil.ExecLog("", f"Failed to download Playwright browser: {brand}", 2) - return False + try: + CommonUtil.ExecLog(sModuleInfo, "Setting up Chrome for Testing for Playwright...", 1) + + # Use Chrome for Testing to get Chrome binary + chrome_bin, driver_bin = chrome_for_testing.setup_chrome_for_testing() + + if chrome_bin and chrome_bin.exists(): + CommonUtil.ExecLog(sModuleInfo, f"Chrome for Testing ready: {chrome_bin}", 1) + return str(chrome_bin), True + else: + CommonUtil.ExecLog(sModuleInfo, "Failed to setup Chrome for Testing", 3) + return None, False + + except Exception as e: + CommonUtil.ExecLog(sModuleInfo, f"Error setting up Chrome for Testing: {str(e)}", 3) + return None, False From e2255a72fb2b603d4b22c31af1261ddb7a2a336f Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Mon, 2 Mar 2026 14:48:45 +0600 Subject: [PATCH 17/41] Add support for `browser driver` as `optional parameter` --- .../Sequential_Actions/sequential_actions.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py b/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py index 6cdb5916..cced453e 100755 --- a/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py +++ b/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py @@ -2327,6 +2327,34 @@ def compare_variable_names(set, dataset): CommonUtil.compare_action_varnames = {"left": "Left", "right": "Right"} +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: The original action subfield (e.g., "selenium action", "playwright action") + data_set: The data set containing optional parameters + + Returns: + Updated action_subfield based on browser driver parameter + """ + if action_subfield not in ("playwright action", "selenium action"): + return action_subfield + + for left, mid, right in data_set: + if (mid.strip().lower().startswith("optional") + and left.strip().lower() == "browser driver" + and right.strip().lower() in ("playwright", "selenium")): + + updated_action_subfield = right.strip().lower() + " action" + if action_subfield != updated_action_subfield: + CommonUtil.ExecLog("", "Browser driver changed from %s to %s" % (action_subfield, updated_action_subfield), 1) + + return updated_action_subfield + + return 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 """ @@ -2337,6 +2365,9 @@ async 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) From 151c2aba02dacd37f6571b623ddc8661203e95f1 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Mon, 2 Mar 2026 16:15:57 +0600 Subject: [PATCH 18/41] Add support for `BROWSER_DRIVER` as runtime parameter --- .../Sequential_Actions/sequential_actions.py | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py b/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py index cced453e..301b7087 100755 --- a/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py +++ b/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py @@ -2332,27 +2332,55 @@ 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: The original action subfield (e.g., "selenium action", "playwright action") - data_set: The data set containing optional parameters + action_subfield (str): The original action subfield (e.g., "selenium action", "playwright action") + data_set (list): The data set containing optional parameters Returns: - Updated action_subfield based on browser driver parameter + 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 + " 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")): - - updated_action_subfield = right.strip().lower() + " action" - if action_subfield != updated_action_subfield: - CommonUtil.ExecLog("", "Browser driver changed from %s to %s" % (action_subfield, updated_action_subfield), 1) - return updated_action_subfield + # 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 - return action_subfield + # 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): From 40959232af150a882bd116eb1ee8e869b7610603 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Mon, 2 Mar 2026 20:50:30 +0600 Subject: [PATCH 19/41] Auto connect Playwright to Selenium browsers --- .../Web/Selenium/BuiltInFunctions.py | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index bb523ea0..49b578b8 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -71,6 +71,8 @@ from Framework.AI.NLP import binary_classification from .utils import ChromeForTesting, ChromeExtensionDownloader +from playwright.async_api import async_playwright + ######################### # # # Global Variables # @@ -656,8 +658,24 @@ 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.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 "passed" + + @logger -def Open_Browser(browser, browser_options: BrowserOptions): +async def Open_Browser(browser, browser_options: BrowserOptions): """Launch browser from options and service object""" try: global selenium_driver @@ -687,6 +705,9 @@ def Open_Browser(browser, browser_options: BrowserOptions): options = generate_options(browser, browser_options) + # Enable remote debugging / CDP + options.add_argument("--remote-debugging-port=9222") + if browser in ("android", "chrome", "chromeheadless"): from selenium.webdriver.chrome.service import Service @@ -756,6 +777,13 @@ def Open_Browser(browser, browser_options: BrowserOptions): ) return "zeuz_failed" + # Connect Playwright to Selenium via CDP + try: + await connect_playwright_to_selenium(port=9222) + CommonUtil.ExecLog(sModuleInfo, "Connected Playwright to Selenium", 1) + except Exception as e: + CommonUtil.ExecLog(sModuleInfo, f"Failed to connect Playwright to Selenium: {e}", 3) + CommonUtil.ExecLog(sModuleInfo, f"Started {browser} browser", 1) Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) CommonUtil.set_screenshot_vars(Shared_Resources.Shared_Variable_Export()) @@ -915,7 +943,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 @@ -1117,7 +1145,7 @@ 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) == "zeuz_failed": return "zeuz_failed" if ConfigModule.get_config_value( @@ -1191,7 +1219,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) else: result = "zeuz_failed" From 7bf65e1a4d00265fc55623fff3fa0416c779426b Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Mon, 2 Mar 2026 21:21:43 +0600 Subject: [PATCH 20/41] Auto connect Selenium to Playwright browsers --- .../Web/Playwright/BuiltInFunctions.py | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index c5e5b90e..bfca34b7 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -59,6 +59,30 @@ def _get_frame_locator(): # Variable doesn't exist yet return None + +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 "passed" + + except Exception as e: + CommonUtil.ExecLog("connect_selenium_to_playwright", f"Failed to connect Selenium to Playwright: {e}", 3) + return "zeuz_failed" + ######################### # # # Global Variables # @@ -195,8 +219,11 @@ async def Open_Browser(step_data): "slow_mo": slow_mo, "devtools": devtools, } - if args: - launch_options["args"] = args + + # Add remote debugging port for CDP connection + all_args = args + ["--remote-debugging-port=9222"] + if all_args: + launch_options["args"] = all_args if downloads_path: launch_options["downloads_path"] = downloads_path @@ -265,6 +292,9 @@ async def Open_Browser(step_data): # Set screenshot variables for CommonUtil.TakeScreenShot() CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) + # Connect Selenium to Playwright via CDP + connect_selenium_to_playwright(port=9222) + CommonUtil.ExecLog(sModuleInfo, f"Browser opened successfully (page_id: {page_id})", 1) return "passed" From b60764814fea10ca877ec5347021b09c07ed8303 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Thu, 5 Mar 2026 11:57:02 +0600 Subject: [PATCH 21/41] Add utility functions for browser sessions --- Framework/Built_In_Automation/Web/utils.py | 69 ++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 Framework/Built_In_Automation/Web/utils.py diff --git a/Framework/Built_In_Automation/Web/utils.py b/Framework/Built_In_Automation/Web/utils.py new file mode 100644 index 00000000..7678824a --- /dev/null +++ b/Framework/Built_In_Automation/Web/utils.py @@ -0,0 +1,69 @@ +from Framework.Built_In_Automation.Shared_Resources import ( + BuiltInFunctionSharedResources as sr, +) + + + +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 create_browser_session( + session_name: str = "default", + selenium_driver: WebDriver | None = None, + playwright_page: Page | None = None, + playwright_browser: Browser | None = None, + playwright_context: BrowserContext | None = None, + playwright_frame: Frame | None = 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 (WebDriver): 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. + """ + + if sr.Test_Shared_Variables("browser_sessions") == False: + initialize_browser_sessions() + + browser_sessions = sr.Get_Shared_Variables("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 + } + 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. + """ + + if sr.Test_Shared_Variables("browser_sessions") == False: + initialize_browser_sessions() + + browser_sessions = sr.Get_Shared_Variables("browser_sessions") + return browser_sessions.get(session_name, {}) From a9b1df513448fc9d181d005cad4f3fb748c55db5 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Thu, 5 Mar 2026 12:28:57 +0600 Subject: [PATCH 22/41] Fix type checking --- Framework/Built_In_Automation/Web/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Framework/Built_In_Automation/Web/utils.py b/Framework/Built_In_Automation/Web/utils.py index 7678824a..8329b25c 100644 --- a/Framework/Built_In_Automation/Web/utils.py +++ b/Framework/Built_In_Automation/Web/utils.py @@ -1,6 +1,8 @@ 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 @@ -16,7 +18,7 @@ def initialize_browser_sessions(): def create_browser_session( session_name: str = "default", - selenium_driver: WebDriver | None = None, + selenium_driver: Chrome | Firefox | Edge | Safari | None = None, playwright_page: Page | None = None, playwright_browser: Browser | None = None, playwright_context: BrowserContext | None = None, @@ -28,7 +30,7 @@ def create_browser_session( Args: session_name (str): The name of the session. - selenium_driver (WebDriver): The Selenium WebDriver instance. + 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. From dab66f3f4b2183d32192783e8bc60821b561732e Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Thu, 5 Mar 2026 12:30:01 +0600 Subject: [PATCH 23/41] Add session creation in Selenium open browser action --- .../Web/Selenium/BuiltInFunctions.py | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index 49b578b8..f376c67d 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -671,11 +671,11 @@ async def connect_playwright_to_selenium(port=9222): Shared_Resources.Set_Shared_Variables("playwright_browser", browser) Shared_Resources.Set_Shared_Variables("playwright_page", page) - return "passed" + return browser, context, page @logger -async 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 @@ -778,12 +778,27 @@ async def Open_Browser(browser, browser_options: BrowserOptions): return "zeuz_failed" # Connect Playwright to Selenium via CDP + playwright_browser = None + playwright_context = None + playwright_page = None try: - await connect_playwright_to_selenium(port=9222) + playwright_browser, playwright_context, playwright_page = await connect_playwright_to_selenium(port=9222) CommonUtil.ExecLog(sModuleInfo, "Connected Playwright to Selenium", 1) except Exception as e: CommonUtil.ExecLog(sModuleInfo, f"Failed to connect Playwright to Selenium: {e}", 3) + # Create browser session + from Framework.Built_In_Automation.Web.utils import create_browser_session + session = create_browser_session( + session_name=session_name, + selenium_driver=selenium_driver, + playwright_page=playwright_page, + playwright_browser=playwright_browser, + playwright_context=playwright_context, + playwright_frame=None + ) + 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) CommonUtil.set_screenshot_vars(Shared_Resources.Shared_Variable_Export()) @@ -1018,6 +1033,7 @@ async 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() @@ -1037,6 +1053,8 @@ async 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 = right.strip() # Capabilities are WebDriver attribute common across different browser elif mid.strip().lower() == "shared capability": @@ -1145,7 +1163,7 @@ async def Go_To_Link(dataset: Dataset) -> ReturnType: sModuleInfo, "Browser not previously opened, doing so now", 1 ) - if await Open_Browser(dependency["Browser"], browser_options) == "zeuz_failed": + if await Open_Browser(dependency["Browser"], browser_options, session_name) == "zeuz_failed": return "zeuz_failed" if ConfigModule.get_config_value( @@ -1219,7 +1237,7 @@ async 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 = await Open_Browser(dependency["Browser"], browser_options) + result = await Open_Browser(dependency["Browser"], browser_options, session_name) else: result = "zeuz_failed" From 422a0abd3e086abedf0144d2b1bfe7044636d3d1 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Thu, 5 Mar 2026 12:44:35 +0600 Subject: [PATCH 24/41] Add type checking for `selenium_driver` --- .../Web/Selenium/BuiltInFunctions.py | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index f376c67d..53253d9f 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -777,27 +777,30 @@ async def Open_Browser(browser, browser_options: BrowserOptions, session_name: s ) return "zeuz_failed" - # Connect Playwright to Selenium via CDP - playwright_browser = None - playwright_context = None - playwright_page = None - try: - playwright_browser, playwright_context, playwright_page = await connect_playwright_to_selenium(port=9222) - CommonUtil.ExecLog(sModuleInfo, "Connected Playwright to Selenium", 1) - except Exception as e: - CommonUtil.ExecLog(sModuleInfo, f"Failed to connect Playwright to Selenium: {e}", 3) - - # Create browser session - from Framework.Built_In_Automation.Web.utils import create_browser_session - session = create_browser_session( - session_name=session_name, - selenium_driver=selenium_driver, - playwright_page=playwright_page, - playwright_browser=playwright_browser, - playwright_context=playwright_context, - playwright_frame=None - ) - CommonUtil.ExecLog(sModuleInfo, f"Created browser session: {session_name=}", 5) + # If selenium_driver is of type Webdriver + from selenium.webdriver import Chrome, Firefox, Edge, Safari + if isinstance(selenium_driver, (Chrome, Firefox, Edge, Safari)): + # Connect Playwright to Selenium via CDP + playwright_browser = None + playwright_context = None + playwright_page = None + try: + playwright_browser, playwright_context, playwright_page = await connect_playwright_to_selenium(port=9222) + CommonUtil.ExecLog(sModuleInfo, "Connected Playwright to Selenium", 1) + except Exception as e: + CommonUtil.ExecLog(sModuleInfo, f"Failed to connect Playwright to Selenium: {e}", 3) + + # Create browser session + from Framework.Built_In_Automation.Web.utils import create_browser_session + session = create_browser_session( + session_name=session_name, + selenium_driver=selenium_driver, + playwright_page=playwright_page, + playwright_browser=playwright_browser, + playwright_context=playwright_context, + playwright_frame=None + ) + 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) From ec2751bfc03ba375ad3440e5b133ecff5447cf2e Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Thu, 5 Mar 2026 13:20:23 +0600 Subject: [PATCH 25/41] Add session creation in Playwright open browser action --- .../Web/Playwright/BuiltInFunctions.py | 21 +++++++++++++++---- .../Web/Selenium/BuiltInFunctions.py | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index bfca34b7..78e297e2 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -77,7 +77,7 @@ def connect_selenium_to_playwright(port=9222): sr.Set_Shared_Variables("selenium_driver", driver) CommonUtil.ExecLog("connect_selenium_to_playwright", "Connected Selenium to Playwright", 1) - return "passed" + return driver except Exception as e: CommonUtil.ExecLog("connect_selenium_to_playwright", f"Failed to connect Selenium to Playwright: {e}", 3) @@ -168,8 +168,6 @@ async 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"): - page_id = right_v elif mid_l == "optional parameter": if left_l == "headless": @@ -199,6 +197,8 @@ async 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 @@ -293,7 +293,20 @@ async def Open_Browser(step_data): CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) # Connect Selenium to Playwright via CDP - connect_selenium_to_playwright(port=9222) + selenium_driver = connect_selenium_to_playwright(port=9222) + + # Create browser session + from Framework.Built_In_Automation.Web.utils import create_browser_session + + create_browser_session( + session_name=page_id, + selenium_driver=selenium_driver, + playwright_page=current_page, + playwright_browser=browser, + playwright_context=context, + playwright_frame=None + ) + CommonUtil.ExecLog(sModuleInfo, f"Created browser session: {page_id}", 5) CommonUtil.ExecLog(sModuleInfo, f"Browser opened successfully (page_id: {page_id})", 1) return "passed" diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index 53253d9f..73e05280 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -792,7 +792,7 @@ async def Open_Browser(browser, browser_options: BrowserOptions, session_name: s # Create browser session from Framework.Built_In_Automation.Web.utils import create_browser_session - session = create_browser_session( + create_browser_session( session_name=session_name, selenium_driver=selenium_driver, playwright_page=playwright_page, From 816928d47123142b0c4d1c3539ba443e4d7a24fd Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Thu, 5 Mar 2026 15:58:16 +0600 Subject: [PATCH 26/41] Add session checking in Playwright go to link action --- .../Web/Playwright/BuiltInFunctions.py | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 78e297e2..b96c794a 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -332,7 +332,59 @@ async def Go_To_Link(step_data): global current_page try: - if current_page is None: + # 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: + from Framework.Built_In_Automation.Web.utils import get_browser_session + existing_session = get_browser_session(session_name) + + if existing_session and existing_session.get("playwright_page"): + # Session exists, use existing browser + current_page = existing_session["playwright_page"] + context = existing_session["playwright_context"] + browser = existing_session["playwright_browser"] + current_page_id = session_name + + # Update globals + globals().update({ + 'current_page': current_page, + 'context': context, + 'browser': browser, + 'current_page_id': current_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"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: + # 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": @@ -351,7 +403,10 @@ async def Go_To_Link(step_data): 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", "wait time"): + 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) From 13653a4fe4338d123fe1be442ce16272bbceee9b Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Thu, 5 Mar 2026 21:09:24 +0600 Subject: [PATCH 27/41] Add session checking in Selenium go to link action --- .../Web/Selenium/BuiltInFunctions.py | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index 73e05280..775324b1 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -72,6 +72,7 @@ from .utils import ChromeForTesting, ChromeExtensionDownloader from playwright.async_api import async_playwright +from Framework.Built_In_Automation.Web.utils import get_browser_session, create_browser_session ######################### # # @@ -466,7 +467,12 @@ def Open_Electron_App(data_set): opts = Options() opts.binary_location = desktop_app_path - opts.add_argument("--remote-debugging-port=9222") + # Generate unique port for Electron app based on driver_id + import hashlib + port_hash = int(hashlib.md5((driver_id or "electron").encode()).hexdigest(), 16) + electron_port = 9230 + (port_hash % 90) # Range 9230-9320 to avoid conflicts with browser sessions + 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": @@ -705,8 +711,13 @@ async def Open_Browser(browser, browser_options: BrowserOptions, session_name: s options = generate_options(browser, browser_options) - # Enable remote debugging / CDP - options.add_argument("--remote-debugging-port=9222") + # Enable remote debugging / CDP with unique port per session + import hashlib + # Generate unique port based on session name (range 9222-9322 to avoid conflicts) + port_hash = int(hashlib.md5(session_name.encode()).hexdigest(), 16) + unique_port = 9222 + (port_hash % 100) + 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 @@ -785,13 +796,12 @@ async def Open_Browser(browser, browser_options: BrowserOptions, session_name: s playwright_context = None playwright_page = None try: - playwright_browser, playwright_context, playwright_page = await connect_playwright_to_selenium(port=9222) + playwright_browser, playwright_context, playwright_page = await connect_playwright_to_selenium(port=unique_port) CommonUtil.ExecLog(sModuleInfo, "Connected Playwright to Selenium", 1) except Exception as e: CommonUtil.ExecLog(sModuleInfo, f"Failed to connect Playwright to Selenium: {e}", 3) # Create browser session - from Framework.Built_In_Automation.Web.utils import create_browser_session create_browser_session( session_name=session_name, selenium_driver=selenium_driver, @@ -1043,7 +1053,7 @@ async def Go_To_Link(dataset: Dataset) -> ReturnType: 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()) @@ -1057,7 +1067,7 @@ async def Go_To_Link(dataset: Dataset) -> ReturnType: elif left == "chrome:version": chrome_version = right.strip() elif left == "session": - session_name = right.strip() + session_name = driver_id = right.strip() # Capabilities are WebDriver attribute common across different browser elif mid.strip().lower() == "shared capability": @@ -1169,6 +1179,8 @@ async def Go_To_Link(dataset: Dataset) -> ReturnType: if await Open_Browser(dependency["Browser"], browser_options, session_name) == "zeuz_failed": return "zeuz_failed" + selenium_driver = get_browser_session(session_name)["selenium_driver"] + if ConfigModule.get_config_value( "RunDefinition", "window_size_x" ) and ConfigModule.get_config_value("RunDefinition", "window_size_y"): @@ -1193,7 +1205,7 @@ async def Go_To_Link(dataset: Dataset) -> ReturnType: "remote-debugging-port": debug_port } else: - selenium_driver = selenium_details[driver_id]["driver"] + selenium_driver = get_browser_session(session_name)["selenium_driver"] Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) current_driver_id = driver_id except Exception: @@ -4622,16 +4634,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", 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')]") From ea06e997ba5611e4af36ad0dff38830b00202be5 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Thu, 5 Mar 2026 21:15:41 +0600 Subject: [PATCH 28/41] Add CDP port hashing to Playwright open browser action Similar to Selenium open browser action --- .../Web/Playwright/BuiltInFunctions.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index b96c794a..7c76b886 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -220,8 +220,13 @@ async def Open_Browser(step_data): "devtools": devtools, } - # Add remote debugging port for CDP connection - all_args = args + ["--remote-debugging-port=9222"] + # Add remote debugging port for CDP connection with unique port per session + import hashlib + # Generate unique port based on page_id (range 9222-9322 to avoid conflicts) + port_hash = int(hashlib.md5(page_id.encode()).hexdigest(), 16) + unique_port = 9222 + (port_hash % 100) + all_args = args + [f"--remote-debugging-port={unique_port}"] + 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: @@ -293,7 +298,7 @@ async def Open_Browser(step_data): CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) # Connect Selenium to Playwright via CDP - selenium_driver = connect_selenium_to_playwright(port=9222) + selenium_driver = connect_selenium_to_playwright(port=unique_port) # Create browser session from Framework.Built_In_Automation.Web.utils import create_browser_session From 0b255b1ecfb2463be10e9c8a590a4bbffd96709c Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Thu, 5 Mar 2026 21:32:20 +0600 Subject: [PATCH 29/41] Add session checking to commonly used actions in Playwright and Selenium --- .../Web/Playwright/BuiltInFunctions.py | 77 +++++++++++++++++++ .../Web/Selenium/BuiltInFunctions.py | 70 +++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 7c76b886..12f7444d 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -112,6 +112,69 @@ def connect_selenium_to_playwright(port=9222): # # ######################### +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 + + # Parse session parameter + 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 + + # If session parameter is provided, switch to that session + if session_name: + from Framework.Built_In_Automation.Web.utils import get_browser_session + existing_session = get_browser_session(session_name) + + if existing_session and existing_session.get("playwright_page"): + # Session exists, use existing browser + current_page = existing_session["playwright_page"] + context = existing_session["playwright_context"] + browser = existing_session["playwright_browser"] + current_page_id = session_name + + # Update globals + globals().update({ + 'current_page': current_page, + 'context': context, + 'browser': browser, + 'current_page_id': current_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) + + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + CommonUtil.ExecLog(sModuleInfo, f"Using existing browser session: {session_name}", 1) + else: + # Session doesn't exist + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + CommonUtil.ExecLog(sModuleInfo, f"Session '{session_name}' not found. Using current browser.", 2) + + return session_name, current_page, current_page_id, context, browser + + @logger async def Open_Browser(step_data): """ @@ -590,6 +653,9 @@ async def Click_Element(step_data): global current_page try: + # Handle session parameter + session_name, current_page, current_page_id, context, browser = _handle_playwright_session(step_data) + if current_page is None: CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) return "zeuz_failed" @@ -609,6 +675,10 @@ async 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") @@ -796,6 +866,9 @@ async def Enter_Text_In_Text_Box(step_data): global current_page try: + # Handle session parameter + session_name, current_page, current_page_id, context, browser = _handle_playwright_session(step_data) + if current_page is None: CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) return "zeuz_failed" @@ -810,6 +883,10 @@ async 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": diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index 775324b1..95594f3d 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -150,6 +150,50 @@ ReturnType = Literal["passed", "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 + + # Parse session parameter + session_name = None + for left, mid, right in step_data: + left = left.replace(" ", "").replace("_", "").replace("-", "").lower() + if left == "session" and mid.strip().lower() == "optional parameter": + session_name = right.strip() + break + + # If session parameter is provided, switch to that session + if session_name: + from Framework.Built_In_Automation.Web.utils import get_browser_session + existing_session = get_browser_session(session_name) + + if existing_session and existing_session.get("selenium_driver"): + # Session exists, use existing driver + selenium_driver = existing_session["selenium_driver"] + current_driver_id = session_name + Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) + + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + CommonUtil.ExecLog(sModuleInfo, f"Using existing browser session: {session_name}", 1) + else: + # Session doesn't exist + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + CommonUtil.ExecLog(sModuleInfo, f"Session '{session_name}' not found. Using current browser.", 2) + + return session_name, selenium_driver, current_driver_id + + class DefaultChromiumArguments(TypedDict): add_argument: list[str] add_experimental_option: dict[str, Any] @@ -1645,6 +1689,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( @@ -1654,6 +1702,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": @@ -2088,8 +2139,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 @@ -3035,6 +3092,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 @@ -3044,6 +3104,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] @@ -3149,6 +3212,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 = "" @@ -3156,6 +3222,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": From e8e79d1b040ebaddca24fd6556553eb7f31022fa Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Thu, 5 Mar 2026 22:16:38 +0600 Subject: [PATCH 30/41] Add session support in tear down browser actions --- .../Web/Playwright/BuiltInFunctions.py | 153 +++++++++++++----- .../Web/Selenium/BuiltInFunctions.py | 56 ++++++- 2 files changed, 170 insertions(+), 39 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 12f7444d..c59268ac 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -507,57 +507,136 @@ async def Tear_Down_Playwright(step_data=None): 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: + from Framework.Built_In_Automation.Web.utils import get_browser_session + 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"] + + if session_page: + await session_page.close() + if session_context: + await session_context.close() + + 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 session from browser_sessions + browser_sessions = sr.Get_Shared_Variables("browser_sessions", {}) + if session_name in browser_sessions: + del browser_sessions[session_name] + sr.Set_Shared_Variables("browser_sessions", browser_sessions) + + # 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) + + # Handle full teardown (backwards compatibility) + else: + # Close all tracked pages/contexts + for page_id, details in playwright_details.items(): + try: + if details.get("page"): + await details["page"].close() + if details.get("context"): + await details["context"].close() + except Exception: + pass + + # Close main instances try: - if details.get("page"): - await details["page"].close() - if details.get("context"): - await 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()]: - await current_page.close() - except Exception: - pass - - try: - if context: - await context.close() - except Exception: - pass + try: + if context: + await context.close() + except Exception: + pass - try: - if browser: - await browser.close() - except Exception: - pass + try: + if browser: + await browser.close() + except Exception: + pass - try: - if playwright_instance: - await playwright_instance.stop() - except Exception: - pass + try: + if playwright_instance: + await 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 + # Reset all globals + current_page = None + context = None + browser = None + playwright_instance = None + playwright_details = {} + current_page_id = None + + # Clear all browser sessions + sr.Set_Shared_Variables("browser_sessions", {}) - CommonUtil.ExecLog(sModuleInfo, "Browser closed successfully", 1) - return "passed" + CommonUtil.ExecLog(sModuleInfo, "Browser closed successfully", 1) + return "passed" except Exception: return CommonUtil.Exception_Handler(sys.exc_info()) diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index 95594f3d..2772f25a 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -3502,12 +3502,62 @@ 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": driver_id = right.strip() - - if not driver_id: + elif left == "session" and mid.strip().lower() == "optional parameter": + session_name = right.strip() + # For backward compatibility, treat session_name as driver_id + driver_id = session_name + + # Handle session-specific teardown + if session_name: + from Framework.Built_In_Automation.Web.utils import get_browser_session + existing_session = get_browser_session(session_name) + + if existing_session and existing_session.get("selenium_driver"): + try: + # 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 session from browser_sessions + browser_sessions = Shared_Resources.Get_Shared_Variables("browser_sessions", {}) + if session_name in browser_sessions: + del browser_sessions[session_name] + Shared_Resources.Set_Shared_Variables("browser_sessions", browser_sessions) + + # 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 @@ -3525,6 +3575,8 @@ 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") + # Clear all browser sessions + Shared_Resources.Set_Shared_Variables("browser_sessions", {}) selenium_details = {} selenium_driver = None From f62ac45db614452c0f599475494c37b588047d51 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Wed, 25 Mar 2026 18:45:50 +0600 Subject: [PATCH 31/41] Normalize routed driver name before building action subfield --- .../Sequential_Actions/sequential_actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py b/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py index 301b7087..bf43ab14 100755 --- a/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py +++ b/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py @@ -2358,7 +2358,7 @@ def get_browser_driver_routing(action_subfield, data_set): # 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 + " action" + 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: From 9b2020a389d65773d84a53d4ee2476dd59bdbeb1 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Wed, 25 Mar 2026 18:46:18 +0600 Subject: [PATCH 32/41] Populate Playwright globals when attaching to Selenium session --- Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index 49b578b8..f7c831dd 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -665,6 +665,9 @@ async def connect_playwright_to_selenium(port=9222): 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) From ef82a971a2695080a488e2a132fd01eacf6bb776 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Wed, 25 Mar 2026 18:56:27 +0600 Subject: [PATCH 33/41] Resolve existing Selenium driver by driver_id, not default session --- Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index 2772f25a..2ec84647 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -1198,6 +1198,7 @@ async def Go_To_Link(dataset: Dataset) -> ReturnType: driver_id = current_driver_id else: driver_id = list(selenium_details.keys())[0] + session_name = driver_id if ( driver_id not in selenium_details From 07728225f5d00fea61eec454a3b74973c371a076 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Wed, 25 Mar 2026 19:01:23 +0600 Subject: [PATCH 34/41] Guard against None remote-debugging port for CDP connect --- .../Built_In_Automation/Web/Selenium/BuiltInFunctions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index 2ec84647..20271e58 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -1245,6 +1245,11 @@ async def Go_To_Link(dataset: Dataset) -> ReturnType: else: selenium_driver.set_window_size(window_size_X, window_size_Y) + if debug_port is None: + import hashlib + port_hash = int(hashlib.md5(session_name.encode()).hexdigest(), 16) + debug_port = 9222 + (port_hash % 100) + selenium_details[driver_id] = { "driver": Shared_Resources.Get_Shared_Variables("selenium_driver"), "remote-debugging-port": debug_port @@ -4765,7 +4770,7 @@ def playwright(dataset): # 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", 9222) + debug_port = selenium_details[current_driver_id].get("remote-debugging-port") or 9222 devtools_url = ( selenium_driver.command_executor._url.replace("http://", "ws://") From 099cee02c22068f876d0cce64888529a7f8612b5 Mon Sep 17 00:00:00 2001 From: Nasif Bin Zafar Date: Wed, 25 Mar 2026 19:03:56 +0600 Subject: [PATCH 35/41] Keep parsing Playwright driver id from input parameter --- .../Built_In_Automation/Web/Playwright/BuiltInFunctions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index c59268ac..268f5f58 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -231,6 +231,8 @@ async 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", "session"): + page_id = right_v elif mid_l == "optional parameter": if left_l == "headless": From 3dbf333f849bd84a2426943ee5cef060c20c18ca Mon Sep 17 00:00:00 2001 From: Nasif Date: Tue, 19 May 2026 15:50:57 +0600 Subject: [PATCH 36/41] Complete browser session action support Add shared browser session utilities for normalized session parsing, safe registry access, session removal, and available CDP port allocation. Wire Selenium and Playwright actions through a pre-action session activator so actions with an explicit session parameter run against the requested browser instead of whichever global driver was last active. Missing explicit sessions now fail for non-creation actions. Keep Selenium and Playwright session registries in sync across open, switch, and teardown paths. Store CDP metadata in browser_sessions, clean up paired CDP resources, and preserve non-targeted sessions during framework-specific teardown. Make Playwright frame state session-aware, fix several Playwright actions that were calling async APIs synchronously, collect browser logs across session Selenium drivers, and make screenshots prefer the active Playwright page when appropriate. Add focused browser session unit tests for session activation, missing-session failure, and session registry removal. --- .../Sequential_Actions/sequential_actions.py | 25 +- .../Web/Playwright/BuiltInFunctions.py | 320 ++++++++++-------- .../Web/Selenium/BuiltInFunctions.py | 241 +++++++++---- Framework/Built_In_Automation/Web/utils.py | 86 ++++- Framework/Utilities/CommonUtil.py | 4 +- tests/test_browser_sessions.py | 106 ++++++ 6 files changed, 577 insertions(+), 205 deletions(-) create mode 100644 tests/test_browser_sessions.py diff --git a/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py b/Framework/Built_In_Automation/Sequential_Actions/sequential_actions.py index 301b7087..49d99f97 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: @@ -2500,6 +2514,13 @@ async 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: @@ -2551,4 +2572,4 @@ async 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 268f5f58..ca0c5adb 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -46,6 +46,14 @@ 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, +) from settings import ZEUZ_NODE_DOWNLOADS_DIR def _get_frame_locator(): @@ -60,6 +68,60 @@ def _get_frame_locator(): 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()) + + +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) + + +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) + if not session_name: + return "passed" + + create_or_cleanup_actions = { + "Open_Browser", + "Go_To_Link", + "Tear_Down_Playwright", + } + if function_name in create_or_cleanup_actions: + return "passed" + + existing_session = get_browser_session(session_name) + if not existing_session or not existing_session.get("playwright_page"): + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + CommonUtil.ExecLog(sModuleInfo, f"Browser session '{session_name}' not found", 3) + return "zeuz_failed" + + _set_active_playwright_session(session_name, existing_session) + return "passed" + + def connect_selenium_to_playwright(port=9222): """Connect Selenium to Playwright browser via CDP""" try: @@ -129,48 +191,20 @@ def _handle_playwright_session(step_data): """ global current_page, current_page_id, context, browser - # Parse session parameter - 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 + session_name = extract_session_name(step_data) # If session parameter is provided, switch to that session if session_name: - from Framework.Built_In_Automation.Web.utils import get_browser_session existing_session = get_browser_session(session_name) if existing_session and existing_session.get("playwright_page"): - # Session exists, use existing browser - current_page = existing_session["playwright_page"] - context = existing_session["playwright_context"] - browser = existing_session["playwright_browser"] - current_page_id = session_name - - # Update globals - globals().update({ - 'current_page': current_page, - 'context': context, - 'browser': browser, - 'current_page_id': current_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) - + _set_active_playwright_session(session_name, existing_session) sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME CommonUtil.ExecLog(sModuleInfo, f"Using existing browser session: {session_name}", 1) else: - # Session doesn't exist sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME - CommonUtil.ExecLog(sModuleInfo, f"Session '{session_name}' not found. Using current browser.", 2) + CommonUtil.ExecLog(sModuleInfo, f"Browser session '{session_name}' not found", 3) + raise ValueError(f"Browser session '{session_name}' not found") return session_name, current_page, current_page_id, context, browser @@ -286,10 +320,7 @@ async def Open_Browser(step_data): } # Add remote debugging port for CDP connection with unique port per session - import hashlib - # Generate unique port based on page_id (range 9222-9322 to avoid conflicts) - port_hash = int(hashlib.md5(page_id.encode()).hexdigest(), 16) - unique_port = 9222 + (port_hash % 100) + unique_port = get_debug_port(page_id) all_args = args + [f"--remote-debugging-port={unique_port}"] CommonUtil.ExecLog(sModuleInfo, f"Using remote debugging port {unique_port} for session '{page_id}'", 1) if all_args: @@ -346,6 +377,7 @@ async def Open_Browser(step_data): "context": context, "browser": browser, "playwright": playwright_instance, + "remote-debugging-port": unique_port, } # Navigate if URL provided @@ -358,6 +390,7 @@ async 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()) @@ -366,15 +399,15 @@ async def Open_Browser(step_data): selenium_driver = connect_selenium_to_playwright(port=unique_port) # Create browser session - from Framework.Built_In_Automation.Web.utils import create_browser_session - create_browser_session( session_name=page_id, selenium_driver=selenium_driver, playwright_page=current_page, playwright_browser=browser, playwright_context=context, - playwright_frame=None + playwright_frame=None, + playwright_instance=playwright_instance, + remote_debugging_port=unique_port, ) CommonUtil.ExecLog(sModuleInfo, f"Created browser session: {page_id}", 5) @@ -415,29 +448,10 @@ async def Go_To_Link(step_data): # Check if session exists and use it if session_name: - from Framework.Built_In_Automation.Web.utils import get_browser_session existing_session = get_browser_session(session_name) if existing_session and existing_session.get("playwright_page"): - # Session exists, use existing browser - current_page = existing_session["playwright_page"] - context = existing_session["playwright_context"] - browser = existing_session["playwright_browser"] - current_page_id = session_name - - # Update globals - globals().update({ - 'current_page': current_page, - 'context': context, - 'browser': browser, - 'current_page_id': current_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) - + _set_active_playwright_session(session_name, existing_session) CommonUtil.ExecLog(sModuleInfo, f"Using existing browser session: {session_name}", 1) else: # Session doesn't exist, open new browser with session name @@ -493,6 +507,7 @@ async def Go_To_Link(step_data): # 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" @@ -534,7 +549,6 @@ async def Tear_Down_Playwright(step_data=None): # Handle session-specific teardown if session_name: - from Framework.Built_In_Automation.Web.utils import get_browser_session existing_session = get_browser_session(session_name) if existing_session and existing_session.get("playwright_page"): @@ -543,22 +557,29 @@ async def Tear_Down_Playwright(step_data=None): 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 session from browser_sessions - browser_sessions = sr.Get_Shared_Variables("browser_sessions", {}) - if session_name in browser_sessions: - del browser_sessions[session_name] - sr.Set_Shared_Variables("browser_sessions", browser_sessions) + remove_browser_session(session_name) # Remove from playwright_details if present if session_name in playwright_details: @@ -588,16 +609,30 @@ async def Tear_Down_Playwright(step_data=None): 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 playwright_details.items(): + 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 @@ -634,18 +669,26 @@ async def Tear_Down_Playwright(step_data=None): playwright_details = {} current_page_id = None - # Clear all browser sessions - sr.Set_Shared_Variables("browser_sessions", {}) + # 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) CommonUtil.ExecLog(sModuleInfo, "Browser closed successfully", 1) return "passed" + return "passed" + except Exception: return CommonUtil.Exception_Handler(sys.exc_info()) @logger -def Switch_Browser(step_data): +async def Switch_Browser(step_data): """ Switch between multiple browser instances/pages. @@ -655,7 +698,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 @@ -665,14 +708,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" @@ -680,12 +731,14 @@ 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()) @@ -1469,7 +1522,7 @@ async def get_element_info(step_data): ######################### @logger -def Navigate(step_data): +async def Navigate(step_data): """ Navigate browser (back, forward, refresh). @@ -1507,13 +1560,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) @@ -1569,7 +1622,7 @@ def Get_Current_URL(step_data): ######################### @logger -def Scroll(step_data): +async def Scroll(step_data): """ Scroll the page in a direction. @@ -1616,7 +1669,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" @@ -1844,7 +1897,7 @@ async 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. @@ -1893,11 +1946,11 @@ 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) @@ -1905,7 +1958,7 @@ def switch_window_or_tab(step_data): 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) @@ -1917,10 +1970,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.set_screenshot_vars(sr.Shared_Variable_Export()) - CommonUtil.ExecLog(sModuleInfo, f"Switched to tab index {switch_by_index}: {current_page.title()}", 1) + 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) @@ -1934,7 +1987,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. @@ -1960,13 +2013,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) @@ -1978,7 +2036,7 @@ def open_new_tab(step_data): @logger -def close_tab(step_data): +async def close_tab(step_data): """ Close a browser tab. @@ -2017,8 +2075,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: @@ -2026,7 +2084,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) @@ -2034,7 +2092,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 @@ -2042,6 +2100,11 @@ 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" @@ -2106,6 +2169,7 @@ async def switch_iframe(step_data): # 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" @@ -2120,6 +2184,7 @@ async def switch_iframe(step_data): # 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" @@ -2134,7 +2199,7 @@ async def switch_iframe(step_data): ######################### @logger -def Handle_Browser_Alert(step_data): +async def Handle_Browser_Alert(step_data): """ Handle browser alerts/dialogs. @@ -2189,45 +2254,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" @@ -2513,7 +2563,7 @@ async def upload_file(step_data): ######################### @logger -def resize_window(step_data): +async def resize_window(step_data): """ Resize the browser viewport. @@ -2562,7 +2612,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" @@ -2652,7 +2702,7 @@ async def Wait_For_Element(step_data): ######################### @logger -def Start_Tracing(step_data): +async def Start_Tracing(step_data): """ Start Playwright trace recording. @@ -2687,7 +2737,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 @@ -2700,7 +2750,7 @@ def Start_Tracing(step_data): @logger -def Stop_Tracing(step_data): +async def Stop_Tracing(step_data): """ Stop tracing and save trace file. @@ -2727,7 +2777,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" @@ -2736,7 +2786,7 @@ def Stop_Tracing(step_data): @logger -def Intercept_Network(step_data): +async def Intercept_Network(step_data): """ Set up network request interception. @@ -2776,20 +2826,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" diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index 20271e58..65e55a00 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -72,7 +72,14 @@ from .utils import ChromeForTesting, ChromeExtensionDownloader from playwright.async_api import async_playwright -from Framework.Built_In_Automation.Web.utils import get_browser_session, create_browser_session +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, +) ######################### # # @@ -165,17 +172,10 @@ def _handle_selenium_session(step_data): """ global selenium_driver, current_driver_id - # Parse session parameter - session_name = None - for left, mid, right in step_data: - left = left.replace(" ", "").replace("_", "").replace("-", "").lower() - if left == "session" and mid.strip().lower() == "optional parameter": - session_name = right.strip() - break + session_name = extract_session_name(step_data) # If session parameter is provided, switch to that session if session_name: - from Framework.Built_In_Automation.Web.utils import get_browser_session existing_session = get_browser_session(session_name) if existing_session and existing_session.get("selenium_driver"): @@ -183,17 +183,91 @@ def _handle_selenium_session(step_data): selenium_driver = existing_session["selenium_driver"] current_driver_id = session_name Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) + Shared_Resources.Set_Shared_Variables("active_web_driver_type", "selenium") sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME CommonUtil.ExecLog(sModuleInfo, f"Using existing browser session: {session_name}", 1) else: - # Session doesn't exist sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME - CommonUtil.ExecLog(sModuleInfo, f"Session '{session_name}' not found. Using current browser.", 2) + CommonUtil.ExecLog(sModuleInfo, f"Browser session '{session_name}' not found", 3) + raise ValueError(f"Browser session '{session_name}' not found") 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) + if not session_name: + return "passed" + + 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" + + existing_session = get_browser_session(session_name) + if not existing_session or not existing_session.get("selenium_driver"): + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + CommonUtil.ExecLog(sModuleInfo, f"Browser session '{session_name}' not found", 3) + return "zeuz_failed" + + selenium_driver = existing_session["selenium_driver"] + current_driver_id = session_name + selenium_details.setdefault(session_name, {})["driver"] = selenium_driver + if existing_session.get("remote_debugging_port"): + selenium_details[session_name]["remote-debugging-port"] = existing_session["remote_debugging_port"] + Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) + Shared_Resources.Set_Shared_Variables("active_web_driver_type", "selenium") + if existing_session.get("playwright_page"): + Shared_Resources.Set_Shared_Variables("playwright_page", existing_session["playwright_page"]) + CommonUtil.set_screenshot_vars(Shared_Resources.Shared_Variable_Export()) + 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] @@ -473,7 +547,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() @@ -483,6 +557,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() @@ -511,10 +587,7 @@ def Open_Electron_App(data_set): opts = Options() opts.binary_location = desktop_app_path - # Generate unique port for Electron app based on driver_id - import hashlib - port_hash = int(hashlib.md5((driver_id or "electron").encode()).hexdigest(), 16) - electron_port = 9230 + (port_hash % 90) # Range 9230-9320 to avoid conflicts with browser sessions + 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) @@ -533,15 +606,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: @@ -721,7 +800,7 @@ async def connect_playwright_to_selenium(port=9222): Shared_Resources.Set_Shared_Variables("playwright_browser", browser) Shared_Resources.Set_Shared_Variables("playwright_page", page) - return browser, context, page + return playwright_instance, browser, context, page @logger @@ -755,11 +834,8 @@ async def Open_Browser(browser, browser_options: BrowserOptions, session_name: s options = generate_options(browser, browser_options) - # Enable remote debugging / CDP with unique port per session - import hashlib - # Generate unique port based on session name (range 9222-9322 to avoid conflicts) - port_hash = int(hashlib.md5(session_name.encode()).hexdigest(), 16) - unique_port = 9222 + (port_hash % 100) + # 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) @@ -836,11 +912,12 @@ async def Open_Browser(browser, browser_options: BrowserOptions, session_name: s from selenium.webdriver import Chrome, Firefox, Edge, Safari if isinstance(selenium_driver, (Chrome, Firefox, Edge, Safari)): # Connect Playwright to Selenium via CDP + playwright_instance = None playwright_browser = None playwright_context = None playwright_page = None try: - playwright_browser, playwright_context, playwright_page = await connect_playwright_to_selenium(port=unique_port) + playwright_instance, playwright_browser, playwright_context, playwright_page = await connect_playwright_to_selenium(port=unique_port) CommonUtil.ExecLog(sModuleInfo, "Connected Playwright to Selenium", 1) except Exception as e: CommonUtil.ExecLog(sModuleInfo, f"Failed to connect Playwright to Selenium: {e}", 3) @@ -852,12 +929,15 @@ async def Open_Browser(browser, browser_options: BrowserOptions, session_name: s playwright_page=playwright_page, playwright_browser=playwright_browser, playwright_context=playwright_context, - playwright_frame=None + playwright_frame=None, + playwright_instance=playwright_instance, + 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" @@ -882,7 +962,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()) @@ -909,6 +989,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: @@ -917,7 +999,13 @@ 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 existing_session.get("selenium_driver"): + selenium_driver = existing_session["selenium_driver"] + selenium_details.setdefault(driver_tag, {})["driver"] = selenium_driver + if existing_session.get("remote_debugging_port"): + selenium_details[driver_tag]["remote-debugging-port"] = existing_session["remote_debugging_port"] + elif driver_tag in selenium_details.keys(): selenium_driver = selenium_details[driver_tag]["driver"] else: if Shared_Resources.Test_Shared_Variables("dependency"): @@ -930,16 +1018,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 ( @@ -953,6 +1053,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" @@ -1224,7 +1326,8 @@ async def Go_To_Link(dataset: Dataset) -> ReturnType: if await Open_Browser(dependency["Browser"], browser_options, session_name) == "zeuz_failed": return "zeuz_failed" - selenium_driver = get_browser_session(session_name)["selenium_driver"] + 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" @@ -1246,17 +1349,17 @@ async def Go_To_Link(dataset: Dataset) -> ReturnType: selenium_driver.set_window_size(window_size_X, window_size_Y) if debug_port is None: - import hashlib - port_hash = int(hashlib.md5(session_name.encode()).hexdigest(), 16) - debug_port = 9222 + (port_hash % 100) + 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 = get_browser_session(session_name)["selenium_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" @@ -3513,20 +3616,24 @@ def Tear_Down_Selenium(step_data=[]): # Parse both driverid (legacy) and session (new) parameters for left, mid, right in step_data: left = left.replace(" ", "").replace("_", "").replace("-", "").lower() - if left == "driverid": - driver_id = right.strip() - elif left == "session" and mid.strip().lower() == "optional parameter": + if left == "session" and mid.strip().lower() == "optional parameter": session_name = right.strip() - # For backward compatibility, treat session_name as driver_id driver_id = session_name + elif left == "driverid": + driver_id = right.strip() # Handle session-specific teardown if session_name: - from Framework.Built_In_Automation.Web.utils import get_browser_session 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() @@ -3536,11 +3643,7 @@ def Tear_Down_Selenium(step_data=[]): CommonUtil.ExecLog(sModuleInfo, errMsg, 2) CommonUtil.Exception_Handler(sys.exc_info(), None, errMsg) - # Remove session from browser_sessions - browser_sessions = Shared_Resources.Get_Shared_Variables("browser_sessions", {}) - if session_name in browser_sessions: - del browser_sessions[session_name] - Shared_Resources.Set_Shared_Variables("browser_sessions", browser_sessions) + remove_browser_session(session_name) # Remove from selenium_details if present if session_name in selenium_details: @@ -3567,6 +3670,15 @@ def Tear_Down_Selenium(step_data=[]): CommonUtil.Join_Thread_and_Return_Result( "screenshot" ) # Let the capturing screenshot end in thread + for session in get_browser_sessions().values(): + if not (isinstance(session, dict) and session.get("selenium_driver")): + 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() @@ -3581,10 +3693,16 @@ 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") - # Clear all browser sessions - Shared_Resources.Set_Shared_Variables("browser_sessions", {}) + sessions = get_browser_sessions() + sessions = { + name: session + for name, session in sessions.items() + if not (isinstance(session, dict) and session.get("selenium_driver")) + } + 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( @@ -3607,6 +3725,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"] @@ -3648,13 +3767,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, @@ -3663,11 +3788,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: diff --git a/Framework/Built_In_Automation/Web/utils.py b/Framework/Built_In_Automation/Web/utils.py index 8329b25c..8af04245 100644 --- a/Framework/Built_In_Automation/Web/utils.py +++ b/Framework/Built_In_Automation/Web/utils.py @@ -1,3 +1,6 @@ +import hashlib +import socket + from Framework.Built_In_Automation.Shared_Resources import ( BuiltInFunctionSharedResources as sr, ) @@ -16,13 +19,80 @@ def initialize_browser_sessions(): 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 + playwright_frame: Frame | None = None, + remote_debugging_port: int | None = None, + playwright_instance = None, ) -> dict: """ Creates a new browser session with the given parameters. @@ -37,16 +107,15 @@ def create_browser_session( playwright_frame (Frame): The Playwright Frame instance. """ - if sr.Test_Shared_Variables("browser_sessions") == False: - initialize_browser_sessions() - - browser_sessions = sr.Get_Shared_Variables("browser_sessions") + 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_frame": playwright_frame, + "playwright_instance": playwright_instance, + "remote_debugging_port": remote_debugging_port, } sr.Set_Shared_Variables("browser_sessions", browser_sessions) @@ -64,8 +133,5 @@ def get_browser_session(session_name: str) -> dict: dict: The browser session with the given name. """ - if sr.Test_Shared_Variables("browser_sessions") == False: - initialize_browser_sessions() - - browser_sessions = sr.Get_Shared_Variables("browser_sessions") + browser_sessions = get_browser_sessions() return browser_sessions.get(session_name, {}) diff --git a/Framework/Utilities/CommonUtil.py b/Framework/Utilities/CommonUtil.py index de2847a4..676cbce0 100644 --- a/Framework/Utilities/CommonUtil.py +++ b/Framework/Utilities/CommonUtil.py @@ -870,7 +870,9 @@ def set_screenshot_vars(shared_variables): "driver" ] # Driver for selected device if screen_capture_type == "web": # Selenium or Playwright driver object - if "selenium_driver" in shared_variables: + 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"] diff --git a/tests/test_browser_sessions.py b/tests/test_browser_sessions.py new file mode 100644 index 00000000..a1b74b55 --- /dev/null +++ b/tests/test_browser_sessions.py @@ -0,0 +1,106 @@ +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 +from Framework.Built_In_Automation.Web.Selenium import BuiltInFunctions as selenium_bif + + +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") == {} From 5dfd129b3b5573bdad78cf85d451f589a881aaa7 Mon Sep 17 00:00:00 2001 From: Nasif Date: Tue, 19 May 2026 18:10:12 +0600 Subject: [PATCH 37/41] Fix Playwright cleanup teardown ordering Run Playwright teardown before Selenium during driver cleanup so Playwright-owned sessions close their async browser resources first. Teach Selenium global teardown to skip sessions that are owned by Playwright, while still allowing Selenium-owned sessions with Playwright bridges to be cleaned up. Add a regression test covering Playwright-owned sessions with Selenium CDP bridges so Selenium teardown does not close or remove them. --- .../Web/Selenium/BuiltInFunctions.py | 10 +++++-- Framework/MainDriverApi.py | 4 +-- tests/test_browser_sessions.py | 28 ++++++++++++++++++- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index 8742442b..df7db39a 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -3673,9 +3673,11 @@ def Tear_Down_Selenium(step_data=[]): CommonUtil.Join_Thread_and_Return_Result( "screenshot" ) # Let the capturing screenshot end in thread - for session in get_browser_sessions().values(): + 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")) @@ -3700,7 +3702,11 @@ def Tear_Down_Selenium(step_data=[]): sessions = { name: session for name, session in sessions.items() - if not (isinstance(session, dict) and session.get("selenium_driver")) + 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 = {} diff --git a/Framework/MainDriverApi.py b/Framework/MainDriverApi.py index add91eea..e8509e71 100644 --- a/Framework/MainDriverApi.py +++ b/Framework/MainDriverApi.py @@ -811,12 +811,12 @@ async def cleanup_driver_instances(): # cleans up driver(selenium, playwright, import Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions as Selenium import Framework.Built_In_Automation.Web.Playwright.BuiltInFunctions as Playwright try: - Selenium.Tear_Down_Selenium() + await Playwright.Tear_Down_Playwright() except: pass try: - await Playwright.Tear_Down_Playwright() + Selenium.Tear_Down_Selenium() except: pass diff --git a/tests/test_browser_sessions.py b/tests/test_browser_sessions.py index a1b74b55..3b7d214f 100644 --- a/tests/test_browser_sessions.py +++ b/tests/test_browser_sessions.py @@ -5,7 +5,9 @@ ) from Framework.Built_In_Automation.Web import utils as browser_utils from Framework.Built_In_Automation.Web.Playwright import BuiltInFunctions as playwright_bif -from Framework.Built_In_Automation.Web.Selenium import BuiltInFunctions as selenium_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(): @@ -104,3 +106,27 @@ def test_remove_browser_session_updates_registry(): 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() From 6ca683edcf9fb05a92350676856659e2ffbf19ec Mon Sep 17 00:00:00 2001 From: Nasif Date: Tue, 19 May 2026 19:36:27 +0600 Subject: [PATCH 38/41] Fix Playwright iframe index switching Treat index rows in Playwright switch_iframe as iframe/frame indexes instead of generic element attributes. This prevents index iframe parameters from producing invalid selectors such as [index='1']. Allow a default-content reset and a subsequent iframe target in the same action, matching the Selenium action behavior used by existing test data. Build indexed frame locators with frame_locator(tag).nth(index), preserve selector-based iframe targets, and save the resulting frame locator back to the active browser session. Add regression coverage for switching to default content and then selecting iframe index 1 with a Playwright action. --- .../Web/Playwright/BuiltInFunctions.py | 72 ++++++++++++++----- tests/test_browser_sessions.py | 33 +++++++++ 2 files changed, 88 insertions(+), 17 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index ca0c5adb..57bec137 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -2147,23 +2147,37 @@ async 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() + right_l = right_v.lower() - 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) + 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 @@ -2171,17 +2185,41 @@ async def switch_iframe(step_data): 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) diff --git a/tests/test_browser_sessions.py b/tests/test_browser_sessions.py index 3b7d214f..5ccc01a4 100644 --- a/tests/test_browser_sessions.py +++ b/tests/test_browser_sessions.py @@ -1,3 +1,4 @@ +import asyncio from unittest.mock import MagicMock from Framework.Built_In_Automation.Shared_Resources import ( @@ -130,3 +131,35 @@ def test_selenium_global_teardown_preserves_playwright_owned_sessions(monkeypatc 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 From e5ba9f23c244f364bd0bf221883eb896137e87b7 Mon Sep 17 00:00:00 2001 From: Nasif Date: Wed, 20 May 2026 17:26:57 +0600 Subject: [PATCH 39/41] Replace custom Chrome installation function with Playwright integrated Chromium installation method --- .../Web/Playwright/BuiltInFunctions.py | 13 +- .../Web/Playwright/utils.py | 156 +++++++++++++++--- uv.lock | 18 +- 3 files changed, 147 insertions(+), 40 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 57bec137..b58f4c4c 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -54,7 +54,6 @@ get_debug_port, remove_browser_session, ) -from settings import ZEUZ_NODE_DOWNLOADS_DIR def _get_frame_locator(): """Helper function to get current frame locator from shared variables.""" @@ -303,8 +302,8 @@ async def Open_Browser(step_data): # Handle Selenium-style capabilities where possible pass - # Ensure Chrome for Testing is available - chrome_binary_path, success = PlaywrightUtils.ensure_chromium_downloads(sModuleInfo) + # 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" @@ -316,23 +315,19 @@ async def Open_Browser(step_data): launch_options = { "headless": headless, "slow_mo": slow_mo, - "devtools": devtools, } # 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 - # Use Chrome for Testing binary if available - if chrome_binary_path and browser_name in ("chrome", "chromium"): - launch_options["executable_path"] = chrome_binary_path - CommonUtil.ExecLog(sModuleInfo, f"Using Chrome for Testing binary: {chrome_binary_path}", 1) - # Select and launch browser if browser_name in ("chrome", "chromium"): browser = await playwright_instance.chromium.launch(**launch_options) diff --git a/Framework/Built_In_Automation/Web/Playwright/utils.py b/Framework/Built_In_Automation/Web/Playwright/utils.py index 731b88fa..f6e531f7 100644 --- a/Framework/Built_In_Automation/Web/Playwright/utils.py +++ b/Framework/Built_In_Automation/Web/Playwright/utils.py @@ -9,37 +9,149 @@ """ import os +import subprocess +import sys from pathlib import Path + +from filelock import FileLock + from Framework.Utilities import CommonUtil -from Framework.Built_In_Automation.Web.Selenium.utils import ChromeForTesting +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 -# Initialize Chrome for Testing instance -chrome_for_testing = ChromeForTesting() + executable_path = result.stdout.strip().splitlines()[-1] if result.stdout.strip() else "" + return Path(executable_path) if executable_path else None -def ensure_chromium_downloads(sModuleInfo): +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 Chrome for Testing is available for Playwright. - + 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: - tuple: (chrome_binary_path, success_flag) where success_flag is True if successful + bool: True if the browser is ready or no managed download is required """ try: - CommonUtil.ExecLog(sModuleInfo, "Setting up Chrome for Testing for Playwright...", 1) - - # Use Chrome for Testing to get Chrome binary - chrome_bin, driver_bin = chrome_for_testing.setup_chrome_for_testing() - - if chrome_bin and chrome_bin.exists(): - CommonUtil.ExecLog(sModuleInfo, f"Chrome for Testing ready: {chrome_bin}", 1) - return str(chrome_bin), True - else: - CommonUtil.ExecLog(sModuleInfo, "Failed to setup Chrome for Testing", 3) - return None, False - + 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 Chrome for Testing: {str(e)}", 3) - return None, False + CommonUtil.ExecLog(sModuleInfo, f"Error setting up Playwright browser: {str(e)}", 3) + return False diff --git a/uv.lock b/uv.lock index f448fba4..cd801351 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]] From 3420b29b95a1e6be0ad974c772eb7a0301d2b4f0 Mon Sep 17 00:00:00 2001 From: Nasif Date: Wed, 20 May 2026 17:53:09 +0600 Subject: [PATCH 40/41] Optimize Playwright screenshots - Defaults to viewport instead of full page - Default image quality is now 70% - Skips `PIL` post-processing for Playwright --- .../Web/Playwright/BuiltInFunctions.py | 26 ++++++++++-- Framework/Utilities/CommonUtil.py | 40 ++++++++++++------- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index b58f4c4c..fd114e3d 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -2411,6 +2411,8 @@ async 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() @@ -2424,15 +2426,32 @@ async 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: @@ -2440,9 +2459,10 @@ async def take_screenshot_playwright(step_data): if locator == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Element not found", 3) return "zeuz_failed" - await locator.screenshot(path=screenshot_path) + await locator.screenshot(**screenshot_options) else: - await 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) diff --git a/Framework/Utilities/CommonUtil.py b/Framework/Utilities/CommonUtil.py index fa9ec18c..546a7421 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 = {} @@ -993,10 +994,11 @@ def _get_window_screenshot_bbox(): return None -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 +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"*", @@ -1007,15 +1009,22 @@ async def Thread_ScreenShot(function_name, image_folder, Method, Driver, image_n 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") @@ -1060,8 +1069,8 @@ async def Thread_ScreenShot(function_name, image_folder, Method, Driver, image_n # Capture screenshot of web browser elif Method == "web": # Check if it's a Playwright page or Selenium driver - if hasattr(Driver, 'screenshot'): # Playwright page - await Driver.screenshot(path=ImageName, full_page=True) + 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 @@ -1077,14 +1086,15 @@ async def Thread_ScreenShot(function_name, image_folder, Method, Driver, image_n ) # 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( From 58674d1a98dc706739582a6285717b24eb38b999 Mon Sep 17 00:00:00 2001 From: Nasif Date: Thu, 21 May 2026 15:54:11 +0600 Subject: [PATCH 41/41] Lazy attach cross-browser automation sessions Avoid starting Playwright during Selenium browser launches and avoid creating Selenium CDP drivers during Playwright launches. Store session metadata with the remote debugging port and attach the opposite framework only when an action from that framework targets the session. --- .../Web/Playwright/BuiltInFunctions.py | 110 +++++++++++---- .../Web/Selenium/BuiltInFunctions.py | 125 ++++++++++++------ 2 files changed, 168 insertions(+), 67 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index fd114e3d..6652809f 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -88,6 +88,53 @@ def _set_active_playwright_session(session_name, session): 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() @@ -96,13 +143,10 @@ def _save_current_playwright_frame(frame_locator): sr.Set_Shared_Variables("browser_sessions", sessions) -def _activate_browser_session_for_action(step_data, function_name=None): +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) - if not session_name: - return "passed" - create_or_cleanup_actions = { "Open_Browser", "Go_To_Link", @@ -111,13 +155,20 @@ def _activate_browser_session_for_action(step_data, function_name=None): 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) - if not existing_session or not existing_session.get("playwright_page"): + 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" - _set_active_playwright_session(session_name, existing_session) return "passed" @@ -173,7 +224,7 @@ def connect_selenium_to_playwright(port=9222): # # ######################### -def _handle_playwright_session(step_data): +async def _handle_playwright_session(step_data): """ Helper function to handle session parameter for Playwright actions. @@ -196,14 +247,17 @@ def _handle_playwright_session(step_data): if session_name: existing_session = get_browser_session(session_name) - if existing_session and existing_session.get("playwright_page"): - _set_active_playwright_session(session_name, existing_session) + 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 @@ -328,12 +382,16 @@ async def Open_Browser(step_data): if downloads_path: launch_options["downloads_path"] = downloads_path + selenium_cdp_supported = True + # Select and launch browser if browser_name in ("chrome", "chromium"): browser = await playwright_instance.chromium.launch(**launch_options) elif browser_name == "firefox": + selenium_cdp_supported = False browser = await playwright_instance.firefox.launch(**launch_options) elif browser_name in ("webkit", "safari"): + selenium_cdp_supported = False browser = await playwright_instance.webkit.launch(**launch_options) elif browser_name in ("edge", "msedge", "microsoft edge"): launch_options["channel"] = "msedge" @@ -390,13 +448,10 @@ async def Open_Browser(step_data): # Set screenshot variables for CommonUtil.TakeScreenShot() CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) - # Connect Selenium to Playwright via CDP - selenium_driver = connect_selenium_to_playwright(port=unique_port) - # Create browser session - create_browser_session( + session = create_browser_session( session_name=page_id, - selenium_driver=selenium_driver, + selenium_driver=None, playwright_page=current_page, playwright_browser=browser, playwright_context=context, @@ -404,6 +459,8 @@ async def Open_Browser(step_data): 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) @@ -445,8 +502,7 @@ async def Go_To_Link(step_data): if session_name: existing_session = get_browser_session(session_name) - if existing_session and existing_session.get("playwright_page"): - _set_active_playwright_session(session_name, existing_session) + 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 @@ -463,12 +519,18 @@ async def Go_To_Link(step_data): return "zeuz_failed" elif current_page is None: - # 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" + 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" @@ -783,7 +845,7 @@ async def Click_Element(step_data): try: # Handle session parameter - session_name, current_page, current_page_id, context, browser = _handle_playwright_session(step_data) + 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) @@ -996,7 +1058,7 @@ async def Enter_Text_In_Text_Box(step_data): try: # Handle session parameter - session_name, current_page, current_page_id, context, browser = _handle_playwright_session(step_data) + 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) diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index df7db39a..17c82249 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -157,6 +157,65 @@ 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. @@ -174,23 +233,19 @@ def _handle_selenium_session(step_data): 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 existing_session.get("selenium_driver"): - # Session exists, use existing driver - selenium_driver = existing_session["selenium_driver"] - current_driver_id = session_name - Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) - Shared_Resources.Set_Shared_Variables("active_web_driver_type", "selenium") - + 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 @@ -201,8 +256,6 @@ def _activate_browser_session_for_action(step_data, function_name=None): global selenium_driver, current_driver_id, selenium_details session_name = extract_session_name(step_data) - if not session_name: - return "passed" create_or_cleanup_actions = { "Go_To_Link", @@ -213,22 +266,18 @@ def _activate_browser_session_for_action(step_data, function_name=None): 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 not existing_session or not existing_session.get("selenium_driver"): + 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" - - selenium_driver = existing_session["selenium_driver"] - current_driver_id = session_name - selenium_details.setdefault(session_name, {})["driver"] = selenium_driver - if existing_session.get("remote_debugging_port"): - selenium_details[session_name]["remote-debugging-port"] = existing_session["remote_debugging_port"] - Shared_Resources.Set_Shared_Variables("selenium_driver", selenium_driver) - Shared_Resources.Set_Shared_Variables("active_web_driver_type", "selenium") - if existing_session.get("playwright_page"): - Shared_Resources.Set_Shared_Variables("playwright_page", existing_session["playwright_page"]) - CommonUtil.set_screenshot_vars(Shared_Resources.Shared_Variable_Export()) return "passed" @@ -914,26 +963,15 @@ async def Open_Browser(browser, browser_options: BrowserOptions, session_name: s # If selenium_driver is of type Webdriver from selenium.webdriver import Chrome, Firefox, Edge, Safari if isinstance(selenium_driver, (Chrome, Firefox, Edge, Safari)): - # Connect Playwright to Selenium via CDP - playwright_instance = None - playwright_browser = None - playwright_context = None - playwright_page = None - try: - playwright_instance, playwright_browser, playwright_context, playwright_page = await connect_playwright_to_selenium(port=unique_port) - CommonUtil.ExecLog(sModuleInfo, "Connected Playwright to Selenium", 1) - except Exception as e: - CommonUtil.ExecLog(sModuleInfo, f"Failed to connect Playwright to Selenium: {e}", 3) - # Create browser session create_browser_session( session_name=session_name, selenium_driver=selenium_driver, - playwright_page=playwright_page, - playwright_browser=playwright_browser, - playwright_context=playwright_context, + playwright_page=None, + playwright_browser=None, + playwright_context=None, playwright_frame=None, - playwright_instance=playwright_instance, + playwright_instance=None, remote_debugging_port=unique_port, ) CommonUtil.ExecLog(sModuleInfo, f"Created browser session: {session_name=}", 5) @@ -1003,11 +1041,8 @@ def Go_To_Link_V2(step_data): options.page_load_strategy = page_load_strategy existing_session = get_browser_session(driver_tag) - if existing_session and existing_session.get("selenium_driver"): - selenium_driver = existing_session["selenium_driver"] - selenium_details.setdefault(driver_tag, {})["driver"] = selenium_driver - if existing_session.get("remote_debugging_port"): - selenium_details[driver_tag]["remote-debugging-port"] = existing_session["remote_debugging_port"] + 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: @@ -1305,6 +1340,10 @@ async def Go_To_Link(dataset: Dataset) -> ReturnType: 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 or selenium_details[driver_id]["driver"]