From e553850009e6a56ea4f43c2ffef46ca7531a3038 Mon Sep 17 00:00:00 2001 From: Nasif Date: Sun, 24 May 2026 17:14:12 +0600 Subject: [PATCH 1/6] Match `"action"` column parsing between Selenium and Playwright actions --- .../Web/Playwright/BuiltInFunctions.py | 24 +++++++++---------- .../Web/Selenium/BuiltInFunctions.py | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 6652809f..679ea783 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -885,7 +885,7 @@ async def Click_Element(step_data): elif left_l == "timeout": timeout = int(float(right_v) * 1000) - elif mid_l == "action": + elif "action" in mid_l: if "double" in left_l: double_click = True elif "right" in left_l: @@ -944,7 +944,7 @@ async def Double_Click_Element(step_data): modified_step_data = list(step_data) # Ensure the action indicates double click for i, (left, mid, right) in enumerate(modified_step_data): - if mid.strip().lower() == "action": + if "action" in mid.strip().lower(): modified_step_data[i] = ("double click", mid, right) break @@ -963,7 +963,7 @@ async def Right_Click_Element(step_data): """ modified_step_data = list(step_data) for i, (left, mid, right) in enumerate(modified_step_data): - if mid.strip().lower() == "action": + if "action" in mid.strip().lower(): modified_step_data[i] = ("right click", mid, right) break @@ -1078,7 +1078,7 @@ async def Enter_Text_In_Text_Box(step_data): if mid_l == "optional parameter" and left_l == "session": continue - if mid_l == "action": + if "action" in mid_l: text_value = right # Don't strip - preserve whitespace elif mid_l == "optional parameter": if left_l == "delay": @@ -1171,7 +1171,7 @@ async def Keystroke_For_Element(step_data): mid_l = mid.strip().lower() right_v = right.strip() - if mid_l == "action": + if "action" in mid_l: if left_l == "keystroke keys": keystroke_type = "keys" keystroke_value = right_v @@ -1320,7 +1320,7 @@ async def Validate_Text(step_data): mid_l = mid.strip().lower() right_v = right.strip() - if mid_l == "action": + if "action" in mid_l: if left_l.startswith("**"): partial_match = True case_insensitive = True @@ -1606,7 +1606,7 @@ async def Navigate(step_data): mid_l = mid.strip().lower() right_v = right.strip().lower() - if mid_l == "action": + if "action" in mid_l: direction = right_v elif mid_l == "optional parameter": if left_l == "timeout": @@ -1830,7 +1830,7 @@ async def Select_Deselect(step_data): mid_l = mid.strip().lower() right_v = right.strip() - if mid_l == "action": + if "action" in mid_l: if "deselect" in left_l: is_deselect = True @@ -1916,7 +1916,7 @@ async def check_uncheck(step_data): mid_l = mid.strip().lower() right_v = right.strip().lower() - if mid_l == "action": + if "action" in mid_l: if "uncheck" in left_l or "uncheck" in right_v: action = "uncheck" else: @@ -2338,7 +2338,7 @@ async def Handle_Browser_Alert(step_data): mid_l = mid.strip().lower() right_v = right.strip() - if mid_l == "action": + if "action" in mid_l: action = right_v.lower() elif mid_l == "input parameter": if left_l in ("prompt text", "text", "send text"): @@ -2575,7 +2575,7 @@ async def execute_javascript(step_data): left_l = left.strip().lower() mid_l = mid.strip().lower() - if mid_l == "action": + if "action" in mid_l: js_code = right elif mid_l == "element parameter": has_element = True @@ -2938,7 +2938,7 @@ async def Intercept_Network(step_data): response_body = right_v elif left_l == "response status": response_status = int(right_v) - elif mid_l == "action": + elif "action" in mid_l: action = right_v.lower() async def handle_route(route): diff --git a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py index 17c82249..cc64d4e9 100644 --- a/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Selenium/BuiltInFunctions.py @@ -1856,7 +1856,7 @@ def Enter_Text_In_Text_Box(step_data): # Skip session parameter - already handled above if left == "session" and mid == "optional parameter": continue - if mid == "action": + if "action" in mid: text_value = right elif left == "delay": delay = float(right.strip()) From 92ecaf0fee61782715edb3708752e289d4fe31d2 Mon Sep 17 00:00:00 2001 From: Nasif Date: Sun, 24 May 2026 17:21:11 +0600 Subject: [PATCH 2/6] Add missing element locator logic to Playwright `locator.py` - Matches Selenium's `Get_Element` behaviour --- .../Web/Playwright/locator.py | 1068 ++++++++++------- tests/test_playwright_locator.py | 249 ++++ 2 files changed, 912 insertions(+), 405 deletions(-) create mode 100644 tests/test_playwright_locator.py diff --git a/Framework/Built_In_Automation/Web/Playwright/locator.py b/Framework/Built_In_Automation/Web/Playwright/locator.py index 9acbe5b8..9fa4c4d7 100644 --- a/Framework/Built_In_Automation/Web/Playwright/locator.py +++ b/Framework/Built_In_Automation/Web/Playwright/locator.py @@ -2,516 +2,774 @@ """ Playwright Element Locator Module -This module provides element location functionality using Playwright's native -Locator API while reusing the query building logic from LocateElement.py. - -Key Features: -- Native Playwright Locator API (lazy evaluation, auto-wait) -- Supports all existing element parameter formats -- Supports Playwright-native selectors (test-id, role, text, etc.) -- Preserves Playwright's speed advantage +This module resolves the standard Zeuz element step-data format to Playwright +Locator objects. Selenium compatibility is the primary contract: Selenium-style +element parameters are converted through LocateElement.py's query builder, then +executed with Playwright's Locator API. """ -import sys import inspect import re +import sys +from dataclasses import dataclass +from typing import Any -from Framework.Utilities import CommonUtil from Framework.Built_In_Automation.Shared_Resources import ( BuiltInFunctionSharedResources as sr, ) -from Framework.Utilities.CommonUtil import passed_tag_list, failed_tag_list +from Framework.Utilities import CommonUtil +from Framework.Utilities.CommonUtil import failed_tag_list MODULE_NAME = inspect.getmodulename(__file__) -async def Get_Element(step_data, page, return_all=False, element_wait=None, frame_locator=None): +@dataclass +class LocatorBuildResult: + locator: Any + query_type: str + query: Any + + +async def Get_Element(step_data, page, return_all=False, element_wait=None, frame_locator=None, return_all_elements=False): """ - Get element using Playwright's native Locator API. - - This function parses the same step_data format as LocateElement.Get_Element() - but uses Playwright's Locator API for execution, preserving auto-wait and - lazy evaluation benefits. - - Args: - step_data: List of (left, mid, right) tuples - standard Zeuz format - 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" - - Example: - step_data = [ - ("id", "element parameter", "submit-btn"), - ("click", "playwright action", "click"), - ] - locator = Get_Element(step_data, page) - locator.click() # Auto-waits for element + Resolve Zeuz step-data to a Playwright Locator. + + The returned object intentionally mirrors Selenium LocateElement.Get_Element() + semantics where possible: visible elements are preferred by default, multiple + matches return the first match unless an index is provided, save/get parameter + rows use shared variables, and failures return "zeuz_failed". """ + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME try: - # Parse all parameters from step_data + if return_all_elements: + return_all = True + params = _parse_element_params(step_data) + if params.get("parse_error"): + CommonUtil.ExecLog(sModuleInfo, params["parse_error"], 3) + return "zeuz_failed" - # Check for get parameter (retrieve saved element) - if params.get('get_parameter'): - result = sr.parse_variable(params['get_parameter']) + if params.get("get_parameter"): + result = sr.parse_variable(params["get_parameter"]) + result = CommonUtil.ZeuZ_map_code_decoder(result) if result not in failed_tag_list: CommonUtil.ExecLog( sModuleInfo, - f"Returning saved element '{params['get_parameter']}' from shared variables", + "Returning saved element '%s' from shared variables" % params["get_parameter"], 1, ) return result - else: - CommonUtil.ExecLog( - sModuleInfo, - f"Element '{params['get_parameter']}' not found in shared variables", - 3, - ) - return "zeuz_failed" - # Build the locator - locator = _build_locator(page, step_data, params, frame_locator) + CommonUtil.ExecLog( + sModuleInfo, + "Element named '%s' not found in shared variables" % params["get_parameter"], + 3, + ) + return "zeuz_failed" - if locator is None: + timeout = _resolve_timeout(params, element_wait) + build_result = _build_locator(page, step_data, params, frame_locator) + if build_result is None or build_result.locator is None: CommonUtil.ExecLog(sModuleInfo, "Could not build locator from step data", 3) return "zeuz_failed" - # Set timeout if specified - if element_wait is not None: - timeout = int(float(element_wait) * 1000) - elif params.get('wait') is not None: - timeout = int(float(params['wait']) * 1000) - else: - # Get default from shared variables - default_wait = sr.Get_Shared_Variables("element_wait") - if default_wait not in failed_tag_list: - timeout = int(float(default_wait) * 1000) - else: - timeout = 10000 # Default 10 seconds - - # Apply visibility filter if not allowing hidden - if not params.get('allow_hidden'): - # Filter to visible elements only - locator = locator.locator("visible=true") - - # Apply index if specified - if params.get('index') is not None: - index = params['index'] - locator = locator.nth(index) - - # Log the locator being used - CommonUtil.ExecLog(sModuleInfo, f"Playwright locator: {locator}", 5) - - # Save if requested - if params.get('save_parameter'): - sr.Set_Shared_Variables(params['save_parameter'], locator) - CommonUtil.ExecLog( - sModuleInfo, - f"Saved element to variable '{params['save_parameter']}'", - 1, - ) + CommonUtil.ExecLog( + sModuleInfo, + f"To locate the Element we used {build_result.query_type}:\n{build_result.query}", + 5, + ) - # Return all elements if requested if return_all: - try: - elements = await locator.all() - CommonUtil.ExecLog(sModuleInfo, f"Found {len(elements)} elements", 1) - return elements - except Exception as e: - CommonUtil.ExecLog(sModuleInfo, f"Error getting all elements: {e}", 3) - return "zeuz_failed" - - # Check if element exists (with timeout) - try: - count = await locator.count() - if count == 0: - CommonUtil.ExecLog(sModuleInfo, "No elements found matching locator", 3) - return "zeuz_failed" - elif count > 1 and params.get('index') is None: + result = await _resolve_all(build_result.locator, params, timeout, sModuleInfo) + if not result and params.get("text_filter"): + result = await _text_filter(step_data, page, frame_locator, params, timeout, return_all) + else: + result = await _resolve_single(build_result.locator, params, timeout, sModuleInfo) + if result == "zeuz_failed" and params.get("text_filter"): + result = await _text_filter(step_data, page, frame_locator, params, timeout, return_all) + + if result not in failed_tag_list: + if not return_all: + await _log_outer_html(result, sModuleInfo) + if params.get("save_parameter"): + sr.Set_Shared_Variables(params["save_parameter"], result) CommonUtil.ExecLog( sModuleInfo, - f"Found {count} elements. Returning first. Consider using index parameter.", - 2, + "Saved element to variable '%s'" % params["save_parameter"], + 1, ) - except Exception: - pass # Count might fail, but locator might still work with auto-wait + sr.Set_Shared_Variables("zeuz_element", result) + return result - return locator + await _log_frame_hint(page, sModuleInfo) + return "zeuz_failed" except Exception: return CommonUtil.Exception_Handler(sys.exc_info()) def _parse_element_params(step_data): - """ - Parse element parameters from step data. - - Returns dict with: - - index: Element index (int or None) - - allow_hidden: Whether to include hidden elements (bool) - - save_parameter: Variable name to save element (str or None) - - get_parameter: Variable name to retrieve element (str or None) - - wait: Custom wait timeout in seconds (float or None) - - element_params: List of element parameter tuples - - parent_params: List of parent parameter tuples - - And other parameter lists... - """ + """Parse Zeuz step-data without losing Selenium locator semantics.""" + params = { - 'index': None, - 'allow_hidden': False, - 'save_parameter': None, - 'get_parameter': None, - 'wait': None, - 'element_params': [], - 'parent_params': [], - 'child_params': [], - 'sibling_params': [], - 'unique_params': [], - 'shadow_root_params': [], + "index": None, + "allow_hidden": False, + "allow_disabled": False, + "save_parameter": None, + "get_parameter": None, + "wait": None, + "text_filter": False, + "parse_error": None, + "element_params": [], + "parent_params": [], + "child_params": [], + "sibling_params": [], + "preceding_params": [], + "following_params": [], + "unique_params": [], + "shadow_root_params": [], + "locator_rows": [], + "element_ds": [], + "all_rows": [], } for left, mid, right in step_data: - left_lower = left.strip().lower() - mid_lower = mid.strip().lower() - right_stripped = right.strip() + left_raw = str(left).strip() + mid_raw = str(mid).strip() + right_raw = str(right) + right_stripped = right_raw.strip() + left_lower = left_raw.lower() + mid_lower = mid_raw.lower() + mid_key = _mid_key(mid_raw) + row = (left_raw, mid_raw, right_raw) + params["all_rows"].append(row) - # Save parameter if mid_lower == "save parameter": if right_stripped != "ignore": - params['save_parameter'] = left.strip() + params["save_parameter"] = left_raw + continue - # Get parameter - elif mid_lower == "get parameter": + if mid_lower == "get parameter": if right_stripped.startswith("%|") and right_stripped.endswith("|%"): - params['get_parameter'] = right_stripped.strip("%").strip("|") - - # Optional parameters - elif mid_lower == "optional parameter": - if left_lower in ("allow hidden", "allow disable"): - params['allow_hidden'] = right_stripped.lower() in ("yes", "true", "ok", "1") + params["get_parameter"] = right_stripped.strip("%").strip("|") + else: + params["parse_error"] = "Use '%| |%' sign at right column to get variable value" + continue + + if mid_lower == "optional parameter": + if left_lower == "allow hidden": + params["allow_hidden"] = _truthy(right_stripped) + elif left_lower == "allow disable": + params["allow_disabled"] = _truthy(right_stripped) elif left_lower == "wait": - params['wait'] = float(right_stripped) - - # Element parameters - elif mid_lower == "element parameter" or "element parameter" in mid_lower: + try: + params["wait"] = float(right_stripped) + except Exception: + params["parse_error"] = "Wait optional parameter must be numeric" + elif left_lower == "text filter": + params["text_filter"] = _truthy(right_stripped) + continue + + if mid_lower.startswith("sr"): + params["shadow_root_params"].append(row) + continue + + if mid_key == "uniqueparameter": + params["unique_params"].append((left_raw, right_raw)) + params["locator_rows"].append(row) + params["element_ds"].append(row) + continue + + if "parent" in mid_key and "parameter" in mid_key: + params["parent_params"].append(row) + params["locator_rows"].append(row) + params["element_ds"].append(row) + continue + + if "child" in mid_key and "parameter" in mid_key: + params["child_params"].append(row) + params["locator_rows"].append(row) + params["element_ds"].append(row) + continue + + if "sibling" in mid_key and "parameter" in mid_key: + params["sibling_params"].append(row) + params["locator_rows"].append(row) + params["element_ds"].append(row) + continue + + if "preceding" in mid_key and "parameter" in mid_key: + params["preceding_params"].append(row) + params["locator_rows"].append(row) + params["element_ds"].append(row) + continue + + if "following" in mid_key and "parameter" in mid_key: + params["following_params"].append(row) + params["locator_rows"].append(row) + params["element_ds"].append(row) + continue + + if mid_key == "elementparameter": + params["locator_rows"].append(row) + params["element_ds"].append(row) if left_lower == "index": try: - params['index'] = int(right_stripped) - except ValueError: - pass + params["index"] = int(right_stripped) + except Exception: + params["parse_error"] = "Index = 0 is set" else: - params['element_params'].append((left.strip(), right_stripped)) + params["element_params"].append((left_raw, right_raw)) + continue - # Unique parameter - elif mid_lower == "unique parameter": - params['unique_params'].append((left.strip(), right_stripped)) + params["element_ds"].append(row) - # Parent parameters (including numbered: parent 2 parameter) - elif "parent" in mid_lower and "parameter" in mid_lower: - params['parent_params'].append((left.strip(), mid.strip(), right_stripped)) + return params - # Child parameters - elif "child" in mid_lower and "parameter" in mid_lower: - params['child_params'].append((left.strip(), mid.strip(), right_stripped)) - # Sibling parameters - elif "sibling" in mid_lower and "parameter" in mid_lower: - params['sibling_params'].append((left.strip(), mid.strip(), right_stripped)) +def _build_locator(page, step_data, params, frame_locator=None): + base_locator = frame_locator if frame_locator else page - # Shadow root parameters - elif mid_lower.startswith("sr"): - params['shadow_root_params'].append((left.strip(), mid.strip(), right_stripped)) + if params["shadow_root_params"]: + return _build_shadow_dom_locator(base_locator, params) - return params + native_locator = _build_native_locator(base_locator, params) + if native_locator is not None: + return native_locator + if params["unique_params"]: + legacy_locator = _build_legacy_locator(base_locator, params) + if legacy_locator is not None: + return legacy_locator -def _build_locator(page, step_data, params, frame_locator=None): - """ - Build a Playwright Locator from step data. - - Attempts these strategies in order: - 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 - """ + raw_locator = _build_raw_locator(base_locator, step_data) + if raw_locator is not None: + return raw_locator - # Use frame locator if provided, otherwise use page - base_locator = frame_locator if frame_locator else page + legacy_locator = _build_legacy_locator(base_locator, params) + if legacy_locator is not None: + return legacy_locator - # 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 base_locator.get_by_test_id(right) - - # Role selector - if left_lower == "role": - # Check if there's a name parameter too - name = None - for l, r in params['element_params']: - if l.lower() in ("name", "role name", "aria-label"): - name = r - break - if name: - return base_locator.get_by_role(right, name=name) - return base_locator.get_by_role(right) - - # Text selectors - if left_lower == "text": - return base_locator.get_by_text(right, exact=True) - if left_lower == "*text": - return base_locator.get_by_text(right, exact=False) - if left_lower == "**text": - # Case-insensitive partial match - return base_locator.get_by_text(re.compile(re.escape(right), re.IGNORECASE)) - - # Label selector - if left_lower == "label": - return base_locator.get_by_label(right) - - # Placeholder selector - if left_lower == "placeholder": - return base_locator.get_by_placeholder(right) - - # Alt text selector - if left_lower in ("alt", "alt text", "alt-text"): - return base_locator.get_by_alt_text(right) - - # Title selector - if left_lower == "title" and "parameter" not in params.get('mid', ''): - return base_locator.get_by_title(right) - - # Direct xpath + return None + + +def _build_native_locator(base_locator, params): + """Support explicit Playwright-only selector aliases without overriding Selenium semantics.""" + + if _has_relationship_params(params) or params["unique_params"] or len(params["element_params"]) != 1: + return None + + left, right = params["element_params"][0] + left_lower = left.lower() + if left_lower in ("test-id", "testid"): + return LocatorBuildResult(base_locator.get_by_test_id(right), "playwright test-id", right) + return None + + +def _build_raw_locator(base_locator, step_data): + xpath_rows = [] + css_rows = [] + for left, mid, right in step_data: + left_lower = str(left).strip().lower() + mid_key = _mid_key(str(mid)) + if mid_key != "elementparameter": + continue if left_lower == "xpath": - return base_locator.locator(f"xpath={right}") - - # Direct CSS selector - if left_lower in ("css", "css selector", "css_selector"): - return base_locator.locator(right) - - # Strategy 2: Check for unique parameters and element parameters - for left, right in params['unique_params'] + params['element_params']: - left_lower = left.lower() - - if left_lower == "id": - return base_locator.locator(f"#{right}") - elif left_lower == "name": - return base_locator.locator(f"[name='{right}']") - elif left_lower == "class": - return base_locator.locator(f".{right}") - elif left_lower == "tag": - return base_locator.locator(right) - - # Strategy 3: Build xpath from element/parent/child parameters - xpath = _build_xpath_from_params(step_data, params) - if xpath: - CommonUtil.ExecLog( - "_build_locator", - f"Built xpath from parameters: {xpath}", - 5 - ) - return base_locator.locator(f"xpath={xpath}") - - # Strategy 4: Simple element parameters as xpath - if params['element_params']: - xpath_parts = [] - tag = "*" - - for left, right in params['element_params']: - left_lower = left.lower() - - if left_lower == "tag": - tag = right - elif left_lower == "id": - xpath_parts.append(f"@id='{right}'") - elif left_lower == "name": - xpath_parts.append(f"@name='{right}'") - elif left_lower == "class": - xpath_parts.append(f"contains(@class,'{right}')") - elif left_lower.startswith("*"): - # Partial match - attr = left_lower[1:] - xpath_parts.append(f"contains(@{attr},'{right}')") - elif left_lower.startswith("**"): - # Case-insensitive partial match - attr = left_lower[2:] - xpath_parts.append( - f"contains(translate(@{attr},'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'{right.lower()}')" - ) - elif left_lower not in ("index", "text", "*text", "**text"): - # Generic attribute - xpath_parts.append(f"@{left}='{right}'") + xpath_rows.append(str(right).strip()) + elif left_lower in ("css", "css selector", "css_selector"): + css_rows.append(str(right).strip()) + + if xpath_rows and not css_rows: + query = xpath_rows[0] + return LocatorBuildResult(base_locator.locator(_as_playwright_xpath(query)), "xpath", query) + if css_rows and not xpath_rows: + query = css_rows[0] + return LocatorBuildResult(base_locator.locator(query), "css", query) + return None + - if xpath_parts: - xpath = f"//{tag}[{' and '.join(xpath_parts)}]" - return base_locator.locator(f"xpath={xpath}") - elif tag != "*": - return base_locator.locator(tag) +def _build_legacy_locator(base_locator, params): + query, query_type = _build_legacy_query(params["locator_rows"]) + if not query or not query_type: + return None + if query_type == "unique": + locator = _build_unique_locator(base_locator, query[0], query[1]) + return LocatorBuildResult(locator, query_type, query) + if query_type == "css": + return LocatorBuildResult(base_locator.locator(query), query_type, query) + if query_type == "xpath": + return LocatorBuildResult(base_locator.locator(_as_playwright_xpath(query)), query_type, query) return None -def _build_xpath_from_params(step_data, params): - """ - Build complex xpath from element/parent/child/sibling parameters. +def _build_legacy_query(locator_rows): + if not locator_rows: + return None, None - This reuses the logic from LocateElement._construct_query() but simplified - for the most common cases. - """ try: - # Import the existing query builder for complex cases from Framework.Built_In_Automation.Shared_Resources import LocateElement - # Filter step_data to only include element-related rows - element_rows = [] - for left, mid, right in step_data: - mid_lower = mid.strip().lower().replace(" ", "") - if any(x in mid_lower for x in [ - "elementparameter", "parentparameter", "childparameter", - "siblingparameter", "uniqueparameter", "precedingparameter", - "followingparameter" - ]): - element_rows.append((left, mid, right)) - - if not element_rows: - return None - - # Use existing query builder - # Temporarily set driver_type to selenium for xpath generation - original_driver_type = getattr(LocateElement, 'driver_type', None) + original_driver_type = getattr(LocateElement, "driver_type", None) LocateElement.driver_type = "selenium" - try: - xpath, query_type = LocateElement._construct_query(element_rows) - if xpath and query_type in ("xpath", "css"): - return xpath + return LocateElement._construct_query(locator_rows) finally: - if original_driver_type is not None: - LocateElement.driver_type = original_driver_type - + LocateElement.driver_type = original_driver_type except Exception as e: + CommonUtil.ExecLog("_build_legacy_query", f"Error building Selenium-compatible query: {e}", 2) + return None, None + + +def _build_unique_locator(base_locator, unique_key, unique_value): + key = str(unique_key).strip().lower() + value = str(unique_value).strip() + + if key in ("accessibility id", "accessibility-id", "content-desc", "content desc"): + return base_locator.locator(_as_playwright_xpath(f"//*[@aria-label={_xpath_literal(value)}]")) + if key == "id": + return base_locator.locator(_as_playwright_xpath(f"//*[@id={_xpath_literal(value)}]")) + if key == "name": + return base_locator.locator(_as_playwright_xpath(f"//*[@name={_xpath_literal(value)}]")) + if key == "class": + klass = _xpath_literal(f" {value} ") + return base_locator.locator( + _as_playwright_xpath(f"//*[contains(concat(' ', normalize-space(@class), ' '), {klass})]") + ) + if key == "tag": + return base_locator.locator(_as_playwright_xpath(f"//{value}")) + if key == "css": + return base_locator.locator(value) + if key == "xpath": + return base_locator.locator(_as_playwright_xpath(value)) + if key == "text": + return base_locator.locator(_as_playwright_xpath(f"//*[text()={_xpath_literal(value)}]")) + if key == "*text": + return base_locator.locator(_as_playwright_xpath(f"//*[contains(text(),{_xpath_literal(value)})]")) + if key.startswith("**"): + attr = key[2:] + return base_locator.locator( + _as_playwright_xpath( + "//*[contains(translate(@%s,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),%s)]" + % (attr, _xpath_literal(value.lower())) + ) + ) + if key.startswith("*"): + attr = key[1:] + return base_locator.locator(_as_playwright_xpath(f"//*[contains(@{attr},{_xpath_literal(value)})]")) + return base_locator.locator(_as_playwright_xpath(f"//*[@{key}={_xpath_literal(value)}]")) + + +def _build_shadow_dom_locator(base_locator, params): + try: + from Framework.Built_In_Automation.Shared_Resources import LocateElement + + shadow_root_params = [] + parent_params = [] + + for left, mid, right in params["shadow_root_params"]: + left_lower = left.strip().lower() + if "text" in left_lower: + CommonUtil.ExecLog( + "_build_shadow_dom_locator", + "Shadow DOM access requires attribute/tag/css selectors; text selectors are not supported", + 3, + ) + return None + + words = mid.strip().lower().split() + if len(words) < 3 or len(words) > 4: + CommonUtil.ExecLog("_build_shadow_dom_locator", f"Invalid shadow root parameter format: {mid}", 3) + return None + idx = int(words[1]) if len(words) == 4 else 1 + param = " ".join(words[-2:]) + normalized_row = [left, param, right] + + if "parent" in param: + parent_params.append((idx, normalized_row)) + elif "element" in param: + shadow_root_params.append((idx, normalized_row)) + else: + CommonUtil.ExecLog( + "_build_shadow_dom_locator", + "Only shadow root parent parameter and element parameter rows are supported", + 3, + ) + return None + + parent_indices = [idx for idx, _ in parent_params] + shadow_indices = [idx for idx, _ in shadow_root_params] + if len(parent_indices) != len(set(parent_indices)) or len(shadow_indices) != len(set(shadow_indices)): + CommonUtil.ExecLog("_build_shadow_dom_locator", "Duplicate shadow root indices found", 3) + return None + + parent_params.sort(key=lambda item: item[0]) + shadow_root_params.sort(key=lambda item: item[0]) + current = base_locator + query_parts = [] + + for idx, shadow_param in shadow_root_params: + query_rows = [] + for parent_idx, parent_param in parent_params: + if parent_idx == idx: + query_rows.append(parent_param) + break + query_rows.append(shadow_param) + host_query = LocateElement.build_css_selector_query(query_rows) + if not host_query: + return None + current = current.locator(host_query) + host_index = _index_from_rows([shadow_param]) or 0 + current = current.nth(host_index) + query_parts.append(host_query) + + element_query = LocateElement.build_css_selector_query([list(row) for row in params["element_ds"]]) + if not element_query: + return None + query_parts.append(element_query) + return LocatorBuildResult(current.locator(element_query), "shadow css", " >> ".join(query_parts)) + except Exception: + return CommonUtil.Exception_Handler(sys.exc_info()) + + +async def _resolve_single(locator, params, timeout, sModuleInfo): + effective_locator = _effective_locator(locator, params) + state = "attached" if params.get("allow_hidden") else "visible" + + if not await _wait_for_locator(effective_locator, state, timeout): + await _log_no_match(locator, params, sModuleInfo) + return "zeuz_failed" + + count = await _safe_count(effective_locator) + total_count = await _safe_count(locator) + hidden_count = max(total_count - count, 0) if not params.get("allow_hidden") else 0 + + index = params.get("index") + if count == 0: + await _log_no_match(locator, params, sModuleInfo) + return "zeuz_failed" + + if index is None: + if count > 1: + CommonUtil.ExecLog( + sModuleInfo, + f"Found {count} displayed elements. Returning the first displayed element only. Consider providing index", + 2, + ) + elif hidden_count > 0: + CommonUtil.ExecLog( + sModuleInfo, + f"Found {hidden_count} hidden elements and {count} displayed element. Returning the displayed element only", + 2, + ) + return _first_locator(effective_locator) + + if count == 1: + if index not in (-1, 0): + CommonUtil.ExecLog( + sModuleInfo, + f"Found {count} element but provided index {index} is out of range. Returning the only element", + 2, + ) + return _first_locator(effective_locator) + + resolved_index = index if index >= 0 else count + index + if 0 <= resolved_index < count: CommonUtil.ExecLog( - "_build_xpath_from_params", - f"Error building xpath: {e}", - 2 + sModuleInfo, + f"Found {count} elements. Returning the element of index {index}", + 1, ) + return effective_locator.nth(resolved_index) - return None + CommonUtil.ExecLog(sModuleInfo, f"Found {count} elements. Index {index} exceeds the number of elements found", 3) + return "zeuz_failed" -def handle_shadow_dom(page, shadow_params, element_params): - """ - Handle Shadow DOM element location. +async def _resolve_all(locator, params, timeout, sModuleInfo): + effective_locator = _effective_locator(locator, params) + state = "attached" if params.get("allow_hidden") else "visible" + if not await _wait_for_locator(effective_locator, state, timeout): + CommonUtil.ExecLog(sModuleInfo, "Found 0 elements", 3) + return [] - Playwright supports automatic shadow DOM piercing with the >> selector. + all_locators = await effective_locator.all() + total_count = await _safe_count(locator) + displayed_count = len(all_locators) + hidden_count = max(total_count - displayed_count, 0) if not params.get("allow_hidden") else 0 - Args: - page: Playwright Page object - shadow_params: List of shadow root parameters - element_params: Final element parameters + if params.get("allow_hidden"): + CommonUtil.ExecLog(sModuleInfo, f"Found {displayed_count} elements. Returning all of them", 1) + else: + CommonUtil.ExecLog( + sModuleInfo, + f"Found {hidden_count} hidden elements and {displayed_count} displayed elements. Returning displayed elements only", + 1, + ) + return all_locators - Returns: - Locator for element inside shadow DOM - """ - sModuleInfo = "handle_shadow_dom" - try: - # Build a chain of selectors using Playwright's shadow-piercing >> - # Sort shadow params by their index (sr 1, sr 2, etc.) - sorted_params = sorted(shadow_params, key=lambda x: _extract_sr_index(x[1])) - - selector_parts = [] - - for left, mid, right in sorted_params: - left_lower = left.lower() - if left_lower == "tag": - selector_parts.append(right) - elif left_lower == "id": - selector_parts.append(f"#{right}") - elif left_lower == "class": - selector_parts.append(f".{right}") - else: - selector_parts.append(f"[{left}='{right}']") - - # Add final element - for left, right in element_params: - left_lower = left.lower() - if left_lower == "tag": - selector_parts.append(right) - elif left_lower == "id": - selector_parts.append(f"#{right}") - elif left_lower == "class": - selector_parts.append(f".{right}") - else: - selector_parts.append(f"[{left}='{right}']") +async def _text_filter(step_data, page, frame_locator, original_params, timeout, return_all): + sModuleInfo = "text_filter : " + MODULE_NAME - # Join with >> for shadow DOM piercing - full_selector = " >> ".join(selector_parts) - CommonUtil.ExecLog(sModuleInfo, f"Shadow DOM selector: {full_selector}", 5) + if original_params["sibling_params"]: + return "zeuz_failed" - return page.locator(full_selector) + filters = [] + temp_step_data = [] + for left, mid, right in step_data: + left_lower = str(left).strip().lower() + mid_lower = str(mid).strip().lower() + if left_lower.replace("*", "") == "text" and mid_lower == "element parameter": + filters.append((left_lower, str(right))) + else: + temp_step_data.append((left, mid, right)) - except Exception: - return CommonUtil.Exception_Handler(sys.exc_info()) + if not filters: + return "zeuz_failed" + temp_params = _parse_element_params(temp_step_data) + temp_params["allow_hidden"] = original_params.get("allow_hidden", False) + build_result = _build_locator(page, temp_step_data, temp_params, frame_locator) + if build_result is None: + return "zeuz_failed" -def _extract_sr_index(mid_value): - """Extract index from 'sr N element parameter' format.""" - try: - parts = mid_value.lower().split() - for i, part in enumerate(parts): - if part == "sr" and i + 1 < len(parts): - return int(parts[i + 1]) - except (ValueError, IndexError): - pass - return 1 + CommonUtil.ExecLog(sModuleInfo, "No Element found. Now we are trying to handle   and ", 1) + CommonUtil.ExecLog(sModuleInfo, f"To locate the Element we used {build_result.query_type}:\n{build_result.query}", 5) + candidate_locator = _effective_locator(build_result.locator, temp_params) + state = "attached" if temp_params.get("allow_hidden") else "visible" + if not await _wait_for_locator(candidate_locator, state, timeout): + return "zeuz_failed" -async def wait_for_element(step_data, page, state="visible", timeout=None): - """ - Wait for element to reach a specific state. + candidates = await candidate_locator.all() + matches = [] + similar_texts = [] + for candidate in candidates: + try: + text = await candidate.text_content() or "" + except Exception: + continue + if _matches_text_filters(text, filters): + matches.append(candidate) + elif _similar_text(text, filters) and text not in similar_texts: + similar_texts.append(text) + + if return_all: + CommonUtil.ExecLog(sModuleInfo, f"Returning {len(matches)} elements after applying Text Filter", 1) + return matches + + if not matches: + CommonUtil.ExecLog(sModuleInfo, "Found no element after applying Text Filter", 3) + if similar_texts: + CommonUtil.ExecLog(sModuleInfo, f"These are the similar texts found in the HTML: {str(similar_texts)[1:-1]}", 3) + return "zeuz_failed" + + index = original_params.get("index") or 0 + resolved_index = index if index >= 0 else len(matches) + index + if not 0 <= resolved_index < len(matches): + CommonUtil.ExecLog(sModuleInfo, f"Found {len(matches)} elements after applying Text Filter. Index out of range", 3) + return "zeuz_failed" - Args: - step_data: Standard step data format - page: Playwright Page object - state: One of "attached", "detached", "visible", "hidden" - timeout: Timeout in milliseconds + CommonUtil.ExecLog(sModuleInfo, f"Found {len(matches)} elements after applying Text Filter. Returning index {index}", 1) + return matches[resolved_index] + + +async def wait_for_element(step_data, page, state="visible", timeout=None): + """Wait for an element to reach a Playwright state.""" - Returns: - "passed" | "zeuz_failed" - """ sModuleInfo = "wait_for_element" try: - locator = await Get_Element(step_data, page) - if locator == "zeuz_failed": + params = _parse_element_params(step_data) + if timeout is None: + timeout = _resolve_timeout(params, None) + build_result = _build_locator(page, step_data, params) + if build_result is None: + CommonUtil.ExecLog(sModuleInfo, "Could not build locator from step data", 3) return "zeuz_failed" - if timeout is None: - default_wait = sr.Get_Shared_Variables("element_wait") - if default_wait not in failed_tag_list: - timeout = int(float(default_wait) * 1000) - else: - timeout = 10000 + locator = build_result.locator + if state == "visible" and not params.get("allow_hidden"): + locator = _effective_locator(locator, params) + if params.get("index") is not None: + locator = locator.nth(params["index"]) + else: + locator = _first_locator(locator) - locator.wait_for(state=state, timeout=timeout) + await locator.wait_for(state=state, timeout=timeout) CommonUtil.ExecLog(sModuleInfo, f"Element reached state: {state}", 1) return "passed" - except Exception as e: CommonUtil.ExecLog(sModuleInfo, f"Wait for element failed: {e}", 3) return "zeuz_failed" + + +def _effective_locator(locator, params): + if params.get("allow_hidden"): + return locator + return locator.filter(visible=True) + + +async def _wait_for_locator(locator, state, timeout): + try: + await _first_locator(locator).wait_for(state=state, timeout=timeout) + return True + except Exception: + return False + + +async def _safe_count(locator): + try: + return await locator.count() + except Exception: + return 0 + + +async def _log_no_match(locator, params, sModuleInfo): + total_count = await _safe_count(locator) + if total_count > 0 and not params.get("allow_hidden"): + CommonUtil.ExecLog( + sModuleInfo, + "Found %s hidden elements and no displayed elements. Nothing to return.\n" % total_count + + 'To get hidden elements add a row ("allow hidden", "optional parameter", "yes")', + 3, + ) + else: + CommonUtil.ExecLog(sModuleInfo, "No elements found matching locator", 3) + + +async def _log_outer_html(locator, sModuleInfo): + try: + outer_html = await locator.evaluate("el => el.outerHTML") + CommonUtil.ExecLog(sModuleInfo, _opening_tag(outer_html), 5) + except Exception: + pass + + +async def _log_frame_hint(page, sModuleInfo): + try: + if await page.locator("iframe").count() > 0: + CommonUtil.ExecLog(sModuleInfo, 'You have Iframes in your Webpage. Try switching Iframe with "Switch Iframe" action', 3) + elif await page.locator("frame").count() > 0: + CommonUtil.ExecLog(sModuleInfo, 'You have Frames in your Webpage. Try switching Frame with "Switch Iframe" action', 3) + except Exception: + pass + + +def _resolve_timeout(params, element_wait): + if element_wait is not None: + return int(float(element_wait) * 1000) + if params.get("wait") is not None: + return int(float(params["wait"]) * 1000) + default_wait = sr.Get_Shared_Variables("element_wait") + if default_wait not in failed_tag_list: + return int(float(default_wait) * 1000) + return 10000 + + +def _first_locator(locator): + first = getattr(locator, "first") + return first() if callable(first) else first + + +def _has_relationship_params(params): + return any( + params[key] + for key in ( + "parent_params", + "child_params", + "sibling_params", + "preceding_params", + "following_params", + "shadow_root_params", + ) + ) + + +def _index_from_rows(rows): + for left, mid, right in rows: + if str(left).strip().lower() == "index" and str(mid).strip().lower() == "element parameter": + try: + return int(str(right).strip()) + except Exception: + return None + return None + + +def _matches_text_filters(text, filters): + normalized_text = text.replace("\xa0", " ") + for left, value in filters: + normalized_value = value.replace("\xa0", " ") + if left.startswith("**") and normalized_value.lower() in normalized_text.lower(): + return True + if left.startswith("*") and normalized_value in normalized_text: + return True + if normalized_value == normalized_text: + return True + return False + + +def _similar_text(text, filters): + collapsed_text = re.sub(r"\s+", "", text.lower().replace("\xa0", "")) + for _, value in filters: + if value.lower().replace("\xa0", "").replace(" ", "") in collapsed_text: + return True + return False + + +def _opening_tag(outer_html): + i, quote_count = 0, 0 + for i in range(len(outer_html)): + if outer_html[i] == '"': + quote_count += 1 + if outer_html[i] == ">" and quote_count % 2 == 0: + break + return outer_html[: i + 1] + + +def _as_playwright_xpath(query): + query = str(query).strip() + return query if query.startswith("xpath=") else f"xpath={query}" + + +def _xpath_literal(value): + value = str(value) + if '"' not in value: + return f'"{value}"' + if "'" not in value: + return f"'{value}'" + parts = value.split('"') + return "concat(%s)" % ", '\"', ".join(f'"{part}"' for part in parts) + + +def _mid_key(mid): + return str(mid).replace(" ", "").lower() + + +def _truthy(value): + return str(value).strip().lower() in ("yes", "true", "ok", "1", "enable") + + +def _extract_sr_index(mid_value): + """Extract index from 'sr N element parameter' format.""" + try: + parts = mid_value.lower().split() + for i, part in enumerate(parts): + if part == "sr" and i + 1 < len(parts): + return int(parts[i + 1]) + except (ValueError, IndexError): + pass + return 1 + + +# Backwards-compatible alias for code that imported the old helper directly. +def handle_shadow_dom(page, shadow_params, element_params): + params = { + "shadow_root_params": shadow_params, + "element_ds": [(left, "element parameter", right) for left, right in element_params], + } + result = _build_shadow_dom_locator(page, params) + return result.locator if result not in failed_tag_list and result is not None else "zeuz_failed" diff --git a/tests/test_playwright_locator.py b/tests/test_playwright_locator.py new file mode 100644 index 00000000..381d4f80 --- /dev/null +++ b/tests/test_playwright_locator.py @@ -0,0 +1,249 @@ +import asyncio + +from Framework.Built_In_Automation.Shared_Resources import ( + BuiltInFunctionSharedResources as sr, +) +from Framework.Built_In_Automation.Web.Playwright import locator as playwright_locator + + +class FakeLocator: + def __init__(self, name="root", count=1, total_count=None, texts=None, text=""): + self.name = name + self._count = count + self._total_count = count if total_count is None else total_count + self.texts = texts + self.text = text + self.filtered = False + self.nth_index = None + self.wait_calls = [] + self.queries = [] + + def locator(self, query): + self.queries.append(query) + return FakeLocator(f"{self.name}.locator({query})", self._count, self._total_count, self.texts, self.text) + + def filter(self, **kwargs): + filtered = FakeLocator(f"{self.name}.filter", self._count, self._total_count, self.texts, self.text) + filtered.filtered = kwargs.get("visible") is True + return filtered + + @property + def first(self): + text = self.texts[0] if self.texts else self.text + first = FakeLocator(f"{self.name}.first", min(self._count, 1), self._total_count, self.texts, text) + first.filtered = self.filtered + return first + + def nth(self, index): + text = self.texts[index] if self.texts and 0 <= index < len(self.texts) else self.text + nth = FakeLocator(f"{self.name}.nth({index})", 1, self._total_count, self.texts, text) + nth.nth_index = index + nth.filtered = self.filtered + return nth + + async def wait_for(self, state="visible", timeout=None): + self.wait_calls.append((state, timeout)) + if self._count == 0: + raise TimeoutError("not found") + + async def count(self): + return self._count + + async def all(self): + return [self.nth(i) for i in range(self._count)] + + async def text_content(self): + return self.text + + async def evaluate(self, script): + return '' + + +class FakePage: + def __init__(self, locator_result): + self.locator_result = locator_result + self.queries = [] + + def locator(self, query): + self.queries.append(query) + return self.locator_result + + +def setup_function(): + sr.shared_variables.clear() + sr.Set_Shared_Variables("element_wait", 1) + + +def test_parser_preserves_numbered_relationship_and_shadow_rows(): + params = playwright_locator._parse_element_params( + [ + ("tag", "element parameter", "button"), + ("class", "parent 2 parameter", "panel"), + ("id", "sr 1 element parameter", "shadow-host"), + ("wait", "optional parameter", "3"), + ] + ) + + assert params["element_params"] == [("tag", "button")] + assert params["parent_params"] == [("class", "parent 2 parameter", "panel")] + assert params["shadow_root_params"] == [("id", "sr 1 element parameter", "shadow-host")] + assert ("id", "sr 1 element parameter", "shadow-host") not in params["locator_rows"] + assert params["wait"] == 3 + + +def test_legacy_query_matches_selenium_for_tag_and_text(): + params = playwright_locator._parse_element_params( + [ + ("tag", "element parameter", "button"), + ("text", "element parameter", "Save"), + ] + ) + + query, query_type = playwright_locator._build_legacy_query(params["locator_rows"]) + + assert query_type == "xpath" + assert query == '//button[text()="Save"]' + + +def test_legacy_query_preserves_parent_child_sibling_rows(): + params = playwright_locator._parse_element_params( + [ + ("tag", "element parameter", "input"), + ("type", "element parameter", "password"), + ("id", "parent parameter", "login-form"), + ("text", "child parameter", "Required"), + ] + ) + + query, query_type = playwright_locator._build_legacy_query(params["locator_rows"]) + + assert query_type == "xpath" + assert "input" in query + assert '[@type="password"]' in query + assert 'ancestor::*[@id="login-form"]' in query + assert 'descendant::*[text()="Required"]' in query + + +def test_resolve_single_returns_first_for_multiple_matches(): + params = playwright_locator._parse_element_params( + [("tag", "element parameter", "button")] + ) + root = FakeLocator(count=3) + + result = asyncio.run(playwright_locator._resolve_single(root, params, 1000, "test")) + + assert isinstance(result, FakeLocator) + assert result.name.endswith(".filter.first") + assert result.filtered is True + + +def test_resolve_single_honors_index_after_visibility_filter(): + params = playwright_locator._parse_element_params( + [ + ("tag", "element parameter", "button"), + ("index", "element parameter", "2"), + ] + ) + root = FakeLocator(count=4) + + result = asyncio.run(playwright_locator._resolve_single(root, params, 1000, "test")) + + assert isinstance(result, FakeLocator) + assert result.nth_index == 2 + assert result.filtered is True + + +def test_get_element_uses_legacy_xpath_and_saves_shared_variables(): + fake_locator = FakeLocator(count=2) + page = FakePage(fake_locator) + + result = asyncio.run( + playwright_locator.Get_Element( + [ + ("tag", "element parameter", "button"), + ("text", "element parameter", "Save"), + ("saved_button", "save parameter", "yes"), + ], + page, + ) + ) + + assert result.name.endswith(".filter.first") + assert page.queries == ['xpath=//button[text()="Save"]'] + assert sr.Get_Shared_Variables("saved_button") is result + assert sr.Get_Shared_Variables("zeuz_element") is result + + +def test_get_element_accepts_selenium_return_all_elements_keyword(): + fake_locator = FakeLocator(count=2) + page = FakePage(fake_locator) + + result = asyncio.run( + playwright_locator.Get_Element( + [("tag", "element parameter", "button")], + page, + return_all_elements=True, + ) + ) + + assert len(result) == 2 + assert page.queries == ["xpath=//button"] + + +def test_unique_parameter_takes_precedence_over_raw_xpath_like_selenium(): + fake_locator = FakeLocator(count=1) + page = FakePage(fake_locator) + + result = playwright_locator._build_locator( + page, + [ + ("id", "unique parameter", "primary"), + ("xpath", "element parameter", "//button[@id='secondary']"), + ], + playwright_locator._parse_element_params( + [ + ("id", "unique parameter", "primary"), + ("xpath", "element parameter", "//button[@id='secondary']"), + ] + ), + ) + + assert result.query_type == "unique" + assert page.queries == ['xpath=//*[@id="primary"]'] + + +def test_shadow_dom_builder_uses_sr_rows_and_css_query_chain(): + params = playwright_locator._parse_element_params( + [ + ("id", "sr 1 element parameter", "host"), + ("tag", "element parameter", "button"), + ("data-action", "element parameter", "save"), + ] + ) + page = FakePage(FakeLocator()) + + result = playwright_locator._build_shadow_dom_locator(page, params) + + assert result.query_type == "shadow css" + assert result.query == '*[id="host"] >> button[data-action="save"]' + + +def test_text_filter_matches_normalized_nbsp_text(): + page = FakePage(FakeLocator(count=2, texts=["Hello\xa0World", "Other"])) + + result = asyncio.run( + playwright_locator._text_filter( + [ + ("tag", "element parameter", "div"), + ("text", "element parameter", "Hello World"), + ], + page, + None, + {"allow_hidden": False, "index": None, "sibling_params": []}, + 1000, + False, + ) + ) + + assert isinstance(result, FakeLocator) + assert result.text == "Hello\xa0World" From 3d52ffb80a89d1754f826dc5307c1036d2c99302 Mon Sep 17 00:00:00 2001 From: Md Nazmul Ahsan <76871125+mnahsanofficial@users.noreply.github.com> Date: Sun, 24 May 2026 21:39:32 +0600 Subject: [PATCH 3/6] Playwright new actions (#695) * new actions added * clean code added * test case fixes * update on previous actions & new action open electron app added * bug fixes --------- Co-authored-by: Nazmul Ahsan Co-authored-by: Nasif --- .../action_declarations/playwright.py | 8 + .../Web/Playwright/BuiltInFunctions.py | 1863 +++++++++++++++-- .../Web/Playwright/locator.py | 62 +- 3 files changed, 1723 insertions(+), 210 deletions(-) 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: