diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index fd5fd5388..ee99e7411 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -3537,9 +3537,91 @@ def open_new_tab(step_data): except Exception: return CommonUtil.Exception_Handler(sys.exc_info()) +def _switch_tab_debug_port_from_driver(driver): + try: + if not driver: + return None + + caps = driver.capabilities or {} + cdp = caps.get("se:cdp") + if cdp: + if "://" in cdp: + parsed = urlparse(cdp) + elif ":" in cdp: + parsed = urlparse(f"//{cdp}") + else: + parsed = None + if parsed and parsed.port: + return parsed.port + + chrome_options = caps.get("goog:chromeOptions", {}) + edge_options = caps.get("ms:edgeOptions", {}) + debugger_address = chrome_options.get("debuggerAddress") or edge_options.get( + "debuggerAddress" + ) + if not debugger_address: + return None + + parsed_address = urlparse( + debugger_address + if "://" in debugger_address + else f"//{debugger_address}" + ) + return parsed_address.port + except Exception: + return None + + +def _switch_tab_resolve_driver_details(): + driver_id = current_driver_id + if driver_id in selenium_details: + return selenium_details[driver_id] + + if driver_id is not None: + for details in selenium_details.values(): + if details.get("driver") is driver_id: + return details + + if selenium_driver is not None: + for details in selenium_details.values(): + if details.get("driver") is selenium_driver: + return details + + if selenium_details: + return next(iter(selenium_details.values())) + + return {} + + +def _switch_tab_get_debug_port(): + driver_details = _switch_tab_resolve_driver_details() + debug_port = driver_details.get("remote-debugging-port") + if debug_port: + return debug_port + + return _switch_tab_debug_port_from_driver( + driver_details.get("driver") or selenium_driver + ) + + +def _switch_tab_run_async(coro, timeout=30): + import asyncio + from concurrent.futures import ThreadPoolExecutor + + def run_in_thread(): + new_loop = asyncio.new_event_loop() + asyncio.set_event_loop(new_loop) + try: + return new_loop.run_until_complete(coro) + finally: + new_loop.close() + + with ThreadPoolExecutor(max_workers=1) as executor: + return executor.submit(run_in_thread).result(timeout=timeout) + + @logger def switch_window_or_tab(step_data): - """ This action will switch tab/window in browser using Selenium or Playwright (via CDP) based on the 'playwright' flag. Example 1: @@ -3608,6 +3690,18 @@ def switch_window_or_tab(step_data): try: import time # Import time for both Playwright and Selenium paths + playwright_requested = playwright_enabled + debug_port = None + if playwright_enabled: + debug_port = _switch_tab_get_debug_port() + if not debug_port: + CommonUtil.ExecLog( + sModuleInfo, + "Playwright tab switching requires a Chromium remote debugging port. Falling back to Selenium", + 2, + ) + playwright_enabled = False + if playwright_enabled: CommonUtil.ExecLog(sModuleInfo, "Playwright is enabled (using async API)", 1) import asyncio @@ -3616,9 +3710,6 @@ def switch_window_or_tab(step_data): # Async function to handle Playwright operations async def run_playwright_switch(): async with async_playwright() as p: - debug_port = selenium_details[current_driver_id][ - "remote-debugging-port" - ] browser = await p.chromium.connect_over_cdp(f"http://localhost:{debug_port}") context = browser.contexts[0] pages = context.pages @@ -3657,24 +3748,10 @@ async def run_playwright_switch(): return result_data - # Run async Playwright code from sync context + # Run async Playwright code from sync context (works with or without a running loop) try: - # We're always called from async context, so we need to run in a new thread with new event loop - loop = asyncio.get_running_loop() - from concurrent.futures import ThreadPoolExecutor - - def run_in_thread(): - new_loop = asyncio.new_event_loop() - asyncio.set_event_loop(new_loop) - try: - return new_loop.run_until_complete(run_playwright_switch()) - finally: - new_loop.close() - - with ThreadPoolExecutor(max_workers=1) as executor: - future = executor.submit(run_in_thread) - playwright_result = future.result(timeout=30) - + playwright_result = _switch_tab_run_async(run_playwright_switch()) + if playwright_result["status"] == "found": # Step 3: Re-align Selenium to match the target tab target_url = playwright_result["target_url"] @@ -3686,7 +3763,7 @@ def run_in_thread(): ): CommonUtil.ExecLog( sModuleInfo, - f"Selenium aligned to: {selenium_driver.title}", + f"Tab switch and Selenium aligned to: {selenium_driver.title}", 1, ) return "passed" @@ -3727,11 +3804,11 @@ def run_in_thread(): # --- Selenium tab switching --- CommonUtil.ExecLog(sModuleInfo, "Using Selenium for tab switching", 1) if window_title_condition: - all_windows = selenium_driver.window_handles current_window = selenium_driver.current_window_handle window_handles_found = False - for _ in range(3): # retry + for attempt in range(1, 7): + all_windows = selenium_driver.window_handles for handle in all_windows: selenium_driver.switch_to.window(handle) if ( @@ -3749,13 +3826,14 @@ def run_in_thread(): ) break else: - CommonUtil.ExecLog( - sModuleInfo, - "Couldn't find the title. Trying again after 1 second delay", - 2, - ) - time.sleep(1) - continue + if attempt < 6: + CommonUtil.ExecLog( + sModuleInfo, + f"Couldn't find the title. Retry {attempt}/6 after 2 second delay", + 2, + ) + time.sleep(2) + continue break if not window_handles_found: @@ -3773,7 +3851,7 @@ def run_in_thread(): selenium_driver.switch_to.window(window_to_switch) CommonUtil.ExecLog( sModuleInfo, - f"Tab switched to index {switch_by_index} title {selenium_driver.title}", + f"Tab switched to index {switch_by_index} title '{selenium_driver.title}'", 1, ) @@ -3784,6 +3862,264 @@ def run_in_thread(): return CommonUtil.Exception_Handler(sys.exc_info()) +#2 +# @logger +# def switch_window_or_tab(step_data): + +# """ +# This action will switch tab/window in browser using Selenium or Playwright (via CDP) based on the 'playwright' flag. +# Example 1: +# Field Sub Field Value +# *window title input parameter google +# playwright option true +# switch window/tab selenium action switch window or frame + + +# Example 2: +# Field Sub Field Value +# window title input parameter google +# playwright option false +# switch window/tab selenium action switch window or frame + +# Example 3: +# Field Sub Field Value +# window index input parameter 9 +# playwright option false +# switch window/tab selenium action switch window or frame +# """ + +# sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME +# global selenium_driver + +# try: +# window_title_condition = False +# window_index_condition = False +# partial_match = False +# playwright_enabled = False + +# for left, mid, right in step_data: +# left = left.lower().strip() +# right = right.strip() +# if left in ("window title", "tab title"): +# switch_by_title = right +# window_title_condition = True +# elif left in ("*window title", "*tab title"): +# switch_by_title = right +# partial_match = True +# window_title_condition = True +# elif left in ("window index", "tab index"): +# switch_by_index = right +# window_index_condition = True +# window_title_condition = False +# elif left == "playwright": +# playwright_enabled = right.lower() == "true" + +# # Validate that at least one switching condition was provided +# if not window_title_condition and not window_index_condition: +# CommonUtil.ExecLog( +# sModuleInfo, +# "Unable to switch window/tab: Neither 'window title'/'tab title' nor 'window index'/'tab index' was provided. Please provide either a title (exact or partial with *) or an index to switch to.", +# 3, +# ) +# return "zeuz_failed" + +# except Exception: +# CommonUtil.ExecLog( +# sModuleInfo, +# "Unable to parse data. Maintain correct format written in documentation", +# 3, +# ) +# return "zeuz_failed" + +# try: +# import time # Import time for both Playwright and Selenium paths + +# playwright_requested = playwright_enabled +# debug_port = None +# if playwright_enabled: +# debug_port = _switch_tab_get_debug_port() +# if not debug_port: +# CommonUtil.ExecLog( +# sModuleInfo, +# "Playwright tab switching requires a Chromium remote debugging port. Falling back to Selenium", +# 2, +# ) +# playwright_enabled = False + +# if playwright_enabled: +# CommonUtil.ExecLog(sModuleInfo, "Playwright is enabled (using async API)", 1) +# import asyncio +# from playwright.async_api import async_playwright + +# # Async function to handle Playwright operations +# async def run_playwright_switch(): +# async with async_playwright() as p: +# browser = await p.chromium.connect_over_cdp(f"http://127.0.0.1:{debug_port}") +# context = browser.contexts[0] +# pages = context.pages + +# result_data = {"status": None, "target_url": None, "error": None} + +# # Handle title-based tab switch +# if window_title_condition: +# for page in pages: +# page_title = await page.title() +# if ( +# partial_match +# and switch_by_title.lower() in page_title.lower() +# ) or ( +# not partial_match +# and switch_by_title.lower() == page_title.lower() +# ): +# # Step 1: Use Playwright to switch tabs +# await page.bring_to_front() +# await asyncio.sleep(1) + +# # Store target URL for Selenium alignment +# result_data["target_url"] = page.url +# result_data["status"] = "found" +# return result_data + +# result_data["status"] = "not_found" +# result_data["error"] = f"Playwright: No tab with title '{switch_by_title}' found" +# return result_data + +# # Index-based switching not supported with Playwright due to CDP ordering inconsistency +# elif window_index_condition: +# result_data["status"] = "not_supported" +# result_data["error"] = "Index-based tab switching is not supported with Playwright. Use title-based switching instead." +# return result_data + +# return result_data + +# # Run async Playwright code from sync context +# try: +# # We're always called from async context, so we need to run in a new thread with new event loop +# loop = asyncio.get_running_loop() +# from concurrent.futures import ThreadPoolExecutor + +# def run_in_thread(): +# new_loop = asyncio.new_event_loop() +# asyncio.set_event_loop(new_loop) +# try: +# return new_loop.run_until_complete(run_playwright_switch()) +# finally: +# new_loop.close() + +# with ThreadPoolExecutor(max_workers=1) as executor: +# future = executor.submit(run_in_thread) +# playwright_result = future.result(timeout=30) + +# if playwright_result["status"] == "found": +# # Step 3: Re-align Selenium to match the target tab +# target_url = playwright_result["target_url"] +# for handle in selenium_driver.window_handles: +# selenium_driver.switch_to.window(handle) +# if ( +# selenium_driver.current_url == target_url +# or target_url in selenium_driver.title +# ): +# CommonUtil.ExecLog( +# sModuleInfo, +# f"Selenium aligned to: {selenium_driver.title}", +# 1, +# ) +# return "passed" + +# CommonUtil.ExecLog( +# sModuleInfo, +# "Failed to align Selenium with target tab", +# 3, +# ) +# return "zeuz_failed" + +# elif playwright_result["status"] == "not_found": +# CommonUtil.ExecLog( +# sModuleInfo, +# playwright_result["error"], +# 3, +# ) +# return "zeuz_failed" + +# elif playwright_result["status"] == "not_supported": +# CommonUtil.ExecLog( +# sModuleInfo, +# playwright_result["error"], +# 3, +# ) +# return "zeuz_failed" + +# except Exception as e: +# CommonUtil.ExecLog( +# sModuleInfo, +# f"Playwright tab switching failed: {e}. Falling back to Selenium", +# 2, +# ) +# playwright_enabled = False +# # Continue with Selenium fallback + +# if not playwright_enabled: +# # --- Selenium tab switching --- +# CommonUtil.ExecLog(sModuleInfo, "Using Selenium for tab switching", 1) +# if window_title_condition: +# all_windows = selenium_driver.window_handles +# current_window = selenium_driver.current_window_handle +# window_handles_found = False + +# for _ in range(3): # retry +# for handle in all_windows: +# selenium_driver.switch_to.window(handle) +# if ( +# partial_match +# and switch_by_title.lower() in selenium_driver.title.lower() +# ) or ( +# not partial_match +# and switch_by_title.lower() == selenium_driver.title.lower() +# ): +# window_handles_found = True +# CommonUtil.ExecLog( +# sModuleInfo, +# f"Tab switched to '{selenium_driver.title}'", +# 1, +# ) +# break +# else: +# CommonUtil.ExecLog( +# sModuleInfo, +# "Couldn't find the title. Trying again after 1 second delay", +# 2, +# ) +# time.sleep(1) +# continue +# break + +# if not window_handles_found: +# selenium_driver.switch_to.window(current_window) +# CommonUtil.ExecLog( +# sModuleInfo, +# "Unable to find the title among the tabs. Use '*tab title' for partial match.", +# 3, +# ) +# return "zeuz_failed" + +# elif window_index_condition: +# window_index = int(switch_by_index) +# window_to_switch = selenium_driver.window_handles[window_index] +# selenium_driver.switch_to.window(window_to_switch) +# CommonUtil.ExecLog( +# sModuleInfo, +# f"Tab switched to index {switch_by_index} title {selenium_driver.title}", +# 1, +# ) + +# return "passed" + +# except Exception: +# CommonUtil.ExecLog(sModuleInfo, "Unable to switch your tab", 3) +# return CommonUtil.Exception_Handler(sys.exc_info()) + + + @logger def close_tab(step_data): """ @@ -3810,7 +4146,7 @@ def close_tab(step_data): Field Sub Field Value close tab selenium action close tab # Closes current active tab - + #Close tab with out title or index provided is not supported with Playwright. Will fall back to selenium. """ sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME global selenium_driver @@ -3845,6 +4181,25 @@ def close_tab(step_data): return "zeuz_failed" try: + playwright_requested = playwright_enabled + debug_port = None + if playwright_enabled: + debug_port = _switch_tab_get_debug_port() + if not debug_port: + CommonUtil.ExecLog( + sModuleInfo, + "Playwright tab closing requires a Chromium remote debugging port. Falling back to Selenium", + 2, + ) + playwright_enabled = False + elif not tab_title and tab_index is None: + CommonUtil.ExecLog( + sModuleInfo, + "Playwright tab closing requires tab title or tab index. Falling back to Selenium", + 2, + ) + playwright_enabled = False + if playwright_enabled: # --- Playwright tab closing --- CommonUtil.ExecLog(sModuleInfo, "Using Playwright for tab closing (async API)", 1) @@ -3854,7 +4209,6 @@ def close_tab(step_data): # Async function to handle Playwright operations async def run_playwright_close(): async with async_playwright() as p: - debug_port = selenium_details[current_driver_id]["remote-debugging-port"] browser = await p.chromium.connect_over_cdp(f"http://localhost:{debug_port}") context = browser.contexts[0] pages = context.pages @@ -3900,9 +4254,10 @@ async def run_playwright_close(): # Find the page with matching URL for page in pages: if page.url == desired_url: + page_title = await page.title() await page.close() result_data["status"] = "closed" - result_data["page_title"] = f"Tab at index {idx}" + result_data["page_title"] = page_title return result_data result_data["status"] = "not_found" @@ -3979,28 +4334,14 @@ async def run_playwright_close(): result_data["error"] = "Playwright: No tabs found to close" return result_data - # Run async Playwright code from sync context + # Run async Playwright code from sync context (works with or without a running loop) try: - # We're always called from async context, so we need to run in a new thread with new event loop - loop = asyncio.get_running_loop() - from concurrent.futures import ThreadPoolExecutor - - def run_in_thread(): - new_loop = asyncio.new_event_loop() - asyncio.set_event_loop(new_loop) - try: - return new_loop.run_until_complete(run_playwright_close()) - finally: - new_loop.close() - - with ThreadPoolExecutor(max_workers=1) as executor: - future = executor.submit(run_in_thread) - playwright_result = future.result(timeout=30) - + playwright_result = _switch_tab_run_async(run_playwright_close()) + if playwright_result["status"] == "closed": CommonUtil.ExecLog( sModuleInfo, - f"Playwright: Tab closed '{playwright_result['page_title']}'", + f"Tab closed '{playwright_result['page_title']}'", 1, ) return "passed" @@ -4012,11 +4353,15 @@ def run_in_thread(): playwright_result["error"], 3, ) - # Don't return here - let it fall back to Selenium + CommonUtil.ExecLog( + sModuleInfo, + "Falling back to Selenium for tab closing", + 2, + ) playwright_enabled = False else: return "zeuz_failed" - + except Exception as e: CommonUtil.ExecLog( sModuleInfo, @@ -4028,6 +4373,12 @@ def run_in_thread(): # --- Selenium tab closing --- if not playwright_enabled: + if playwright_requested: + CommonUtil.ExecLog( + sModuleInfo, + "Using Selenium for tab closing (Playwright fallback)", + 1, + ) window_handles_found = False if tab_title: diff --git a/tests/test_close_tab.py b/tests/test_close_tab.py index 47fe3ed03..07ab2f9fc 100644 --- a/tests/test_close_tab.py +++ b/tests/test_close_tab.py @@ -108,6 +108,19 @@ def mock_cdp_response(): return mock_response +@pytest.fixture +def mock_cdp_env(): + """Patch selenium_details with a debug port for Playwright close_tab tests.""" + with patch.dict( + "Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_details", + {"test_driver": {"remote-debugging-port": 9222}}, + ), patch( + "Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.current_driver_id", + "test_driver", + ): + yield + + def test_parse_data_failure(): """Test handling of malformed step_data""" malformed_data = [("invalid", "data")] @@ -181,181 +194,193 @@ def test_selenium_close_multiple_tabs_by_index(mock_exec_log, mock_driver): assert result == "passed" -@patch("urllib.request.urlopen") +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_driver") @patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") -def test_playwright_close_tab_by_title( - mock_exec_log, mock_urlopen, mock_step_data_with_title, mock_playwright_objects +def test_playwright_close_falls_back_when_debug_port_missing( + mock_exec_log, mock_driver, mock_step_data_with_title ): - """Test Playwright closing tab by title""" - # Mock CDP response - mock_urlopen.return_value.__enter__.return_value = mock_playwright_objects["page"] + mock_driver.current_window_handle = "window1" + mock_driver.window_handles = ["window1", "window2"] + mock_driver.title = "Google" + mock_driver.close.return_value = None + mock_driver.switch_to.window.return_value = None - with patch( - "playwright.sync_api.sync_playwright", - return_value=mock_playwright_objects["playwright"], + with patch.dict( + "Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_details", + {}, + clear=True, + ), patch( + "Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.current_driver_id", + None, ): result = close_tab(mock_step_data_with_title) - # Verify the tab was closed - mock_playwright_objects["page"].close.assert_called_once() - # Check that the success message was logged (among other calls) mock_exec_log.assert_any_call( - "close_tab : BuiltInFunctions", "Playwright: Tab closed 'Google'", 1 + "close_tab : BuiltInFunctions", + "Playwright tab closing requires a Chromium remote debugging port. Falling back to Selenium", + 2, + ) + mock_exec_log.assert_any_call( + "close_tab : BuiltInFunctions", + "Using Selenium for tab closing (Playwright fallback)", + 1, ) assert result == "passed" -@patch("urllib.request.urlopen") +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_driver") +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") +def test_playwright_close_falls_back_when_no_title_or_index( + mock_exec_log, mock_driver, mock_step_data_playwright, mock_cdp_env +): + mock_driver.current_window_handle = "window1" + mock_driver.window_handles = ["window1"] + mock_driver.title = "Test Tab" + mock_driver.close.return_value = None + mock_driver.switch_to.window.return_value = None + + result = close_tab(mock_step_data_playwright) + + mock_exec_log.assert_any_call( + "close_tab : BuiltInFunctions", + "Playwright tab closing requires tab title or tab index. Falling back to Selenium", + 2, + ) + mock_exec_log.assert_any_call( + "close_tab : BuiltInFunctions", + "Using Selenium for tab closing (Playwright fallback)", + 1, + ) + mock_exec_log.assert_any_call( + "close_tab : BuiltInFunctions", + "Current tab closed 'Test Tab'", + 1, + ) + mock_driver.close.assert_called_once() + assert result == "passed" + + +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions._switch_tab_run_async") +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") +def test_playwright_close_tab_by_title( + mock_exec_log, mock_run_async, mock_step_data_with_title, mock_cdp_env +): + """Test Playwright closing tab by title""" + mock_run_async.return_value = {"status": "closed", "page_title": "Google"} + + result = close_tab(mock_step_data_with_title) + + mock_run_async.assert_called_once() + mock_exec_log.assert_any_call( + "close_tab : BuiltInFunctions", "Tab closed 'Google'", 1 + ) + assert result == "passed" + + +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions._switch_tab_run_async") @patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") def test_playwright_close_tab_by_index( - mock_exec_log, mock_urlopen, mock_step_data_with_index, mock_playwright_objects + mock_exec_log, mock_run_async, mock_step_data_with_index, mock_cdp_env ): """Test Playwright closing tab by index""" - # Mock CDP response - mock_response = MagicMock() - mock_response.read.return_value = json.dumps( - [ - {"type": "page", "url": "https://www.google.com", "title": "Google"}, - {"type": "page", "url": "https://www.youtube.com", "title": "YouTube"}, - ] - ).encode() - mock_urlopen.return_value.__enter__.return_value = mock_response + mock_run_async.return_value = {"status": "closed", "page_title": "YouTube"} - # Fix: Set the mock page URL to match what the function expects - # The function reverses the target_urls, so index 0 will be 'https://www.youtube.com' - mock_playwright_objects["page"].url = "https://www.youtube.com" - - with patch( - "playwright.sync_api.sync_playwright", - return_value=mock_playwright_objects["playwright"], - ): - result = close_tab(mock_step_data_with_index) + result = close_tab(mock_step_data_with_index) - # Verify the tab was closed - mock_playwright_objects["page"].close.assert_called_once() - # Check that the success message was logged (among other calls) + mock_run_async.assert_called_once() mock_exec_log.assert_any_call( - "close_tab : BuiltInFunctions", "Playwright: Tab closed at index 0", 1 + "close_tab : BuiltInFunctions", "Tab closed 'YouTube'", 1 ) assert result == "passed" -@patch("urllib.request.urlopen") +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_driver") @patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") def test_playwright_close_current_active_tab( - mock_exec_log, mock_urlopen, mock_step_data_playwright, mock_playwright_objects + mock_exec_log, mock_driver, mock_step_data_playwright, mock_cdp_env ): - """Test Playwright closing current active tab""" - # Mock CDP response - mock_response = MagicMock() - mock_response.read.return_value = json.dumps( - [{"type": "page", "url": "https://www.google.com", "title": "Google"}] - ).encode() - mock_urlopen.return_value.__enter__.return_value = mock_response - - # Mock page to have focus - mock_playwright_objects["page"].evaluate.return_value = True + """Playwright without tab title/index falls back to Selenium current-tab close""" + mock_driver.current_window_handle = "window1" + mock_driver.window_handles = ["window1"] + mock_driver.title = "Google" + mock_driver.close.return_value = None + mock_driver.switch_to.window.return_value = None - with patch( - "playwright.sync_api.sync_playwright", - return_value=mock_playwright_objects["playwright"], - ): - result = close_tab(mock_step_data_playwright) + result = close_tab(mock_step_data_playwright) - # Verify the tab was closed - mock_playwright_objects["page"].close.assert_called_once() - # Check that the success message was logged (among other calls) mock_exec_log.assert_any_call( - "close_tab : BuiltInFunctions", "Playwright: Current tab closed 'Google'", 1 + "close_tab : BuiltInFunctions", + "Playwright tab closing requires tab title or tab index. Falling back to Selenium", + 2, ) + mock_driver.close.assert_called_once() assert result == "passed" -@patch("urllib.request.urlopen") +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_driver") @patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") def test_playwright_close_last_remaining_tab( - mock_exec_log, mock_urlopen, mock_step_data_playwright, mock_playwright_objects + mock_exec_log, mock_driver, mock_step_data_playwright, mock_cdp_env ): - """Test Playwright closing the last remaining tab""" - # Mock CDP response - mock_response = MagicMock() - mock_response.read.return_value = json.dumps( - [{"type": "page", "url": "https://www.google.com", "title": "Google"}] - ).encode() - mock_urlopen.return_value.__enter__.return_value = mock_response - - # Mock page to not have focus (will fall back to CDP) - mock_playwright_objects["page"].evaluate.return_value = False + """Closing the last tab via Selenium fallback still passes""" + mock_driver.current_window_handle = "window1" + mock_driver.window_handles = ["window1"] + mock_driver.title = "Google" + mock_driver.close.return_value = None + mock_driver.switch_to.window.return_value = None - with patch( - "playwright.sync_api.sync_playwright", - return_value=mock_playwright_objects["playwright"], - ): - result = close_tab(mock_step_data_playwright) + result = close_tab(mock_step_data_playwright) - # Verify the tab was closed (should work even for last tab) - mock_playwright_objects["page"].close.assert_called_once() + mock_driver.close.assert_called_once() assert result == "passed" -@patch("urllib.request.urlopen") +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions._switch_tab_run_async") +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_driver") @patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") def test_playwright_no_tabs_found( - mock_exec_log, mock_urlopen, mock_step_data_playwright + mock_exec_log, mock_driver, mock_run_async, mock_step_data_with_title, mock_cdp_env ): - """Test Playwright when no tabs are found""" - # Mock CDP response - mock_response = MagicMock() - mock_response.read.return_value = json.dumps( - [{"type": "page", "url": "https://www.google.com", "title": "Google"}] - ).encode() - mock_urlopen.return_value.__enter__.return_value = mock_response - - # Mock Playwright objects - no pages - mock_context = MagicMock() - mock_context.pages = [] - - mock_browser = MagicMock() - mock_browser.contexts = [mock_context] - - mock_playwright = MagicMock() - mock_playwright.chromium.connect_over_cdp.return_value = mock_browser - mock_playwright.__enter__.return_value = mock_playwright - mock_playwright.__exit__.return_value = None + """Test Playwright when no tabs are found, then Selenium also fails""" + mock_run_async.return_value = { + "status": "no_tabs", + "error": "Playwright: No tabs found to close", + } + mock_driver.current_window_handle = "window1" + mock_driver.window_handles = ["window1"] + mock_driver.title = "Other Tab" + mock_driver.switch_to.window.return_value = None - with patch("playwright.sync_api.sync_playwright", return_value=mock_playwright): - result = close_tab(mock_step_data_playwright) + result = close_tab(mock_step_data_with_title) - # Verify error message mock_exec_log.assert_any_call( "close_tab : BuiltInFunctions", "Playwright: No tabs found to close", 3 ) + mock_exec_log.assert_any_call( + "close_tab : BuiltInFunctions", + "Falling back to Selenium for tab closing", + 2, + ) + mock_exec_log.assert_any_call( + "close_tab : BuiltInFunctions", "No tab with title 'Google' found", 3 + ) assert result == "zeuz_failed" -@patch("urllib.request.urlopen") +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions._switch_tab_run_async") +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_driver") @patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") -def test_playwright_invalid_tab_index(mock_exec_log, mock_urlopen): - """Test Playwright with invalid tab index""" - # Mock CDP response - mock_response = MagicMock() - mock_response.read.return_value = json.dumps( - [{"type": "page", "url": "https://www.google.com", "title": "Google"}] - ).encode() - mock_urlopen.return_value.__enter__.return_value = mock_response - - # Mock Playwright objects - mock_page = MagicMock() - mock_context = MagicMock() - mock_context.pages = [mock_page] - - mock_browser = MagicMock() - mock_browser.contexts = [mock_context] - - mock_playwright = MagicMock() - mock_playwright.chromium.connect_over_cdp.return_value = mock_playwright - mock_playwright.__enter__.return_value = mock_playwright - mock_playwright.__exit__.return_value = None +def test_playwright_invalid_tab_index( + mock_exec_log, mock_driver, mock_run_async, mock_cdp_env +): + """Test Playwright with invalid tab index, then Selenium validation failure""" + mock_run_async.return_value = { + "status": "invalid_index", + "error": "Playwright: Invalid tab index 'invalid'", + } + mock_driver.current_window_handle = "window1" + mock_driver.window_handles = ["window1"] + mock_driver.switch_to.window.return_value = None step_data_invalid_index = [ ("close tab", "selenium action", "close tab"), @@ -363,46 +388,43 @@ def test_playwright_invalid_tab_index(mock_exec_log, mock_urlopen): ("tab index", "input parameter", "invalid"), ] - with patch("playwright.sync_api.sync_playwright", return_value=mock_playwright): - result = close_tab(step_data_invalid_index) + result = close_tab(step_data_invalid_index) - # Verify error message mock_exec_log.assert_any_call( "close_tab : BuiltInFunctions", "Playwright: Invalid tab index 'invalid'", 3 ) + mock_exec_log.assert_any_call( + "close_tab : BuiltInFunctions", "Invalid tab index 'invalid'", 3 + ) assert result == "zeuz_failed" -@patch("urllib.request.urlopen") +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions._switch_tab_run_async") +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_driver") @patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") def test_playwright_fallback_to_selenium( - mock_exec_log, mock_urlopen, mock_step_data_playwright + mock_exec_log, mock_driver, mock_run_async, mock_step_data_with_title, mock_cdp_env ): - """Test Playwright fallback to Selenium when Playwright fails""" - # Mock CDP response to fail - mock_urlopen.side_effect = Exception("Connection failed") - - # Mock selenium driver for fallback - with patch( - "Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_driver" - ) as mock_driver: - mock_driver.current_window_handle = "window1" - mock_driver.window_handles = ["window1"] - mock_driver.title = "Test Tab" - mock_driver.close.return_value = None - mock_driver.switch_to.window.return_value = None - - result = close_tab(mock_step_data_playwright) - - # Verify fallback message and Selenium execution - # The actual error message is about CDP connection failure + """Test Playwright fallback to Selenium when Playwright raises""" + mock_run_async.side_effect = Exception("Connection failed") + mock_driver.current_window_handle = "window1" + mock_driver.window_handles = ["window1", "window2"] + mock_driver.title = "Google" + mock_driver.close.return_value = None + mock_driver.switch_to.window.return_value = None + + result = close_tab(mock_step_data_with_title) + mock_exec_log.assert_any_call( "close_tab : BuiltInFunctions", - "Playwright tab closing failed: BrowserType.connect_over_cdp: connect ECONNREFUSED ::1:9222\nCall log:\n - retrieving websocket url from http://localhost:9222\n. Falling back to Selenium", + "Playwright tab closing failed: Connection failed. Falling back to Selenium", 2, ) - - # Verify that Selenium was used as fallback + mock_exec_log.assert_any_call( + "close_tab : BuiltInFunctions", + "Using Selenium for tab closing (Playwright fallback)", + 1, + ) mock_driver.close.assert_called_once() assert result == "passed" diff --git a/tests/test_switch_tab.py b/tests/test_switch_tab.py index b36b6ce4e..29807b713 100644 --- a/tests/test_switch_tab.py +++ b/tests/test_switch_tab.py @@ -122,6 +122,19 @@ def mock_selenium_details(): } +@pytest.fixture +def mock_cdp_env(): + """Patch selenium_details with a debug port for Playwright switch_tab tests.""" + with patch.dict( + "Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_details", + {"test_driver": {"remote-debugging-port": 9222}}, + ), patch( + "Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.current_driver_id", + "test_driver", + ): + yield + + def test_parse_data_failure(): """Test handling of malformed step_data""" malformed_data = [("invalid", "data")] @@ -212,148 +225,119 @@ def test_selenium_switch_by_index_success(mock_exec_log, mock_driver, mock_step_ mock_driver.switch_to.window.assert_called_with("window2") # Index 1 # Check that success message was logged mock_exec_log.assert_any_call( - "switch_window_or_tab : BuiltInFunctions", - "Tab switched to index 1 title Tab at Index 1", - 1 + "switch_window_or_tab : BuiltInFunctions", + "Tab switched to index 1 title 'Tab at Index 1'", + 1, ) assert result == "passed" +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions._switch_tab_run_async") @patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_driver") @patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") -@patch("time.sleep") def test_playwright_switch_by_title_success( - mock_sleep, mock_exec_log, mock_driver, mock_step_data_playwright_title, mock_playwright_objects + mock_exec_log, mock_driver, mock_run_async, mock_step_data_playwright_title, mock_cdp_env ): """Test Playwright tab switching by title - success case""" - # Configure selenium_details and current_driver_id mock - with patch.dict("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_details", - {"test_driver": {"remote-debugging-port": 9222}}), \ - patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.current_driver_id", "test_driver"): - - # Configure selenium driver mock for re-alignment - mock_driver.window_handles = ["window1", "window2"] - mock_driver.current_url = "https://www.google.com" - mock_driver.title = "Google" - mock_driver.switch_to.window.return_value = None - - with patch( - "playwright.sync_api.sync_playwright", - return_value=mock_playwright_objects["playwright"], - ): - result = switch_window_or_tab(mock_step_data_playwright_title) + mock_run_async.return_value = { + "status": "found", + "target_url": "https://www.google.com", + } + mock_driver.window_handles = ["window1", "window2"] + mock_driver.current_url = "https://www.google.com" + mock_driver.title = "Google" + mock_driver.switch_to.window.return_value = None - # Verify Playwright bring_to_front was called - mock_playwright_objects["page1"].bring_to_front.assert_called_once() - - # Verify Selenium re-alignment was attempted - assert mock_driver.switch_to.window.call_count >= 1 - - # Check that success message was logged - mock_exec_log.assert_any_call( - "switch_window_or_tab : BuiltInFunctions", - "Selenium aligned to: Google", - 1 - ) - assert result == "passed" + result = switch_window_or_tab(mock_step_data_playwright_title) + + mock_run_async.assert_called_once() + assert mock_driver.switch_to.window.call_count >= 1 + mock_exec_log.assert_any_call( + "switch_window_or_tab : BuiltInFunctions", + "Tab switch and Selenium aligned to: Google", + 1, + ) + assert result == "passed" +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions._switch_tab_run_async") @patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") def test_playwright_switch_by_title_not_found( - mock_exec_log, mock_playwright_objects + mock_exec_log, mock_run_async, mock_cdp_env ): """Test Playwright tab switching by title - title not found""" - # Configure selenium_details and current_driver_id mock - with patch.dict("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_details", - {"test_driver": {"remote-debugging-port": 9222}}), \ - patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.current_driver_id", "test_driver"): - - step_data = [ - ("window title", "input parameter", "NonExistentTitle"), - ("playwright", "option", "true"), - ("switch window/tab", "selenium action", "switch window or frame"), - ] - - with patch( - "playwright.sync_api.sync_playwright", - return_value=mock_playwright_objects["playwright"], - ): - result = switch_window_or_tab(step_data) - - # Check that error message was logged - mock_exec_log.assert_any_call( - "switch_window_or_tab : BuiltInFunctions", - "Playwright: No tab with title 'NonExistentTitle' found", - 3 - ) - assert result == "zeuz_failed" + mock_run_async.return_value = { + "status": "not_found", + "error": "Playwright: No tab with title 'NonExistentTitle' found", + } + + step_data = [ + ("window title", "input parameter", "NonExistentTitle"), + ("playwright", "option", "true"), + ("switch window/tab", "selenium action", "switch window or frame"), + ] + + result = switch_window_or_tab(step_data) + + mock_exec_log.assert_any_call( + "switch_window_or_tab : BuiltInFunctions", + "Playwright: No tab with title 'NonExistentTitle' found", + 3, + ) + assert result == "zeuz_failed" +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions._switch_tab_run_async") @patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") def test_playwright_switch_by_index_not_supported( - mock_exec_log, mock_step_data_playwright_index, mock_playwright_objects + mock_exec_log, mock_run_async, mock_step_data_playwright_index, mock_cdp_env ): """Test Playwright tab switching by index - not supported""" - # Configure selenium_details and current_driver_id mock - with patch.dict("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_details", - {"test_driver": {"remote-debugging-port": 9222}}), \ - patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.current_driver_id", "test_driver"): - - with patch( - "playwright.sync_api.sync_playwright", - return_value=mock_playwright_objects["playwright"], - ): - result = switch_window_or_tab(mock_step_data_playwright_index) - - # Check that error message was logged - mock_exec_log.assert_any_call( - "switch_window_or_tab : BuiltInFunctions", - "Index-based tab switching is not supported with Playwright. Use title-based switching instead.", - 3 - ) - assert result == "zeuz_failed" + mock_run_async.return_value = { + "status": "not_supported", + "error": "Index-based tab switching is not supported with Playwright. Use title-based switching instead.", + } + result = switch_window_or_tab(mock_step_data_playwright_index) + mock_exec_log.assert_any_call( + "switch_window_or_tab : BuiltInFunctions", + "Index-based tab switching is not supported with Playwright. Use title-based switching instead.", + 3, + ) + assert result == "zeuz_failed" + + +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions._switch_tab_run_async") @patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_driver") @patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") -@patch("time.sleep") def test_playwright_alignment_failure( - mock_sleep, mock_exec_log, mock_driver, mock_playwright_objects + mock_exec_log, mock_driver, mock_run_async, mock_cdp_env ): """Test Playwright tab switching - Selenium alignment failure""" - # Configure selenium_details and current_driver_id mock - with patch.dict("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_details", - {"test_driver": {"remote-debugging-port": 9222}}), \ - patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.current_driver_id", "test_driver"): - - # Configure selenium driver mock - URLs don't match for alignment - mock_driver.window_handles = ["window1", "window2"] - mock_driver.current_url = "https://www.different.com" - mock_driver.title = "Different Title" - mock_driver.switch_to.window.return_value = None - - step_data = [ - ("window title", "input parameter", "Google"), - ("playwright", "option", "true"), - ("switch window/tab", "selenium action", "switch window or frame"), - ] - - with patch( - "playwright.sync_api.sync_playwright", - return_value=mock_playwright_objects["playwright"], - ): - result = switch_window_or_tab(step_data) - - # Verify Playwright bring_to_front was called - mock_playwright_objects["page1"].bring_to_front.assert_called_once() - - # Check that alignment failure message was logged - mock_exec_log.assert_any_call( - "switch_window_or_tab : BuiltInFunctions", - "Failed to align Selenium with target tab", - 3 - ) - assert result == "zeuz_failed" + mock_run_async.return_value = { + "status": "found", + "target_url": "https://www.google.com", + } + mock_driver.window_handles = ["window1", "window2"] + mock_driver.current_url = "https://www.different.com" + mock_driver.title = "Different Title" + mock_driver.switch_to.window.return_value = None + + step_data = [ + ("window title", "input parameter", "Google"), + ("playwright", "option", "true"), + ("switch window/tab", "selenium action", "switch window or frame"), + ] + + result = switch_window_or_tab(step_data) + + mock_exec_log.assert_any_call( + "switch_window_or_tab : BuiltInFunctions", + "Failed to align Selenium with target tab", + 3, + ) + assert result == "zeuz_failed" @patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") @@ -377,6 +361,36 @@ def test_playwright_connection_failure(mock_exec_log, mock_step_data_playwright_ assert result == "zeuz_failed" +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_driver") +@patch("Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.CommonUtil.ExecLog") +def test_playwright_switch_falls_back_when_debug_port_missing( + mock_exec_log, mock_driver, mock_step_data_playwright_title +): + mock_driver.current_window_handle = "window1" + mock_driver.window_handles = ["window1"] + mock_driver.title = "Google" + mock_driver.current_url = "https://www.google.com" + mock_driver.capabilities = {} + mock_driver.switch_to.window.return_value = None + + with patch.dict( + "Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.selenium_details", + {}, + clear=True, + ), patch( + "Framework.Built_In_Automation.Web.Selenium.BuiltInFunctions.current_driver_id", + None, + ): + result = switch_window_or_tab(mock_step_data_playwright_title) + + mock_exec_log.assert_any_call( + "switch_window_or_tab : BuiltInFunctions", + "Playwright tab switching requires a Chromium remote debugging port. Falling back to Selenium", + 2, + ) + assert result == "passed" + + # Parametrized tests for better coverage @pytest.mark.parametrize( "playwright_flag,expected_playwright",