From 421c168942595bd953d9c5cc054b13dd0c936e94 Mon Sep 17 00:00:00 2001 From: Nazmul Ahsan Date: Wed, 20 May 2026 16:00:11 +0600 Subject: [PATCH 1/5] new actions added --- .../action_declarations/playwright.py | 7 + .../Web/Playwright/BuiltInFunctions.py | 605 ++++++++++++++++++ .../Web/Playwright/locator.py | 17 +- 3 files changed, 623 insertions(+), 6 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..c86025f1 100644 --- a/Framework/Built_In_Automation/Sequential_Actions/action_declarations/playwright.py +++ b/Framework/Built_In_Automation/Sequential_Actions/action_declarations/playwright.py @@ -34,6 +34,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 +46,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 +61,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 57bec137..358854d7 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -1727,6 +1727,611 @@ async def scroll_to_element(step_data): return CommonUtil.Exception_Handler(sys.exc_info()) +_SCROLL_TO_TOP_JS = """ +(() => { + var pre_x = window.pageXOffset; + var pre_y = window.pageYOffset; + window.scrollTo(window.pageXOffset, 0); + return [pre_x, pre_y, window.pageXOffset, window.pageYOffset]; +})() +""" + + +async def _pw_attr_value_for_save_list(locator, search_by_attribute): + """Read text / tag / checked / attribute like Selenium save_attribute_values_in_list.""" + if search_by_attribute == "text": + return await locator.inner_text() + if search_by_attribute == "tag": + tag = await locator.evaluate("el => el.tagName") + return tag.lower() if tag else "" + if search_by_attribute == "checked": + return str(await locator.is_checked()) + return await locator.get_attribute(search_by_attribute) + + +@logger +async def scroll_to_top(step_data): + """ + Scroll the main window to the top (y = 0). + + Example: + scroll to top playwright action scroll to top + """ + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + global current_page + + try: + session_name, current_page, current_page_id, context, browser = _handle_playwright_session(step_data) + + if current_page is None: + CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) + return "zeuz_failed" + + pre_x, pre_y, x, y = await current_page.evaluate(_SCROLL_TO_TOP_JS) + CommonUtil.ExecLog( + sModuleInfo, + f"Scrolled to top of the html.\npre_x, pre_y, x, y = [{pre_x}, {pre_y}, {x}, {y}]", + 1 if (x, y) == (0, 0) else 2, + ) + return "passed" + + except Exception: + return CommonUtil.Exception_Handler(sys.exc_info()) + + +@logger +async def save_attribute_values_in_list(step_data): + """ + Collect attribute/text values from multiple element groups under a parent (same as Selenium). + + Use \"playwright action\" for the action row. See Selenium docs for target parameter format. + """ + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + global current_page + + try: + session_name, current_page, current_page_id, context, browser = _handle_playwright_session(step_data) + + if current_page is None: + CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) + return "zeuz_failed" + + frame = _get_frame_locator() + parent = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=frame) + if parent == "zeuz_failed": + CommonUtil.ExecLog( + sModuleInfo, "Unable to locate your element with given data.", 3 + ) + return "zeuz_failed" + + all_elements = [] + target_index = 0 + target = [] + paired = True + + try: + for left, mid, right in step_data: + if mid.strip().lower() == "optional parameter" and left.strip().lower() == "session": + continue + left = left.strip().lower() + mid = mid.strip().lower() + right = right.strip() + if "target parameter" in mid: + target.append([[], [], [], []]) + temp = right.strip(",").split(",\n") + data = [] + for each in temp: + data.append(each.strip().split("=", 1)) + for i in range(len(data)): + for j in range(len(data[i])): + data[i][j] = data[i][j].strip() + if j == 1: + data[i][j] = CommonUtil.strip1(data[i][j], '"') + + for Left, Right in data: + if Left == "return": + target[target_index][1] = Right + elif Left == "return_contains": + target[target_index][2].append(Right) + elif Left == "return_does_not_contain": + target[target_index][3].append(Right) + elif Left.replace(" ", "").replace("_", "") in ("allowhidden", "allowdisable"): + target[target_index][0].append( + ("allow hidden", "optional parameter", Right) + ) + else: + target[target_index][0].append( + (Left, "element parameter", Right) + ) + + target_index = target_index + 1 + elif left == "save attribute values in list": + variable_name = right + elif left == "paired": + paired = False if right.lower() == "no" else True + + except Exception: + CommonUtil.ExecLog( + sModuleInfo, + "Unable to parse data. Please write data in correct format", + 3, + ) + return "zeuz_failed" + + for each in target: + elist = await PlaywrightLocator.Get_Element( + each[0], current_page, return_all=True, frame_locator=frame, parent_locator=parent + ) + if elist == "zeuz_failed": + CommonUtil.ExecLog(sModuleInfo, "Unable to locate target elements.", 3) + return "zeuz_failed" + all_elements.append(elist) + + variable_value_size = 0 + for each in all_elements: + variable_value_size = max(variable_value_size, len(each)) + + variable_value = [[] for _ in range(variable_value_size)] + + i = 0 + for each in all_elements: + search_by_attribute = target[i][1] + j = 0 + for elem in each: + Attribute_value = await _pw_attr_value_for_save_list(elem, search_by_attribute) + try: + for search_contain in target[i][2]: + if ( + not isinstance(search_contain, type(Attribute_value)) + or search_contain in Attribute_value + or len(search_contain) == 0 + ): + break + else: + if target[i][2]: + Attribute_value = None + + for search_doesnt_contain in target[i][3]: + if ( + isinstance(search_doesnt_contain, type(Attribute_value)) + and search_doesnt_contain in Attribute_value + and len(search_doesnt_contain) != 0 + ): + Attribute_value = None + except Exception: + CommonUtil.ExecLog( + sModuleInfo, + "Couldn't search by return_contains and return_does_not_contain", + 2, + ) + variable_value[j].append(Attribute_value) + j = j + 1 + i = i + 1 + if target_index == 1: + variable_value = list(map(list, zip(*variable_value)))[0] + elif not paired: + variable_value = list(map(list, zip(*variable_value))) + + return sr.Set_Shared_Variables(variable_name, variable_value) + + except Exception: + return CommonUtil.Exception_Handler(sys.exc_info()) + + +@logger +async def save_web_elements_in_list(step_data): + """ + Save lists of ElementHandle-like locators under optional parent scope (parity with Selenium). + + Omit parent element rows to capture from the whole page context. + """ + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + global current_page + + try: + session_name, current_page, current_page_id, context, browser = _handle_playwright_session(step_data) + + if current_page is None: + CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) + return "zeuz_failed" + + frame = _get_frame_locator() + + has_element = False + all_elements = [] + target_index = 0 + target = [] + variable_name = "" + + try: + for left, mid, right in step_data: + if mid.strip().lower() == "optional parameter" and left.strip().lower() == "session": + continue + left = left.strip().lower() + mid = mid.strip().lower() + right = right.strip() + if not has_element and mid in ( + "element parameter", + "parent parameter", + "unique parameter", + "child parameter", + "sibling parameter", + ): + has_element = True + elif "target parameter" in mid: + target.append([[], [], [], []]) + temp = right.strip(",").split(",") + data = [] + for each in temp: + if each.strip("\n").startswith("return_contains"): + data.append( + [ + "return_contains", + each.split("return_contains")[1].strip()[1:-1].split("="), + ] + ) + elif each.strip("\n").startswith("return_does_not_contain"): + data.append( + [ + "return_does_not_contain", + each.split("return_does_not_contain")[1].strip()[1:-1].split("="), + ] + ) + else: + data.append(each.strip().split("=")) + for i in range(len(data)): + for j in range(len(data[i])): + if isinstance(data[i][j], str): + data[i][j] = data[i][j].strip() + if j == 1: + if isinstance(data[i][j], list): + data[i][j][0], data[i][j][1] = ( + data[i][j][0].strip().strip('"'), + data[i][j][1].strip().strip('"'), + ) + elif isinstance(data[i][j], str): + data[i][j] = data[i][j].strip('"') + + for Left, Right in data: + if Left == "return_contains": + target[target_index][2].append(Right) + elif Left == "return_does_not_contain": + target[target_index][3].append(Right) + else: + target[target_index][0].append( + (Left, "element parameter", Right) + ) + + target_index = target_index + 1 + elif left == "save web elements in list": + variable_name = right + + if has_element: + parent = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=frame) + if parent == "zeuz_failed": + CommonUtil.ExecLog( + sModuleInfo, "Unable to locate your element with given data.", 3 + ) + return "zeuz_failed" + else: + parent = None + except Exception: + CommonUtil.ExecLog( + sModuleInfo, + "Unable to parse data. Please write data in correct format", + 3, + ) + return "zeuz_failed" + + for each in target: + elist = await PlaywrightLocator.Get_Element( + each[0], current_page, return_all=True, frame_locator=frame, parent_locator=parent + ) + if elist == "zeuz_failed": + CommonUtil.ExecLog(sModuleInfo, "Unable to locate target elements.", 3) + return "zeuz_failed" + all_elements.append(elist) + + cnt = 0 + while cnt < target_index: + if target[cnt][2]: + count, to_del = 0, [] + for elem in all_elements[cnt]: + elem_text = await elem.inner_text() + elem_tag = (await elem.evaluate("el => el.tagName")).lower() + for each in target[cnt][2]: + if each[0] == "text" and each[1] in elem_text: + break + else: + for each in target[cnt][2]: + if each[0] == "tag" and each[1] in elem_tag: + break + else: + for each in target[cnt][2]: + if ( + each[0] not in ("text", "tag") + and await elem.get_attribute(each[0]) is None + ): + break + else: + for each in target[cnt][2]: + if each[0] not in ("text", "tag") and each[1] in ( + await elem.get_attribute(each[0]) or "" + ): + break + else: + to_del.append(count) + count += 1 + all_elements[cnt] = CommonUtil.Delete_from_list(all_elements[cnt], to_del) + + if target[cnt][3]: + count, to_del = 0, [] + for elem in all_elements[cnt]: + elem_text = await elem.inner_text() + elem_tag = (await elem.evaluate("el => el.tagName")).lower() + for each in target[cnt][3]: + if each[0] == "text" and each[1] in elem_text: + to_del.append(count) + break + else: + for each in target[cnt][3]: + if each[0] == "tag" and each[1] in elem_tag: + to_del.append(count) + break + else: + for each in target[cnt][3]: + if ( + each[0] not in ("text", "tag") + and await elem.get_attribute(each[0]) is None + ): + to_del.append(count) + break + else: + for each in target[cnt][3]: + if each[0] not in ("text", "tag") and each[1] in ( + await elem.get_attribute(each[0]) or "" + ): + to_del.append(count) + break + + count += 1 + all_elements[cnt] = CommonUtil.Delete_from_list(all_elements[cnt], to_del) + + cnt += 1 + + if target_index == 1: + return sr.Set_Shared_Variables(variable_name, all_elements[0]) + else: + return sr.Set_Shared_Variables(variable_name, all_elements) + + except Exception: + return CommonUtil.Exception_Handler(sys.exc_info()) + + +def _insert_string_targets(string: str, str_to_insert: str, index: int) -> str: + return string[:index] + str_to_insert + string[index:] + + +@logger +async def multiple_check_uncheck(data_set): + """Check or uncheck multiple checkbox/radio elements.""" + + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + global current_page + + use_js = False + inside = False + allow_hidden = "" + targets = None + + try: + for left, mid, right in data_set: + left = left.lower().strip() + mid = mid.lower().strip() + if mid == "optional parameter" and left == "session": + continue + if "use js" == left: + use_js = right.strip().lower() in ("true", "yes", "ok") + elif "allow hidden" == left: + allow_hidden = right + elif "target parameter" == mid: + targets = [] + temp = right.strip() + i = 0 + while True: + if i >= len(temp): + break + if temp[i] == "(": + inside = True + temp = _insert_string_targets(temp, '"', i + 1) + elif inside and temp[i] == ",": + temp = _insert_string_targets(temp, '"', i + 1) + temp = _insert_string_targets(temp, '"', i) + i += 1 + if temp[i] == ")": + inside = False + temp = _insert_string_targets(temp, '"', i) + i += 1 + i += 1 + temp = _insert_string_targets(temp, "[", 0) + temp = _insert_string_targets(temp, "]", len(temp)) + temp = CommonUtil.parse_value_into_object(temp) + for Left, Mid, Right in temp: + targets.append( + (Left.strip().lower(), Mid.strip(), Right.strip().lower()) + ) + + except Exception: + return CommonUtil.Exception_Handler( + sys.exc_info(), None, "Error parsing data set" + ) + + if not targets: + CommonUtil.ExecLog(sModuleInfo, "No target parameter found in step data", 3) + return "zeuz_failed" + + try: + session_name, current_page, current_page_id, context, browser = _handle_playwright_session(data_set) + except Exception: + return CommonUtil.Exception_Handler(sys.exc_info()) + + if current_page is None: + CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) + return "zeuz_failed" + + frame = _get_frame_locator() + parent = await PlaywrightLocator.Get_Element(data_set, current_page, frame_locator=frame) + if parent == "zeuz_failed": + CommonUtil.ExecLog(sModuleInfo, "Could not find the parent element", 3) + return "zeuz_failed" + + element_params = [] + for left, mid, right in targets: + if allow_hidden: + element_params.append( + [ + ("allow hidden", "option", allow_hidden), + (left, "element parameter", mid), + ] + ) + else: + element_params.append([(left, "element parameter", mid)]) + + all_elements = [] + for i in element_params: + loc = await PlaywrightLocator.Get_Element(i, current_page, frame_locator=frame, parent_locator=parent) + all_elements.append(loc) + + for i in range(len(all_elements)): + if all_elements[i] == "zeuz_failed": + CommonUtil.ExecLog("", str(targets[i]) + " was not found so skipped it", 3) + continue + if targets[i][2] == "check" and await all_elements[i].is_checked(): + CommonUtil.ExecLog( + "", str(targets[i]) + " is already checked so skipped it", 1 + ) + continue + if targets[i][2] == "uncheck" and not await all_elements[i].is_checked(): + CommonUtil.ExecLog( + "", str(targets[i]) + " is already unchecked so skipped it", 1 + ) + continue + + try: + if use_js: + await all_elements[i].evaluate("el => el.click()") + if targets[i][2] == "check": + CommonUtil.ExecLog( + "", + str(targets[i]) + " is checked successfully using Java Script", + 1, + ) + else: + CommonUtil.ExecLog( + "", + str(targets[i]) + " is unchecked successfully using Java Script", + 1, + ) + else: + try: + await all_elements[i].click() + if targets[i][2] == "check": + CommonUtil.ExecLog("", str(targets[i]) + " is checked successfully", 1) + else: + CommonUtil.ExecLog("", str(targets[i]) + " is unchecked successfully", 1) + except Exception: + try: + await all_elements[i].evaluate("el => el.click()") + if targets[i][2] == "check": + CommonUtil.ExecLog( + "", + str(targets[i]) + + " is checked successfully using Java Script", + 1, + ) + else: + CommonUtil.ExecLog( + "", + str(targets[i]) + + " is unchecked successfully using Java Script", + 1, + ) + except Exception: + if targets[i][2] == "check": + CommonUtil.ExecLog( + "", + str(targets[i]) + " couldn't be checked so skipped it", + 3, + ) + else: + CommonUtil.ExecLog( + "", + str(targets[i]) + " couldn't be unchecked so skipped it", + 3, + ) + except Exception: + if targets[i][2] == "check": + CommonUtil.ExecLog( + "", str(targets[i]) + " couldn't be checked so skipped it", 3 + ) + else: + CommonUtil.ExecLog( + "", str(targets[i]) + " couldn't be unchecked so skipped it", 3 + ) + + return "passed" + + +@logger +async def Change_Attribute_Value(step_data): + """ + Set a JavaScript property on the located element (e.g. value on an input). + + Provide attribute name as the left field and new value on the right of input parameter rows. + """ + sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME + global current_page + + try: + session_name, current_page, current_page_id, context, browser = _handle_playwright_session(step_data) + + if current_page is None: + CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) + return "zeuz_failed" + + change_value = "" + attribute_name = "" + + locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) + if locator == "zeuz_failed": + CommonUtil.ExecLog( + sModuleInfo, "Unable to locate your element with given data.", 3 + ) + return "zeuz_failed" + for left, mid, right in step_data: + if mid.strip().lower() == "optional parameter" and left.strip().lower() == "session": + continue + mid = mid.strip().lower() + left = left.strip().lower() + if "input parameter" in mid: + attribute_name = left + change_value = right + + await locator.evaluate( + """(el, payload) => { el[payload.name] = payload.value; }""", + {"name": attribute_name, "value": change_value}, + ) + CommonUtil.ExecLog( + sModuleInfo, + "Successfully set the value of the attribute to: %s" % change_value, + 1, + ) + return "passed" + except Exception: + errMsg = "Could not find your element." + return CommonUtil.Exception_Handler(sys.exc_info(), None, errMsg) + + ######################### # # # Select/Dropdown # diff --git a/Framework/Built_In_Automation/Web/Playwright/locator.py b/Framework/Built_In_Automation/Web/Playwright/locator.py index 9acbe5b8..5ff35cc7 100644 --- a/Framework/Built_In_Automation/Web/Playwright/locator.py +++ b/Framework/Built_In_Automation/Web/Playwright/locator.py @@ -25,7 +25,7 @@ MODULE_NAME = inspect.getmodulename(__file__) -async def Get_Element(step_data, page, return_all=False, element_wait=None, frame_locator=None): +async def Get_Element(step_data, page, return_all=False, element_wait=None, frame_locator=None, parent_locator=None): """ Get element using Playwright's native Locator API. @@ -39,6 +39,7 @@ async def Get_Element(step_data, page, return_all=False, element_wait=None, fram 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 + parent_locator: Optional Locator to scope the search under a container element Returns: Locator | List[ElementHandle] | "zeuz_failed" @@ -76,7 +77,7 @@ async def Get_Element(step_data, page, return_all=False, element_wait=None, fram return "zeuz_failed" # Build the locator - locator = _build_locator(page, step_data, params, frame_locator) + locator = _build_locator(page, step_data, params, frame_locator, parent_locator) if locator is None: CommonUtil.ExecLog(sModuleInfo, "Could not build locator from step data", 3) @@ -192,7 +193,7 @@ def _parse_element_params(step_data): params['get_parameter'] = right_stripped.strip("%").strip("|") # Optional parameters - elif mid_lower == "optional parameter": + elif mid_lower == "optional parameter" or mid_lower == "option": if left_lower in ("allow hidden", "allow disable"): params['allow_hidden'] = right_stripped.lower() in ("yes", "true", "ok", "1") elif left_lower == "wait": @@ -231,7 +232,7 @@ def _parse_element_params(step_data): return params -def _build_locator(page, step_data, params, frame_locator=None): +def _build_locator(page, step_data, params, frame_locator=None, parent_locator=None): """ Build a Playwright Locator from step data. @@ -245,10 +246,14 @@ def _build_locator(page, step_data, params, frame_locator=None): step_data: Step data for building xpath params: Parsed element parameters frame_locator: Optional frame locator for iframe context + parent_locator: Scope search within this locator (overrides frame when both set). """ - # Use frame locator if provided, otherwise use page - base_locator = frame_locator if frame_locator else page + # Parent scope wins, then iframe, then full page. + if parent_locator is not None: + base_locator = parent_locator + else: + base_locator = frame_locator if frame_locator else page # Strategy 1: Check for Playwright-native selectors (fastest path) for left, right in params['element_params']: From 690d4cec464a071e554953def75e81f0eeb1f17d Mon Sep 17 00:00:00 2001 From: Nazmul Ahsan Date: Wed, 20 May 2026 16:08:34 +0600 Subject: [PATCH 2/5] clean code added --- .../Web/Playwright/BuiltInFunctions.py | 792 +++++++++--------- 1 file changed, 399 insertions(+), 393 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 358854d7..e9f4130b 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -1737,37 +1737,173 @@ async def scroll_to_element(step_data): """ -async def _pw_attr_value_for_save_list(locator, search_by_attribute): - """Read text / tag / checked / attribute like Selenium save_attribute_values_in_list.""" - if search_by_attribute == "text": +def _is_session_step_row(left, mid): + return mid.strip().lower() == "optional parameter" and left.strip().lower() == "session" + + +def _require_playwright_page(step_data, sModuleInfo): + """Activate session from step_data and ensure a page is open.""" + global current_page + _handle_playwright_session(step_data) + if current_page is None: + CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) + return "zeuz_failed" + return current_page + + +async def _read_element_attribute_for_list(locator, attribute_name): + if attribute_name == "text": return await locator.inner_text() - if search_by_attribute == "tag": + if attribute_name == "tag": tag = await locator.evaluate("el => el.tagName") return tag.lower() if tag else "" - if search_by_attribute == "checked": + if attribute_name == "checked": return str(await locator.is_checked()) - return await locator.get_attribute(search_by_attribute) + return await locator.get_attribute(attribute_name) + + +def _apply_return_contains_filter(value, contains_rules): + if not contains_rules: + return value + for rule in contains_rules: + if ( + not isinstance(rule, type(value)) + or rule in value + or len(rule) == 0 + ): + break + else: + return None + return value + + +def _apply_return_does_not_contain_filter(value, exclude_rules): + for rule in exclude_rules: + if ( + isinstance(rule, type(value)) + and rule in value + and len(rule) != 0 + ): + return None + return value + + +def _parse_target_kv_pairs(right, split_on_newline=False): + """Parse target parameter value into (key, value) pairs.""" + chunks = right.strip(",").split(",\n") if split_on_newline else right.strip(",").split(",") + pairs = [] + for chunk in chunks: + chunk = chunk.strip() + if chunk.startswith("return_contains"): + inner = chunk.split("return_contains", 1)[1].strip()[1:-1].split("=") + pairs.append(("return_contains", inner)) + elif chunk.startswith("return_does_not_contain"): + inner = chunk.split("return_does_not_contain", 1)[1].strip()[1:-1].split("=") + pairs.append(("return_does_not_contain", inner)) + else: + pairs.append(tuple(chunk.split("=", 1))) + return pairs + + +def _normalize_target_pair_values(pairs): + for index, (key, value) in enumerate(pairs): + key = key.strip() if isinstance(key, str) else key + if isinstance(value, str): + value = CommonUtil.strip1(value.strip(), '"') + elif isinstance(value, list) and len(value) == 2: + value = (value[0].strip().strip('"'), value[1].strip().strip('"')) + pairs[index] = (key, value) + + +def _append_target_spec(target_specs, key, value, spec_index): + spec = target_specs[spec_index] + if key == "return": + spec[1] = value + elif key == "return_contains": + if isinstance(value, list) and len(value) == 2: + spec[2].append((value[0], value[1])) + else: + spec[2].append(value) + elif key == "return_does_not_contain": + if isinstance(value, list) and len(value) == 2: + spec[3].append((value[0], value[1])) + else: + spec[3].append(value) + elif key.replace(" ", "").replace("_", "") in ("allowhidden", "allowdisable"): + spec[0].append(("allow hidden", "optional parameter", value)) + else: + spec[0].append((key, "element parameter", value)) + + +async def _element_matches_return_contains(elem, rules): + text = await elem.inner_text() + tag = (await elem.evaluate("el => el.tagName")).lower() + for attr, needle in rules: + if attr == "text" and needle in text: + return True + if attr == "tag" and needle in tag: + return True + if attr not in ("text", "tag"): + attr_val = await elem.get_attribute(attr) + if attr_val is None: + return False + if needle in attr_val: + return True + return False + + +async def _element_matches_return_does_not_contain(elem, rules): + text = await elem.inner_text() + tag = (await elem.evaluate("el => el.tagName")).lower() + for attr, needle in rules: + if attr == "text" and needle in text: + return True + if attr == "tag" and needle in tag: + return True + if attr not in ("text", "tag"): + attr_val = await elem.get_attribute(attr) + if attr_val is None or needle in (attr_val or ""): + return True + return False + + +async def _filter_elements_return_contains(elements, contains_rules): + if not contains_rules: + return elements + filtered = [] + for elem in elements: + if await _element_matches_return_contains(elem, contains_rules): + filtered.append(elem) + return filtered + + +async def _filter_elements_return_does_not_contain(elements, exclude_rules): + if not exclude_rules: + return elements + return [ + elem + for elem in elements + if not await _element_matches_return_does_not_contain(elem, exclude_rules) + ] @logger async def scroll_to_top(step_data): """ - Scroll the main window to the top (y = 0). + Scroll the browser window to the top of the page (vertical offset 0). Example: - scroll to top playwright action scroll to top + Field Sub Field Value + scroll to top playwright action scroll to top """ sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME - global current_page try: - session_name, current_page, current_page_id, context, browser = _handle_playwright_session(step_data) - - if current_page is None: - CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) + page = _require_playwright_page(step_data, sModuleInfo) + if page == "zeuz_failed": return "zeuz_failed" - pre_x, pre_y, x, y = await current_page.evaluate(_SCROLL_TO_TOP_JS) + pre_x, pre_y, x, y = await page.evaluate(_SCROLL_TO_TOP_JS) CommonUtil.ExecLog( sModuleInfo, f"Scrolled to top of the html.\npre_x, pre_y, x, y = [{pre_x}, {pre_y}, {x}, {y}]", @@ -1782,18 +1918,35 @@ async def scroll_to_top(step_data): @logger async def save_attribute_values_in_list(step_data): """ - Collect attribute/text values from multiple element groups under a parent (same as Selenium). - - Use \"playwright action\" for the action row. See Selenium docs for target parameter format. + Collect attribute or text values from multiple child element groups under a parent. + + Each target parameter block defines locators plus optional return filters. Results are + stored in a shared variable as rows (paired) or columns (paired=no). + + Example 1 - Product names and prices: + Field Sub Field Value + aria-label element parameter Calendar + attributes target parameter data-automation="productItemName", + class="S58f2saa25a3w1", + return="text" + attributes target parameter class="productPricingContainer_3gTS3", + return="text", + return_does_not_contain="99.99" + save attribute values in list playwright action product_rows + + Example 2 - Unpaired columns: + Field Sub Field Value + tag element parameter html + class target parameter item, return="text" + class target parameter price, return="text" + paired optional parameter no + save attribute values in list playwright action columns_list """ sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME global current_page try: - session_name, current_page, current_page_id, context, browser = _handle_playwright_session(step_data) - - if current_page is None: - CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) + if _require_playwright_page(step_data, sModuleInfo) == "zeuz_failed": return "zeuz_failed" frame = _get_frame_locator() @@ -1804,52 +1957,29 @@ async def save_attribute_values_in_list(step_data): ) return "zeuz_failed" - all_elements = [] - target_index = 0 - target = [] + targets = [] + variable_name = "" paired = True try: + spec_index = -1 for left, mid, right in step_data: - if mid.strip().lower() == "optional parameter" and left.strip().lower() == "session": + if _is_session_step_row(left, mid): continue left = left.strip().lower() mid = mid.strip().lower() right = right.strip() if "target parameter" in mid: - target.append([[], [], [], []]) - temp = right.strip(",").split(",\n") - data = [] - for each in temp: - data.append(each.strip().split("=", 1)) - for i in range(len(data)): - for j in range(len(data[i])): - data[i][j] = data[i][j].strip() - if j == 1: - data[i][j] = CommonUtil.strip1(data[i][j], '"') - - for Left, Right in data: - if Left == "return": - target[target_index][1] = Right - elif Left == "return_contains": - target[target_index][2].append(Right) - elif Left == "return_does_not_contain": - target[target_index][3].append(Right) - elif Left.replace(" ", "").replace("_", "") in ("allowhidden", "allowdisable"): - target[target_index][0].append( - ("allow hidden", "optional parameter", Right) - ) - else: - target[target_index][0].append( - (Left, "element parameter", Right) - ) - - target_index = target_index + 1 + targets.append([[], "", [], []]) + spec_index += 1 + pairs = _parse_target_kv_pairs(right, split_on_newline=True) + _normalize_target_pair_values(pairs) + for key, value in pairs: + _append_target_spec(targets, key, value, spec_index) elif left == "save attribute values in list": variable_name = right elif left == "paired": - paired = False if right.lower() == "no" else True - + paired = right.lower() != "no" except Exception: CommonUtil.ExecLog( sModuleInfo, @@ -1858,156 +1988,116 @@ async def save_attribute_values_in_list(step_data): ) return "zeuz_failed" - for each in target: - elist = await PlaywrightLocator.Get_Element( - each[0], current_page, return_all=True, frame_locator=frame, parent_locator=parent + element_groups = [] + for locator_rows, return_attr, contains_rules, exclude_rules in targets: + elements = await PlaywrightLocator.Get_Element( + locator_rows, + current_page, + return_all=True, + frame_locator=frame, + parent_locator=parent, ) - if elist == "zeuz_failed": + if elements == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Unable to locate target elements.", 3) return "zeuz_failed" - all_elements.append(elist) - - variable_value_size = 0 - for each in all_elements: - variable_value_size = max(variable_value_size, len(each)) + element_groups.append((elements, return_attr, contains_rules, exclude_rules)) - variable_value = [[] for _ in range(variable_value_size)] + max_len = max((len(group[0]) for group in element_groups), default=0) + rows = [[] for _ in range(max_len)] - i = 0 - for each in all_elements: - search_by_attribute = target[i][1] - j = 0 - for elem in each: - Attribute_value = await _pw_attr_value_for_save_list(elem, search_by_attribute) + for elements, return_attr, contains_rules, exclude_rules in element_groups: + for index, elem in enumerate(elements): + value = await _read_element_attribute_for_list(elem, return_attr) try: - for search_contain in target[i][2]: - if ( - not isinstance(search_contain, type(Attribute_value)) - or search_contain in Attribute_value - or len(search_contain) == 0 - ): - break - else: - if target[i][2]: - Attribute_value = None - - for search_doesnt_contain in target[i][3]: - if ( - isinstance(search_doesnt_contain, type(Attribute_value)) - and search_doesnt_contain in Attribute_value - and len(search_doesnt_contain) != 0 - ): - Attribute_value = None + value = _apply_return_contains_filter(value, contains_rules) + value = _apply_return_does_not_contain_filter(value, exclude_rules) except Exception: CommonUtil.ExecLog( sModuleInfo, "Couldn't search by return_contains and return_does_not_contain", 2, ) - variable_value[j].append(Attribute_value) - j = j + 1 - i = i + 1 - if target_index == 1: - variable_value = list(map(list, zip(*variable_value)))[0] + rows[index].append(value) + + if len(targets) == 1: + result = rows[0] if rows else [] elif not paired: - variable_value = list(map(list, zip(*variable_value))) + result = list(map(list, zip(*rows))) if rows else [] + else: + result = rows - return sr.Set_Shared_Variables(variable_name, variable_value) + return sr.Set_Shared_Variables(variable_name, result) except Exception: return CommonUtil.Exception_Handler(sys.exc_info()) +_ELEMENT_SCOPE_MIDS = frozenset( + { + "element parameter", + "parent parameter", + "unique parameter", + "child parameter", + "sibling parameter", + } +) + + @logger async def save_web_elements_in_list(step_data): """ - Save lists of ElementHandle-like locators under optional parent scope (parity with Selenium). + Save Playwright locators for multiple element groups into a shared variable. + + Optional parent scope limits the search. Use return_contains / return_does_not_contain + in target parameters to filter matched elements before saving. - Omit parent element rows to capture from the whole page context. + Example 1 - Under a container: + Field Sub Field Value + id element parameter product-list + class target parameter item, return_contains=text=Phone + save web elements in list playwright action phone_items + + Example 2 - Whole page (no parent rows): + Field Sub Field Value + class target parameter btn-primary + save web elements in list playwright action all_buttons """ sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME global current_page try: - session_name, current_page, current_page_id, context, browser = _handle_playwright_session(step_data) - - if current_page is None: - CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) + if _require_playwright_page(step_data, sModuleInfo) == "zeuz_failed": return "zeuz_failed" frame = _get_frame_locator() - - has_element = False - all_elements = [] - target_index = 0 - target = [] + targets = [] variable_name = "" + has_parent_scope = False try: + spec_index = -1 for left, mid, right in step_data: - if mid.strip().lower() == "optional parameter" and left.strip().lower() == "session": + if _is_session_step_row(left, mid): continue left = left.strip().lower() mid = mid.strip().lower() right = right.strip() - if not has_element and mid in ( - "element parameter", - "parent parameter", - "unique parameter", - "child parameter", - "sibling parameter", - ): - has_element = True + if not has_parent_scope and mid in _ELEMENT_SCOPE_MIDS: + has_parent_scope = True elif "target parameter" in mid: - target.append([[], [], [], []]) - temp = right.strip(",").split(",") - data = [] - for each in temp: - if each.strip("\n").startswith("return_contains"): - data.append( - [ - "return_contains", - each.split("return_contains")[1].strip()[1:-1].split("="), - ] - ) - elif each.strip("\n").startswith("return_does_not_contain"): - data.append( - [ - "return_does_not_contain", - each.split("return_does_not_contain")[1].strip()[1:-1].split("="), - ] - ) - else: - data.append(each.strip().split("=")) - for i in range(len(data)): - for j in range(len(data[i])): - if isinstance(data[i][j], str): - data[i][j] = data[i][j].strip() - if j == 1: - if isinstance(data[i][j], list): - data[i][j][0], data[i][j][1] = ( - data[i][j][0].strip().strip('"'), - data[i][j][1].strip().strip('"'), - ) - elif isinstance(data[i][j], str): - data[i][j] = data[i][j].strip('"') - - for Left, Right in data: - if Left == "return_contains": - target[target_index][2].append(Right) - elif Left == "return_does_not_contain": - target[target_index][3].append(Right) - else: - target[target_index][0].append( - (Left, "element parameter", Right) - ) - - target_index = target_index + 1 + targets.append([[], [], [], []]) + spec_index += 1 + pairs = _parse_target_kv_pairs(right) + _normalize_target_pair_values(pairs) + for key, value in pairs: + _append_target_spec(targets, key, value, spec_index) elif left == "save web elements in list": variable_name = right - if has_element: - parent = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=frame) + if has_parent_scope: + parent = await PlaywrightLocator.Get_Element( + step_data, current_page, frame_locator=frame + ) if parent == "zeuz_failed": CommonUtil.ExecLog( sModuleInfo, "Unable to locate your element with given data.", 3 @@ -2023,86 +2113,25 @@ async def save_web_elements_in_list(step_data): ) return "zeuz_failed" - for each in target: - elist = await PlaywrightLocator.Get_Element( - each[0], current_page, return_all=True, frame_locator=frame, parent_locator=parent + element_lists = [] + for locator_rows, _return_attr, contains_rules, exclude_rules in targets: + elements = await PlaywrightLocator.Get_Element( + locator_rows, + current_page, + return_all=True, + frame_locator=frame, + parent_locator=parent, ) - if elist == "zeuz_failed": + if elements == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Unable to locate target elements.", 3) return "zeuz_failed" - all_elements.append(elist) - - cnt = 0 - while cnt < target_index: - if target[cnt][2]: - count, to_del = 0, [] - for elem in all_elements[cnt]: - elem_text = await elem.inner_text() - elem_tag = (await elem.evaluate("el => el.tagName")).lower() - for each in target[cnt][2]: - if each[0] == "text" and each[1] in elem_text: - break - else: - for each in target[cnt][2]: - if each[0] == "tag" and each[1] in elem_tag: - break - else: - for each in target[cnt][2]: - if ( - each[0] not in ("text", "tag") - and await elem.get_attribute(each[0]) is None - ): - break - else: - for each in target[cnt][2]: - if each[0] not in ("text", "tag") and each[1] in ( - await elem.get_attribute(each[0]) or "" - ): - break - else: - to_del.append(count) - count += 1 - all_elements[cnt] = CommonUtil.Delete_from_list(all_elements[cnt], to_del) - - if target[cnt][3]: - count, to_del = 0, [] - for elem in all_elements[cnt]: - elem_text = await elem.inner_text() - elem_tag = (await elem.evaluate("el => el.tagName")).lower() - for each in target[cnt][3]: - if each[0] == "text" and each[1] in elem_text: - to_del.append(count) - break - else: - for each in target[cnt][3]: - if each[0] == "tag" and each[1] in elem_tag: - to_del.append(count) - break - else: - for each in target[cnt][3]: - if ( - each[0] not in ("text", "tag") - and await elem.get_attribute(each[0]) is None - ): - to_del.append(count) - break - else: - for each in target[cnt][3]: - if each[0] not in ("text", "tag") and each[1] in ( - await elem.get_attribute(each[0]) or "" - ): - to_del.append(count) - break - - count += 1 - all_elements[cnt] = CommonUtil.Delete_from_list(all_elements[cnt], to_del) - - cnt += 1 - - if target_index == 1: - return sr.Set_Shared_Variables(variable_name, all_elements[0]) - else: - return sr.Set_Shared_Variables(variable_name, all_elements) + elements = await _filter_elements_return_contains(elements, contains_rules) + elements = await _filter_elements_return_does_not_contain(elements, exclude_rules) + element_lists.append(elements) + + if len(targets) == 1: + return sr.Set_Shared_Variables(variable_name, element_lists[0]) + return sr.Set_Shared_Variables(variable_name, element_lists) except Exception: return CommonUtil.Exception_Handler(sys.exc_info()) @@ -2112,15 +2141,86 @@ def _insert_string_targets(string: str, str_to_insert: str, index: int) -> str: return string[:index] + str_to_insert + string[index:] +def _parse_multiple_check_targets(target_parameter_value): + """Parse target parameter string into (locator_key, locator_value, action) tuples.""" + inside = False + temp = target_parameter_value.strip() + i = 0 + while i < len(temp): + if temp[i] == "(": + inside = True + temp = _insert_string_targets(temp, '"', i + 1) + elif inside and temp[i] == ",": + temp = _insert_string_targets(temp, '"', i + 1) + temp = _insert_string_targets(temp, '"', i) + i += 1 + elif temp[i] == ")": + inside = False + temp = _insert_string_targets(temp, '"', i) + i += 1 + i += 1 + temp = _insert_string_targets(temp, "[", 0) + temp = _insert_string_targets(temp, "]", len(temp)) + parsed = CommonUtil.parse_value_into_object(temp) + return [ + (row[0].strip().lower(), row[1].strip(), row[2].strip().lower()) + for row in parsed + ] + + +async def _toggle_checkbox_target(locator, action, use_js, target_label): + """Check or uncheck one target; logs and swallows per-target failures like Selenium.""" + if action == "check" and await locator.is_checked(): + CommonUtil.ExecLog("", f"{target_label} is already checked so skipped it", 1) + return + if action == "uncheck" and not await locator.is_checked(): + CommonUtil.ExecLog("", f"{target_label} is already unchecked so skipped it", 1) + return + + via_js = use_js + try: + if use_js: + await locator.evaluate("el => el.click()") + else: + try: + await locator.click() + except Exception: + await locator.evaluate("el => el.click()") + via_js = True + verb = "checked" if action == "check" else "unchecked" + suffix = " using Java Script" if via_js else "" + CommonUtil.ExecLog("", f"{target_label} is {verb} successfully{suffix}", 1) + except Exception: + verb = "checked" if action == "check" else "unchecked" + CommonUtil.ExecLog("", f"{target_label} couldn't be {verb} so skipped it", 3) + + @logger async def multiple_check_uncheck(data_set): - """Check or uncheck multiple checkbox/radio elements.""" + """ + Check or uncheck multiple checkbox/radio elements under one parent. + Each entry in target parameter is (locator field, locator value, check|uncheck). + Missing targets are skipped; the step still returns passed. + + Example 1 - Basic: + Field Sub Field Value + id element parameter form-panel + target parameter target parameter (id, opt-a, check), (id, opt-b, uncheck) + multiple check uncheck playwright action multiple check uncheck + + Example 2 - JavaScript click: + Field Sub Field Value + class element parameter options-group + use js optional parameter true + allow hidden optional parameter yes + target parameter target parameter (name, hidden-opt, check) + multiple check uncheck playwright action multiple check uncheck + """ sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME global current_page use_js = False - inside = False allow_hidden = "" targets = None @@ -2128,38 +2228,14 @@ async def multiple_check_uncheck(data_set): for left, mid, right in data_set: left = left.lower().strip() mid = mid.lower().strip() - if mid == "optional parameter" and left == "session": + if _is_session_step_row(left, mid): continue - if "use js" == left: + if left == "use js": use_js = right.strip().lower() in ("true", "yes", "ok") - elif "allow hidden" == left: + elif left == "allow hidden": allow_hidden = right - elif "target parameter" == mid: - targets = [] - temp = right.strip() - i = 0 - while True: - if i >= len(temp): - break - if temp[i] == "(": - inside = True - temp = _insert_string_targets(temp, '"', i + 1) - elif inside and temp[i] == ",": - temp = _insert_string_targets(temp, '"', i + 1) - temp = _insert_string_targets(temp, '"', i) - i += 1 - if temp[i] == ")": - inside = False - temp = _insert_string_targets(temp, '"', i) - i += 1 - i += 1 - temp = _insert_string_targets(temp, "[", 0) - temp = _insert_string_targets(temp, "]", len(temp)) - temp = CommonUtil.parse_value_into_object(temp) - for Left, Mid, Right in temp: - targets.append( - (Left.strip().lower(), Mid.strip(), Right.strip().lower()) - ) + elif mid == "target parameter": + targets = _parse_multiple_check_targets(right) except Exception: return CommonUtil.Exception_Handler( @@ -2171,165 +2247,95 @@ async def multiple_check_uncheck(data_set): return "zeuz_failed" try: - session_name, current_page, current_page_id, context, browser = _handle_playwright_session(data_set) - except Exception: - return CommonUtil.Exception_Handler(sys.exc_info()) + if _require_playwright_page(data_set, sModuleInfo) == "zeuz_failed": + return "zeuz_failed" - if current_page is None: - CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) - return "zeuz_failed" + frame = _get_frame_locator() + parent = await PlaywrightLocator.Get_Element(data_set, current_page, frame_locator=frame) + if parent == "zeuz_failed": + CommonUtil.ExecLog(sModuleInfo, "Could not find the parent element", 3) + return "zeuz_failed" - frame = _get_frame_locator() - parent = await PlaywrightLocator.Get_Element(data_set, current_page, frame_locator=frame) - if parent == "zeuz_failed": - CommonUtil.ExecLog(sModuleInfo, "Could not find the parent element", 3) - return "zeuz_failed" + for locator_key, locator_value, action in targets: + locate_rows = [(locator_key, "element parameter", locator_value)] + if allow_hidden: + locate_rows.insert(0, ("allow hidden", "option", allow_hidden)) - element_params = [] - for left, mid, right in targets: - if allow_hidden: - element_params.append( - [ - ("allow hidden", "option", allow_hidden), - (left, "element parameter", mid), - ] - ) - else: - element_params.append([(left, "element parameter", mid)]) - - all_elements = [] - for i in element_params: - loc = await PlaywrightLocator.Get_Element(i, current_page, frame_locator=frame, parent_locator=parent) - all_elements.append(loc) - - for i in range(len(all_elements)): - if all_elements[i] == "zeuz_failed": - CommonUtil.ExecLog("", str(targets[i]) + " was not found so skipped it", 3) - continue - if targets[i][2] == "check" and await all_elements[i].is_checked(): - CommonUtil.ExecLog( - "", str(targets[i]) + " is already checked so skipped it", 1 + locator = await PlaywrightLocator.Get_Element( + locate_rows, current_page, frame_locator=frame, parent_locator=parent ) - continue - if targets[i][2] == "uncheck" and not await all_elements[i].is_checked(): - CommonUtil.ExecLog( - "", str(targets[i]) + " is already unchecked so skipped it", 1 - ) - continue + target_label = str((locator_key, locator_value, action)) + if locator == "zeuz_failed": + CommonUtil.ExecLog("", f"{target_label} was not found so skipped it", 3) + continue - try: - if use_js: - await all_elements[i].evaluate("el => el.click()") - if targets[i][2] == "check": - CommonUtil.ExecLog( - "", - str(targets[i]) + " is checked successfully using Java Script", - 1, - ) - else: - CommonUtil.ExecLog( - "", - str(targets[i]) + " is unchecked successfully using Java Script", - 1, - ) - else: - try: - await all_elements[i].click() - if targets[i][2] == "check": - CommonUtil.ExecLog("", str(targets[i]) + " is checked successfully", 1) - else: - CommonUtil.ExecLog("", str(targets[i]) + " is unchecked successfully", 1) - except Exception: - try: - await all_elements[i].evaluate("el => el.click()") - if targets[i][2] == "check": - CommonUtil.ExecLog( - "", - str(targets[i]) - + " is checked successfully using Java Script", - 1, - ) - else: - CommonUtil.ExecLog( - "", - str(targets[i]) - + " is unchecked successfully using Java Script", - 1, - ) - except Exception: - if targets[i][2] == "check": - CommonUtil.ExecLog( - "", - str(targets[i]) + " couldn't be checked so skipped it", - 3, - ) - else: - CommonUtil.ExecLog( - "", - str(targets[i]) + " couldn't be unchecked so skipped it", - 3, - ) - except Exception: - if targets[i][2] == "check": - CommonUtil.ExecLog( - "", str(targets[i]) + " couldn't be checked so skipped it", 3 - ) - else: - CommonUtil.ExecLog( - "", str(targets[i]) + " couldn't be unchecked so skipped it", 3 - ) + await _toggle_checkbox_target(locator, action, use_js, target_label) - return "passed" + return "passed" + + except Exception: + return CommonUtil.Exception_Handler(sys.exc_info()) @logger async def Change_Attribute_Value(step_data): """ - Set a JavaScript property on the located element (e.g. value on an input). + Set a DOM property on the located element via JavaScript (same idea as Selenium). - Provide attribute name as the left field and new value on the right of input parameter rows. + The left column of an input parameter row is the property name; the right column is the value. + + Example 1 - Input value: + Field Sub Field Value + id element parameter email-field + value input parameter user@example.com + change attribute value playwright action change attribute value + + Example 2 - Read-only flag: + Field Sub Field Value + id element parameter age-input + readOnly input parameter true + change attribute value playwright action change attribute value """ sModuleInfo = inspect.currentframe().f_code.co_name + " : " + MODULE_NAME global current_page try: - session_name, current_page, current_page_id, context, browser = _handle_playwright_session(step_data) - - if current_page is None: - CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) + if _require_playwright_page(step_data, sModuleInfo) == "zeuz_failed": return "zeuz_failed" - change_value = "" attribute_name = "" + change_value = "" + for left, mid, right in step_data: + if _is_session_step_row(left, mid): + continue + if "input parameter" in mid.strip().lower(): + attribute_name = left.strip().lower() + change_value = right - locator = await PlaywrightLocator.Get_Element(step_data, current_page, frame_locator=_get_frame_locator()) + locator = await PlaywrightLocator.Get_Element( + step_data, current_page, frame_locator=_get_frame_locator() + ) if locator == "zeuz_failed": CommonUtil.ExecLog( sModuleInfo, "Unable to locate your element with given data.", 3 ) return "zeuz_failed" - for left, mid, right in step_data: - if mid.strip().lower() == "optional parameter" and left.strip().lower() == "session": - continue - mid = mid.strip().lower() - left = left.strip().lower() - if "input parameter" in mid: - attribute_name = left - change_value = right await locator.evaluate( - """(el, payload) => { el[payload.name] = payload.value; }""", + "(el, payload) => { el[payload.name] = payload.value; }", {"name": attribute_name, "value": change_value}, ) CommonUtil.ExecLog( sModuleInfo, - "Successfully set the value of the attribute to: %s" % change_value, + f"Successfully set the value of the attribute to: {change_value}", 1, ) return "passed" + except Exception: - errMsg = "Could not find your element." - return CommonUtil.Exception_Handler(sys.exc_info(), None, errMsg) + return CommonUtil.Exception_Handler( + sys.exc_info(), None, "Could not find your element." + ) ######################### From 30d6a2046f81857de69d8186b9e9117fe1b675ee Mon Sep 17 00:00:00 2001 From: Nazmul Ahsan Date: Wed, 20 May 2026 23:04:58 +0600 Subject: [PATCH 3/5] test case fixes --- .../Web/Playwright/BuiltInFunctions.py | 176 +++++++++++++++--- 1 file changed, 147 insertions(+), 29 deletions(-) diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index e9f4130b..7e443c5d 100644 --- a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py +++ b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py @@ -1738,7 +1738,59 @@ async def scroll_to_element(step_data): def _is_session_step_row(left, mid): - return mid.strip().lower() == "optional parameter" and left.strip().lower() == "session" + return _normalize_step_mid(mid) == "optionalparameter" and left.strip().lower() == "session" + + +def _normalize_step_mid(mid): + """Match LocateElement: ignore spaces/newlines in sub-field names.""" + return ( + mid.replace(" ", "") + .replace("\n", "") + .replace("\r", "") + .replace("\t", "") + .lower() + ) + + +def _is_element_parameter_mid(mid): + mid_norm = _normalize_step_mid(mid) + return mid_norm == "elementparameter" or ( + "element" in mid_norm and "parameter" in mid_norm and "target" not in mid_norm + ) + + +def _is_target_parameter_mid(mid): + mid_norm = _normalize_step_mid(mid) + # UI may show only "target" on one line; "parameter" on the next (full name is still one row in data). + return mid_norm in ("targetparameter", "target") or ( + "target" in mid_norm and "parameter" in mid_norm + ) + + +def _resolve_list_action_variable_name(left, mid, right, action_key): + """ + Read shared-variable name from a save-*-in-list action row. + + Zeuz may send the variable in the Value column (right) or Field column (left) + when the other column repeats the action keyword. + """ + left_raw = left.strip() + right_raw = right.strip() + left_norm = left_raw.lower().replace(" ", "").replace("_", "") + right_norm = right_raw.lower().replace(" ", "").replace("_", "") + mid_norm = _normalize_step_mid(mid) + + if left_norm == action_key: + if right_norm and right_norm != action_key: + return right_raw + return None + + if mid_norm in ("playwrightaction", "seleniumaction"): + if right_norm == action_key and left_norm and left_norm != action_key: + return left_raw + if right_norm and right_norm != action_key: + return right_raw + return None def _require_playwright_page(step_data, sModuleInfo): @@ -1788,31 +1840,59 @@ def _apply_return_does_not_contain_filter(value, exclude_rules): return value -def _parse_target_kv_pairs(right, split_on_newline=False): +def _target_param_continues_previous_target(right): + """True when this target-parameter row adds return/filter rules to the prior target.""" + text = right.strip().lower() + return text.startswith( + ("return", "return_contains", "return_does_not_contain", "allow hidden", "allowhidden") + ) + + +def _parse_target_kv_pairs(right, split_on_newline=False, field_hint=None): """Parse target parameter value into (key, value) pairs.""" - chunks = right.strip(",").split(",\n") if split_on_newline else right.strip(",").split(",") + text = right.strip().rstrip(",") + # UI may use comma-only (one line) or comma+newline (multi-line) separators. + if split_on_newline and ",\n" in text: + chunks = text.split(",\n") + else: + chunks = text.split(",") pairs = [] + hint = (field_hint or "").strip().lower() or "class" for chunk in chunks: - chunk = chunk.strip() + chunk = chunk.strip().rstrip(",") + if not chunk: + continue if chunk.startswith("return_contains"): inner = chunk.split("return_contains", 1)[1].strip()[1:-1].split("=") pairs.append(("return_contains", inner)) elif chunk.startswith("return_does_not_contain"): inner = chunk.split("return_does_not_contain", 1)[1].strip()[1:-1].split("=") pairs.append(("return_does_not_contain", inner)) + elif "=" in chunk: + key, value = chunk.split("=", 1) + pairs.append((key.strip(), value.strip())) + elif hint: + # Shorthand: "productItemName" with Field column "class" + pairs.append((hint, chunk)) else: - pairs.append(tuple(chunk.split("=", 1))) + pairs.append((chunk, "")) return pairs def _normalize_target_pair_values(pairs): - for index, (key, value) in enumerate(pairs): + normalized = [] + for pair in pairs: + if len(pair) == 1: + normalized.append((pair[0].strip(), "")) + continue + key, value = pair[0], pair[1] key = key.strip() if isinstance(key, str) else key if isinstance(value, str): value = CommonUtil.strip1(value.strip(), '"') elif isinstance(value, list) and len(value) == 2: value = (value[0].strip().strip('"'), value[1].strip().strip('"')) - pairs[index] = (key, value) + normalized.append((key, value)) + pairs[:] = normalized def _append_target_spec(target_specs, key, value, spec_index): @@ -1966,24 +2046,46 @@ class target parameter price, return="text" for left, mid, right in step_data: if _is_session_step_row(left, mid): continue - left = left.strip().lower() - mid = mid.strip().lower() + left_l = left.strip().lower() + left_key = left_l.replace(" ", "").replace("_", "") right = right.strip() - if "target parameter" in mid: - targets.append([[], "", [], []]) - spec_index += 1 - pairs = _parse_target_kv_pairs(right, split_on_newline=True) + if _is_target_parameter_mid(mid): + if spec_index >= 0 and _target_param_continues_previous_target(right): + pass # append to current target spec + else: + targets.append([[], "", [], []]) + spec_index += 1 + pairs = _parse_target_kv_pairs( + right, split_on_newline=True, field_hint=left_l + ) _normalize_target_pair_values(pairs) for key, value in pairs: _append_target_spec(targets, key, value, spec_index) - elif left == "save attribute values in list": - variable_name = right - elif left == "paired": + else: + var_candidate = _resolve_list_action_variable_name( + left, + mid, + right, + "saveattributevaluesinlist", + ) + if var_candidate: + variable_name = var_candidate + if left_l == "paired": paired = right.lower() != "no" - except Exception: + if not targets: + CommonUtil.ExecLog(sModuleInfo, "No target parameter rows found in step data", 3) + return "zeuz_failed" + if not variable_name: + CommonUtil.ExecLog( + sModuleInfo, + "No variable name for save attribute values in list (set action value e.g. product_data)", + 3, + ) + return "zeuz_failed" + except Exception as exc: CommonUtil.ExecLog( sModuleInfo, - "Unable to parse data. Please write data in correct format", + f"Unable to parse data. Please write data in correct format ({exc})", 3, ) return "zeuz_failed" @@ -2020,7 +2122,8 @@ class target parameter price, return="text" rows[index].append(value) if len(targets) == 1: - result = rows[0] if rows else [] + # Match Selenium: one target => flat list of values (all elements), not rows[0] only. + result = list(map(list, zip(*rows)))[0] if rows else [] elif not paired: result = list(map(list, zip(*rows))) if rows else [] else: @@ -2082,17 +2185,27 @@ class target parameter btn-primary left = left.strip().lower() mid = mid.strip().lower() right = right.strip() - if not has_parent_scope and mid in _ELEMENT_SCOPE_MIDS: + if not has_parent_scope and _is_element_parameter_mid(mid): has_parent_scope = True - elif "target parameter" in mid: - targets.append([[], [], [], []]) - spec_index += 1 - pairs = _parse_target_kv_pairs(right) + elif _is_target_parameter_mid(mid): + if spec_index >= 0 and _target_param_continues_previous_target(right): + pass + else: + targets.append([[], [], [], []]) + spec_index += 1 + pairs = _parse_target_kv_pairs(right, field_hint=left) _normalize_target_pair_values(pairs) for key, value in pairs: _append_target_spec(targets, key, value, spec_index) - elif left == "save web elements in list": - variable_name = right + else: + var_candidate = _resolve_list_action_variable_name( + left, + mid, + right, + "savewebelementsinlist", + ) + if var_candidate: + variable_name = var_candidate if has_parent_scope: parent = await PlaywrightLocator.Get_Element( @@ -3108,10 +3221,15 @@ async def execute_javascript(step_data): for left, mid, right in step_data: left_l = left.strip().lower() mid_l = mid.strip().lower() + right_v = right.strip() - if mid_l == "action": - js_code = right - elif mid_l == "element parameter": + if _is_session_step_row(left, mid): + continue + if mid_l in ("playwright action", "selenium action"): + continue + if "javascript" in left_l: + js_code = right_v + elif _is_element_parameter_mid(mid): has_element = True elif mid_l == "save parameter": save_variable = left.strip() From cd7253b43b6e50f680cb2c62dde9c4dc5a342d3d Mon Sep 17 00:00:00 2001 From: Nazmul Ahsan Date: Fri, 22 May 2026 19:07:35 +0600 Subject: [PATCH 4/5] update on previous actions & new action open electron app added --- .../action_declarations/playwright.py | 1 + .../Web/Playwright/BuiltInFunctions.py | 1105 ++++++++++++++--- 2 files changed, 908 insertions(+), 198 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 c86025f1..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" }, diff --git a/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py b/Framework/Built_In_Automation/Web/Playwright/BuiltInFunctions.py index 7e443c5d..d4585c30 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 @@ -419,17 +420,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 - wait until options: load, domcontentloaded, networkidle, commit + 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" + + 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 @@ -441,32 +611,32 @@ 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 existing_session.get("playwright_page"): _set_active_playwright_session(session_name, existing_session) CommonUtil.ExecLog(sModuleInfo, f"Using existing browser session: {session_name}", 1) else: # Session doesn't exist, open new browser with session name CommonUtil.ExecLog(sModuleInfo, f"Session '{session_name}' not found. Opening new browser.", 2) - + # Add session parameter to step_data for Open_Browser step_data_with_session = step_data.copy() if not any(left.strip().lower() == "session" and mid.strip().lower() == "optional parameter" for left, mid, right in step_data_with_session): step_data_with_session.append(("session", "optional parameter", session_name)) - + result = await Open_Browser(step_data_with_session) if result == "zeuz_failed": CommonUtil.ExecLog(sModuleInfo, "Failed to open browser for new session", 3) return "zeuz_failed" - + elif current_page is None: # No session specified and no browser open CommonUtil.ExecLog(sModuleInfo, "No browser open. Opening browser with default settings.", 2) @@ -478,42 +648,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 @@ -757,7 +965,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. @@ -766,19 +974,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 @@ -789,14 +1002,14 @@ async def Click_Element(step_data): try: # Handle session parameter session_name, current_page, current_page_id, context, browser = _handle_playwright_session(step_data) - + if current_page is None: CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) return "zeuz_failed" # Parse options use_js = False - offset = None + offset_value = "" double_click = False right_click = False click_count = 1 @@ -817,8 +1030,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": @@ -837,15 +1049,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: @@ -856,18 +1104,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()) @@ -987,7 +1269,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 @@ -1002,7 +1284,7 @@ async def Enter_Text_In_Text_Box(step_data): try: # Handle session parameter session_name, current_page, current_page_id, context, browser = _handle_playwright_session(step_data) - + if current_page is None: CommonUtil.ExecLog(sModuleInfo, "No browser open", 3) return "zeuz_failed" @@ -1035,38 +1317,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 @@ -1316,14 +1638,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 @@ -1333,35 +1663,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 @@ -1369,7 +1723,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 @@ -1377,13 +1737,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: