diff --git a/Framework/Built_In_Automation/Sequential_Actions/action_declarations/playwright.py b/Framework/Built_In_Automation/Sequential_Actions/action_declarations/playwright.py index d0aca46a..f98188b5 100644 --- a/Framework/Built_In_Automation/Sequential_Actions/action_declarations/playwright.py +++ b/Framework/Built_In_Automation/Sequential_Actions/action_declarations/playwright.py @@ -11,6 +11,7 @@ declarations = ( # Browser Management { "name": "open browser", "function": "Open_Browser", "screenshot": "web" }, + { "name": "open electron app", "function": "Open_Electron_App", "screenshot": "web" }, { "name": "go to link", "function": "Go_To_Link", "screenshot": "web" }, { "name": "tear down browser", "function": "Tear_Down_Playwright", "screenshot": "none" }, { "name": "teardown", "function": "Tear_Down_Playwright", "screenshot": "none" }, @@ -34,6 +35,7 @@ # Element Information { "name": "save attribute", "function": "Save_Attribute", "screenshot": "web" }, + { "name": "change attribute value", "function": "Change_Attribute_Value", "screenshot": "web" }, { "name": "get element info", "function": "get_element_info", "screenshot": "web" }, { "name": "extract table data", "function": "Extract_Table_Data", "screenshot": "web" }, @@ -45,6 +47,11 @@ { "name": "scroll", "function": "Scroll", "screenshot": "web" }, { "name": "scroll to element", "function": "scroll_to_element", "screenshot": "web" }, { "name": "scroll element to top", "function": "scroll_to_element", "screenshot": "web" }, + { "name": "scroll to top", "function": "scroll_to_top", "screenshot": "web" }, + + # Lists / attributes + { "name": "save attribute values in list", "function": "save_attribute_values_in_list", "screenshot": "web" }, + { "name": "save web elements in list", "function": "save_web_elements_in_list", "screenshot": "web" }, # Selection (Dropdowns/Checkboxes) { "name": "select by visible text", "function": "Select_Deselect", "screenshot": "web" }, @@ -55,6 +62,7 @@ { "name": "deselect by index", "function": "Select_Deselect", "screenshot": "web" }, { "name": "deselect all", "function": "Select_Deselect", "screenshot": "web" }, { "name": "check uncheck", "function": "check_uncheck", "screenshot": "web" }, + { "name": "multiple check uncheck", "function": "multiple_check_uncheck", "screenshot": "web" }, # Window/Tab Management { "name": "switch window", "function": "switch_window_or_tab", "screenshot": "web" }, diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 679ea783..64f4d0d9 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -24,6 +24,7 @@ import sys import os import inspect +import platform import time import re from pathlib import Path @@ -471,17 +472,186 @@ async def Open_Browser(step_data): @logger -async def Go_To_Link(step_data): +async def Open_Electron_App(step_data): """ - Navigate to a URL. + Launch an Electron desktop app via Playwright's Electron API. - Example: + Example - Basic (per-OS binary paths, like Selenium): Field Sub Field Value - go to link input parameter https://example.com - wait until optional parameter networkidle - go to link playwright action go to link + windows input parameter C:\\Path\\To\\MyApp.exe + mac input parameter /Applications/MyApp.app/Contents/MacOS/MyApp + linux input parameter /opt/myapp/myapp + open electron app playwright action open electron app + + Example - With optional parameters: + Field Sub Field Value + mac input parameter /Applications/MyApp.app/Contents/MacOS/MyApp + session optional parameter electron_1 + add argument optional parameter --no-sandbox + cwd optional parameter /tmp/working_dir + timeout optional parameter 30 + open electron app playwright action open electron app + + Notes: + - Only the path matching the current OS is used; other rows are ignored. + - The first Electron BrowserWindow becomes the active page, so subsequent + element / click / text actions work the same as in a normal browser session. + """ + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + global playwright_instance, browser, context, current_page + global current_page_id, playwright_details + + try: + desktop_app_path = "" + driver_id = "" + args = [] + cwd = None + env_vars = {} + timeout = None + record_video = False + video_dir = None + + for left, mid, right in step_data: + left_compact = left.replace(" ", "").replace("_", "").replace("-", "").lower() + mid_l = mid.strip().lower() + right_v = right.strip() + + if "windows" in left_compact and platform.system() == "Windows": + desktop_app_path = right_v + elif "mac" in left_compact and platform.system() == "Darwin": + desktop_app_path = right_v + elif "linux" in left_compact and platform.system() == "Linux": + desktop_app_path = right_v + elif left_compact == "driverid": + driver_id = right_v + elif left_compact == "session" and mid_l == "optional parameter": + driver_id = right_v + elif mid_l == "optional parameter": + if left_compact in ("addargument", "arg", "argument"): + args.append(right_v) + elif left_compact == "cwd": + cwd = right_v + elif left_compact == "env": + # Format: KEY=VALUE + if "=" in right_v: + k, v = right_v.split("=", 1) + env_vars[k.strip()] = v.strip() + elif left_compact == "timeout": + try: + timeout = int(float(right_v) * 1000) + except ValueError: + pass + elif left_compact == "recordvideo": + record_video = right_v.lower() in ("true", "yes", "1") + elif left_compact == "videodir": + video_dir = right_v + + if not desktop_app_path: + CommonUtil.ExecLog( + sModuleInfo, + f"You did not provide an Electron app path for {platform.system()} OS", + 3, + ) + return "zeuz_failed" + + if not driver_id: + driver_id = "default" + + desktop_app_path = CommonUtil.path_parser(desktop_app_path) + + # Reserve a debug port for the session even though Playwright drives Electron via CDP automatically. + electron_port = get_debug_port(driver_id or "electron", start=9230, stop=9320) + + launch_options = {"executable_path": desktop_app_path} + if args: + launch_options["args"] = args + if cwd: + launch_options["cwd"] = cwd + if env_vars: + launch_options["env"] = env_vars + if timeout: + launch_options["timeout"] = timeout + if record_video: + launch_options["record_video_dir"] = video_dir or "videos/" + + playwright_instance = await async_playwright().start() + try: + electron_app = await playwright_instance._electron.launch(**launch_options) + except Exception: + return CommonUtil.Exception_Handler(sys.exc_info()) + + try: + current_page = await electron_app.first_window() + except Exception: + # Some Electron apps create no visible BrowserWindow at startup. + current_page = None + + # In Electron there is no BrowserContext we own - bind the app object in its place so + # downstream session-aware code keeps working. + context = electron_app.context if hasattr(electron_app, "context") else None + browser = electron_app # `browser` slot holds the launched app for teardown. + current_page_id = driver_id + + playwright_details[driver_id] = { + "page": current_page, + "context": context, + "browser": electron_app, + "playwright": playwright_instance, + "remote-debugging-port": electron_port, + } + + sr.Set_Shared_Variables("playwright_page", current_page) + sr.Set_Shared_Variables("playwright_context", context) + sr.Set_Shared_Variables("playwright_browser", electron_app) + sr.Set_Shared_Variables("active_web_driver_type", "playwright") + if timeout: + sr.Set_Shared_Variables("element_wait", timeout / 1000) + CommonUtil.set_screenshot_vars(sr.Shared_Variable_Export()) + + create_browser_session( + session_name=driver_id, + playwright_page=current_page, + playwright_browser=electron_app, + playwright_context=context, + playwright_frame=None, + playwright_instance=playwright_instance, + remote_debugging_port=electron_port, + ) + + CommonUtil.ExecLog(sModuleInfo, "Started Electron App", 1) + return "passed" - wait until options: load, domcontentloaded, networkidle, commit + except Exception: + return CommonUtil.Exception_Handler(sys.exc_info()) + + +@logger +async def Go_To_Link(step_data): + """ + Navigate to a URL (and open browser if not already open). + + Example 1 - Basic: + Field Sub Field Value + go to link input parameter https://example.com + go to link playwright action go to link + + Example 2 - Selenium-compatible options: + Field Sub Field Value + go to link input parameter https://example.com + wait time to appear element optional parameter 20 + wait time to page load optional parameter 60 + resolution optional parameter 1920,1080 + wait until optional parameter networkidle + go to link playwright action go to link + + Options: + - wait until (load | domcontentloaded | networkidle | commit) + - timeout / wait time to page load: page load timeout in seconds + - wait for element / wait time to appear element: element wait timeout + (seconds) saved to the "element_wait" shared variable so subsequent + element lookups use it + - resolution: WIDTHxHEIGHT or WIDTH,HEIGHT (applied to the current page) + - session: reuse or create a named browser session """ sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME global current_page @@ -493,31 +663,30 @@ async def Go_To_Link(step_data): left_l = left.strip().lower() mid_l = mid.strip().lower() right_v = right.strip() - + if mid_l == "optional parameter" and left_l == "session": session_name = right_v break - + # Check if session exists and use it if session_name: existing_session = get_browser_session(session_name) - if existing_session and await _ensure_playwright_session(session_name, existing_session) not in failed_tag_list: CommonUtil.ExecLog(sModuleInfo, f"Using existing browser session: {session_name}", 1) else: # Session doesn't exist, open new browser with session name CommonUtil.ExecLog(sModuleInfo, f"Session '{session_name}' not found. Opening new browser.", 2) - + # Add session parameter to step_data for Open_Browser step_data_with_session = step_data.copy() if not any(left.strip().lower() == "session" and mid.strip().lower() == "optional parameter" for left, mid, right in step_data_with_session): step_data_with_session.append(("session", "optional parameter", session_name)) - + result = await Open_Browser(step_data_with_session) if result == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Failed to open browser for new session", 3) return "zeuz_failed" - + elif current_page is None: default_session = get_browser_session("default") if default_session and default_session.get("selenium_driver"): @@ -535,42 +704,80 @@ async def Go_To_Link(step_data): url = None wait_until = "domcontentloaded" timeout = None + element_wait_sec = None + window_size_x = None + window_size_y = None for left, mid, right in step_data: - left_l = left.strip().lower() + left_raw = left.strip() + left_l = left_raw.lower() + left_compact = left_l.replace(" ", "").replace("_", "").replace("-", "") mid_l = mid.strip().lower() right_v = right.strip() if left_l in ("go to link", "url", "link"): - url = right_v + url = right_v elif mid_l == "optional parameter": if left_l == "session": - # Skip session parameter - already processed above continue - elif left_l in ("wait until", "wait_until", "waituntil", "wait time"): + if left_l in ("wait until", "wait_until", "waituntil"): wait_until = right_v.lower() - elif left_l == "timeout": - timeout = int(float(right_v) * 1000) + elif left_compact in ("timeout", "waittimetopageload", "pageloadtimeout"): + try: + timeout = int(float(right_v) * 1000) + except ValueError: + pass + elif left_compact in ("waittimetoappearelement", "waitforelement", "elementwait"): + try: + element_wait_sec = float(right_v) + except ValueError: + pass + elif left_l == "resolution": + try: + parts = right_v.replace("x", ",").split(",") + window_size_x = int(parts[0].strip()) + window_size_y = int(parts[1].strip()) + except (ValueError, IndexError): + pass if not url: CommonUtil.ExecLog(sModuleInfo, "No URL provided", 3) return "zeuz_failed" + if element_wait_sec is not None: + sr.Set_Shared_Variables("element_wait", element_wait_sec) + + if timeout: + try: + current_page.set_default_navigation_timeout(timeout) + current_page.set_default_timeout(timeout) + except Exception: + pass + + if window_size_x and window_size_y: + try: + await current_page.set_viewport_size({"width": window_size_x, "height": window_size_y}) + except Exception: + pass + goto_options = {"wait_until": wait_until} if timeout: goto_options["timeout"] = timeout - await current_page.goto(url, **goto_options) - + try: + await current_page.goto(url, **goto_options) + except PlaywrightTimeoutError: + CommonUtil.ExecLog(sModuleInfo, "Maximum page load time reached. Loading and proceeding", 2) + # 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) + + CommonUtil.ExecLog(sModuleInfo, f"Successfully opened your link: {url}", 1) return "passed" except Exception: - return CommonUtil.Exception_Handler(sys.exc_info()) + return CommonUtil.Exception_Handler(sys.exc_info(), None, "failed to open your link") @logger @@ -814,7 +1021,7 @@ async def Switch_Browser(step_data): ######################### @logger -async def Click_Element(step_data): +async def Click_Element(step_data, retry=0): """ Click an element. @@ -823,19 +1030,24 @@ async def Click_Element(step_data): id element parameter submit-btn click playwright action click - Example 2 - With options: + Example 2 - With JS click (forces click via JS .click()): Field Sub Field Value id element parameter submit-btn use js optional parameter true - offset optional parameter 10,5 click playwright action click - Example 3 - Double click: + Example 3 - Click at offset (Selenium-compatible: percent from element center): + Field Sub Field Value + id element parameter submit-btn + offset optional parameter 20,30 + click playwright action click + + Example 4 - Double click: Field Sub Field Value id element parameter item double click playwright action double click - Example 4 - Right click: + Example 5 - Right click: Field Sub Field Value id element parameter item right click playwright action right click @@ -846,14 +1058,13 @@ async def Click_Element(step_data): try: # Handle session parameter session_name, current_page, current_page_id, context, browser = await _handle_playwright_session(step_data) - if current_page is None: CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) return "zeuz_failed" # Parse options use_js = False - offset = None + offset_value = "" double_click = False right_click = False click_count = 1 @@ -874,8 +1085,7 @@ async def Click_Element(step_data): if left_l == "use js": use_js = right_v.lower() in ("true", "yes", "1") elif left_l == "offset": - parts = right_v.split(",") - offset = {"x": float(parts[0].strip()), "y": float(parts[1].strip())} + offset_value = right_v elif left_l == "click count": click_count = int(right_v) elif left_l == "modifier": @@ -894,15 +1104,51 @@ async def Click_Element(step_data): # Get element 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) + CommonUtil.ExecLog(sModuleInfo, "Could not find element", 3) return "zeuz_failed" + # Click using offset (Selenium-compatible: percentage of half element size from center) + if offset_value: + try: + box = await locator.bounding_box() + if not box: + CommonUtil.ExecLog(sModuleInfo, "Cannot determine element bounding box for offset click", 3) + return "zeuz_failed" + parts = offset_value.replace(" ", "").split(",") + pct_x = float(parts[0]) + pct_y = float(parts[1]) + # Selenium-style: percent of half-size from center, anchored at top-left of element + offset_x = (box["width"] / 2.0) + (box["width"] / 2.0) * (pct_x / 100.0) + offset_y = (box["height"] / 2.0) + (box["height"] / 2.0) * (pct_y / 100.0) + click_options = {"position": {"x": offset_x, "y": offset_y}} + if modifiers: + click_options["modifiers"] = modifiers + if delay: + click_options["delay"] = delay + if timeout: + click_options["timeout"] = timeout + if right_click: + click_options["button"] = "right" + if double_click: + await locator.dblclick(**click_options) + else: + await locator.click(**click_options) + CommonUtil.ExecLog(sModuleInfo, "Click on location successful", 1) + return "passed" + except Exception: + return CommonUtil.Exception_Handler(sys.exc_info(), None, "Error clicking location") + + # JS click - matches Selenium use_js behavior (true HTMLElement.click() via JS) + if use_js: + try: + await locator.evaluate("el => el.click()") + CommonUtil.ExecLog(sModuleInfo, "Successfully clicked the element via JS", 1) + return "passed" + except Exception: + return CommonUtil.Exception_Handler(sys.exc_info()) + # Build click options click_options = {} - if use_js: - click_options["force"] = True - if offset: - click_options["position"] = offset if modifiers: click_options["modifiers"] = modifiers if delay: @@ -913,18 +1159,52 @@ async def Click_Element(step_data): click_options["click_count"] = click_count # Perform click - if double_click: - 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" - await locator.click(**click_options) - CommonUtil.ExecLog(sModuleInfo, "Right click performed", 1) - else: - await locator.click(**click_options) - CommonUtil.ExecLog(sModuleInfo, "Click performed", 1) - - return "passed" + try: + if double_click: + 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" + await locator.click(**click_options) + CommonUtil.ExecLog(sModuleInfo, "Right click performed", 1) + else: + await locator.click(**click_options) + CommonUtil.ExecLog(sModuleInfo, "Successfully clicked the element", 1) + return "passed" + except PlaywrightTimeoutError: + # Click intercepted or element not actionable - fall back to JS click (matches Selenium behavior) + try: + await locator.evaluate("el => el.click()") + CommonUtil.ExecLog( + sModuleInfo, + "Your element is overlapped with another sibling element. Clicked the element successfully by executing JavaScript", + 2, + ) + return "passed" + except Exception: + return CommonUtil.Exception_Handler(sys.exc_info()) + except PlaywrightError as e: + err_msg = str(e).lower() + # Stale element: retry up to 5 times with 1s delay + if ("stale" in err_msg or "detached" in err_msg) and retry < 5: + CommonUtil.ExecLog( + sModuleInfo, + "Javascript of the element is not fully loaded. Trying again after 1 second delay", + 2, + ) + await asyncio.sleep(1) + return await Click_Element(step_data, retry + 1) + # Try JS click fallback + try: + await locator.evaluate("el => el.click()") + CommonUtil.ExecLog( + sModuleInfo, + "Click failed natively; clicked successfully via JavaScript", + 2, + ) + return "passed" + except Exception: + return CommonUtil.Exception_Handler(sys.exc_info()) except Exception: return CommonUtil.Exception_Handler(sys.exc_info()) @@ -1044,7 +1324,7 @@ async def Enter_Text_In_Text_Box(step_data): text action my_username text playwright action text - Example 2 - With options: + Example 2 - With options (Selenium-compatible): Field Sub Field Value id element parameter username text action my_username @@ -1059,7 +1339,6 @@ async def Enter_Text_In_Text_Box(step_data): try: # Handle session parameter session_name, current_page, current_page_id, context, browser = await _handle_playwright_session(step_data) - if current_page is None: CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) return "zeuz_failed" @@ -1092,38 +1371,78 @@ async def Enter_Text_In_Text_Box(step_data): 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) + CommonUtil.ExecLog(sModuleInfo, "Unable to locate your element with given data.", 3) return "zeuz_failed" # Enter text based on options if use_js: - # Use JavaScript to set value directly - await locator.evaluate(f"el => {{ el.value = `{text_value}`; }}") - # Trigger events + # JS mode mirrors Selenium: click, set value, dispatch input/change events, click again. + try: + await locator.evaluate("el => el.click()") + except Exception: + CommonUtil.ExecLog(sModuleInfo, "Entering text without clicking the element", 2) + # Use JS template-literal so embedded quotes/newlines are preserved (matches Selenium). + escaped = text_value.replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${") + await locator.evaluate(f"el => {{ el.value = `{escaped}`; }}") 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 - await locator.fill(text_value, **fill_options) - CommonUtil.ExecLog(sModuleInfo, f"Text filled: {text_value[:50]}{'...' if len(text_value) > 50 else ''}", 1) + try: + await locator.evaluate("el => el.click()") + except Exception: + pass + CommonUtil.ExecLog(sModuleInfo, f"Successfully set the value of to text to: {text_value}", 1) + return "passed" + + # Non-JS path: click first to focus (best-effort), clear if requested, then type/fill. + try: + await locator.click() + except Exception: + CommonUtil.ExecLog(sModuleInfo, "Entering text without clicking the element", 2) + + if clear: + try: + # Select-all + delete pattern matches Selenium clear logic across platforms. + if sys.platform == "darwin": + await locator.press("Meta+A") + else: + await locator.press("Control+A") + await locator.press("Delete") + except Exception: + pass + try: + # fill() always clears first; also handles inputs where Select-All didn't apply. + fill_options = {} + if timeout: + fill_options["timeout"] = timeout + if delay == 0: + await locator.fill(text_value, **fill_options) + else: + # Caller wants per-keystroke delay -> type after clearing. + type_options = {"delay": int(delay * 1000)} + if timeout: + type_options["timeout"] = timeout + await locator.type(text_value, **type_options) + except Exception: + return CommonUtil.Exception_Handler(sys.exc_info()) else: - # type() appends to existing value type_options = {} if delay > 0: type_options["delay"] = int(delay * 1000) if timeout: type_options["timeout"] = timeout await locator.type(text_value, **type_options) - CommonUtil.ExecLog(sModuleInfo, f"Text typed: {text_value[:50]}{'...' if len(text_value) > 50 else ''}", 1) + # Some text fields become unclickable after entering text - best-effort click. + try: + await locator.click() + except Exception: + pass + + CommonUtil.ExecLog(sModuleInfo, f"Successfully set the value of to text to: {text_value}", 1) return "passed" except Exception: - return CommonUtil.Exception_Handler(sys.exc_info()) + return CommonUtil.Exception_Handler(sys.exc_info(), None, "Could not select/click your element.") @logger @@ -1373,14 +1692,22 @@ async def Validate_Text(step_data): @logger async def if_element_exists(step_data): """ - Check if an element exists on the page. + Check whether an element exists (true/false). - Example: + Selenium-compatible form (writes the result to a shared variable, always returns "passed"): + Field Sub Field Value + id element parameter optional-element + if element exists playwright action true=my_flag + + - If found: shared variable my_flag is set to "true" + - If not found: shared variable my_flag is set to "false" + + Plain form (no save): Field Sub Field Value id element parameter optional-element if element exists playwright action if element exists - Returns "passed" if element exists, "zeuz_failed" if not. + - Returns "passed" if found, "zeuz_failed" if not. """ sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME global current_page @@ -1390,35 +1717,59 @@ async def if_element_exists(step_data): CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) return "zeuz_failed" + variable_name = "" + value = "" timeout = 1000 # Short timeout for existence check + 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 == "timeout": + if "action" in mid_l and "=" in right_v: + try: + value_part, var_part = right_v.split("=", 1) + value = value_part.strip() + variable_name = var_part.strip() + except ValueError: + pass + elif 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, frame_locator=_get_frame_locator()) + locator = await PlaywrightLocator.Get_Element( + step_data, + current_page, + element_wait=timeout / 1000, + frame_locator=_get_frame_locator(), + ) - if locator == "zeuz_failed": - CommonUtil.ExecLog(sModuleInfo, "Element does not exist", 1) - return "zeuz_failed" + found = False + if locator != "zeuz_failed": + try: + if await locator.count() > 0: + found = True + except Exception: + found = False - try: - count = await locator.count() - if count > 0: - CommonUtil.ExecLog(sModuleInfo, f"Element exists ({count} found)", 1) - return "passed" - else: - CommonUtil.ExecLog(sModuleInfo, "Element does not exist", 1) - return "zeuz_failed" - except Exception: - CommonUtil.ExecLog(sModuleInfo, "Element does not exist", 1) - return "zeuz_failed" + if variable_name: + # Selenium-compatible: always returns "passed"; the truthiness lives in the variable. + sr.Set_Shared_Variables(variable_name, value if found else "false") + CommonUtil.ExecLog( + sModuleInfo, + f"Element {'found' if found else 'not found'} - saved '{value if found else 'false'}' to '{variable_name}'", + 1, + ) + return "passed" + + if found: + CommonUtil.ExecLog(sModuleInfo, "Element exists", 1) + return "passed" + CommonUtil.ExecLog(sModuleInfo, "Element does not exist", 1) + return "zeuz_failed" except Exception: - return CommonUtil.Exception_Handler(sys.exc_info()) + errMsg = "Failed to parse data/locate element. Data format: variableName = value" + return CommonUtil.Exception_Handler(sys.exc_info(), None, errMsg) @logger @@ -1426,7 +1777,13 @@ async def Save_Attribute(step_data): """ Save an element's attribute value to a shared variable. - Example: + Selenium-compatible form (recommended): + Field Sub Field Value + id element parameter my-link + href save parameter my_variable + save attribute playwright action save attribute + + Alternative form (attribute via input parameter): Field Sub Field Value id element parameter my-link href input parameter attribute_name @@ -1434,13 +1791,15 @@ async def Save_Attribute(step_data): save attribute playwright action save attribute Special attribute names: - - text: Get text content - - innertext: Get inner text - - innerhtml: Get inner HTML - - outerhtml: Get outer HTML - - value: Get input value - - checked: Get checkbox state - - selected: Get select option state + - text: text content (Selenium .text) + - tag: tag name (Selenium .tag_name) + - checked: checkbox/radio selected state + - innertext: inner text + - innerhtml: inner HTML + - outerhtml: outer HTML + - value: input value + - selected: