From ef7fda8f53ff5cee99978d68c1afe036b3373c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Mon, 9 Mar 2026 14:47:39 +0200 Subject: [PATCH 01/21] feat(windows): add Get Checkbox State keyword for toggle state detection Add new `Get Checkbox State` keyword to RPA.Windows that uses the UIAutomation TogglePattern to return True (checked), False (unchecked), or None (indeterminate) for checkbox controls. Closes #1166. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- docs/source/releasenotes.rst | 1 + .../src/RPA/Windows/keywords/action.py | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/docs/source/releasenotes.rst b/docs/source/releasenotes.rst index eed8d23899..b40745fcb5 100644 --- a/docs/source/releasenotes.rst +++ b/docs/source/releasenotes.rst @@ -12,6 +12,7 @@ Latest versions `Upcoming release `_ +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- ``RPA.Windows``: New ``Get Checkbox State`` keyword to retrieve the toggle state of a checkbox control, returning ``True`` (checked), ``False`` (unchecked) or ``None`` (indeterminate). Closes :issue:`1166`. `Released `_ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/packages/windows/src/RPA/Windows/keywords/action.py b/packages/windows/src/RPA/Windows/keywords/action.py index beaad8651a..28d024af92 100644 --- a/packages/windows/src/RPA/Windows/keywords/action.py +++ b/packages/windows/src/RPA/Windows/keywords/action.py @@ -290,6 +290,58 @@ def is_selected(self, locator: Locator) -> Optional[bool]: f"Element found with {locator!r} doesn't support selection item retrieval" ) + @keyword(tags=["action"]) + def get_checkbox_state(self, locator: Locator) -> Optional[bool]: + """Get the toggle state of a checkbox element defined by the provided `locator`. + + Returns ``True`` if the checkbox is checked, ``False`` if unchecked, and + ``None`` if the state is indeterminate. + + The ``ActionNotPossible`` exception is raised if the identified element doesn't + support the toggle pattern (i.e. is not a checkbox or similar toggleable control). + + :param locator: String locator or element object. + :returns: ``True`` if checked, ``False`` if unchecked, ``None`` if indeterminate. + + **Example: Robot Framework** + + .. code-block:: robotframework + + ${checked} = Get Checkbox State type:CheckBoxControl name:Accept + IF ${checked} + Log Checkbox is checked + END + + **Example: Python** + + .. code-block:: python + + from RPA.Windows import Windows + + lib_win = Windows() + state = lib_win.get_checkbox_state("type:CheckBoxControl name:Accept") + print(state) # True, False, or None + """ + element = self.ctx.get_element(locator) + get_toggle_pattern = getattr(element.item, "GetTogglePattern", None) + + if get_toggle_pattern: + toggle_pattern = get_toggle_pattern() + if toggle_pattern is None: + raise ActionNotPossible( + f"Element found with {locator!r} doesn't support the toggle pattern" + ) + toggle_state = toggle_pattern.ToggleState + if utils.IS_WINDOWS: + if toggle_state == auto.ToggleState.On: + return True + if toggle_state == auto.ToggleState.Off: + return False + return None # Indeterminate + raise ActionNotPossible( + f"Element found with {locator!r} doesn't support the toggle pattern" + ) + @keyword(tags=["action", "keyboard"]) def send_keys( self, From 52e04bc0560ffcdffe8cfce29b24338cca94a3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Mon, 9 Mar 2026 14:49:11 +0200 Subject: [PATCH 02/21] fix(windows): remove redundant IS_WINDOWS guard in get_checkbox_state The IS_WINDOWS guard around ToggleState comparisons caused fall-through to ActionNotPossible with a misleading error on non-Windows. Since auto.ToggleState is already guarded at import time, the check is redundant and the indeterminate None return path was unreachable. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- packages/windows/src/RPA/Windows/keywords/action.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/windows/src/RPA/Windows/keywords/action.py b/packages/windows/src/RPA/Windows/keywords/action.py index 28d024af92..2e96b941ff 100644 --- a/packages/windows/src/RPA/Windows/keywords/action.py +++ b/packages/windows/src/RPA/Windows/keywords/action.py @@ -332,12 +332,11 @@ def get_checkbox_state(self, locator: Locator) -> Optional[bool]: f"Element found with {locator!r} doesn't support the toggle pattern" ) toggle_state = toggle_pattern.ToggleState - if utils.IS_WINDOWS: - if toggle_state == auto.ToggleState.On: - return True - if toggle_state == auto.ToggleState.Off: - return False - return None # Indeterminate + if toggle_state == auto.ToggleState.On: + return True + if toggle_state == auto.ToggleState.Off: + return False + return None # Indeterminate raise ActionNotPossible( f"Element found with {locator!r} doesn't support the toggle pattern" ) From 3710bb4315d8700fee1c70903a22d01f368db3b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Mon, 9 Mar 2026 15:01:59 +0200 Subject: [PATCH 03/21] fix(windows): respect timeout in path: locator strategy _get_control_from_path() was ignoring the active UIAutomation timeout when traversing path steps, raising ElementNotFound immediately if a child wasn't present. Now it reads the active timeout (set by @with_timeout via SetGlobalSearchTimeout) and retries each step until the deadline, matching the behaviour of all other locator strategies. Closes #1125. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- docs/source/releasenotes.rst | 1 + .../core/src/RPA/core/windows/locators.py | 83 ++++++++++++------- 2 files changed, 53 insertions(+), 31 deletions(-) diff --git a/docs/source/releasenotes.rst b/docs/source/releasenotes.rst index b40745fcb5..2a8c448ffe 100644 --- a/docs/source/releasenotes.rst +++ b/docs/source/releasenotes.rst @@ -13,6 +13,7 @@ Latest versions +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ``RPA.Windows``: New ``Get Checkbox State`` keyword to retrieve the toggle state of a checkbox control, returning ``True`` (checked), ``False`` (unchecked) or ``None`` (indeterminate). Closes :issue:`1166`. +- ``RPA.Windows``: Fix ``path:`` locator strategy ignoring the ``timeout`` parameter — path traversal now retries each step until the active timeout expires instead of failing immediately. Closes :issue:`1125`. `Released `_ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/packages/core/src/RPA/core/windows/locators.py b/packages/core/src/RPA/core/windows/locators.py index cb11d1edbf..5d6a9f73fa 100644 --- a/packages/core/src/RPA/core/windows/locators.py +++ b/packages/core/src/RPA/core/windows/locators.py @@ -410,7 +410,7 @@ def _get_control_from_path( Returns: Control at the end of the path Raises: - ElementNotFound: If path cannot be followed + ElementNotFound: If path cannot be followed within the active timeout """ # Follow a path in the tree of controls until reaching the final target. search_params = search_params.copy() # to keep idempotent behaviour @@ -422,42 +422,63 @@ def _get_control_from_path( max_retries = 3 retry_delay = 0.1 + # Respect the active timeout (set via @with_timeout / SetGlobalSearchTimeout). + # self.current_timeout reads auto.uiautomation.TIME_OUT_SECOND which is kept + # in sync by the set_timeout() context manager. + timeout = self.current_timeout if IS_WINDOWS else 0.0 + deadline = time.monotonic() + timeout for index, position in enumerate(path): - # Retry logic for GetChildren() which can raise COM errors - last_error = None - children = None - for attempt in range(max_retries): - try: - children = current.GetChildren() - break - except COMError as err: - last_error = err - if LocatorMethods._is_com_cantcallout_error(err) and attempt < max_retries - 1: - self.logger.debug( - "COM error 0x8001010d during GetChildren (attempt %d/%d), retrying...", - attempt + 1, - max_retries, - ) - time.sleep(retry_delay) - continue - # Non-retryable error or last attempt, raise immediately - raise - if children is None and last_error is not None: - # If we exhausted retries, re-raise the last error - raise last_error - - if position > len(children): - raise ElementNotFound( - f"Unable to retrieve child on position {position!r} under a parent" - f" with partial path {to_path(index)!r}" + # Retry at this path step until the child appears or the timeout expires. + while True: + # Inner retry loop for transient COM errors on GetChildren(). + last_error = None + children = None + for attempt in range(max_retries): + try: + children = current.GetChildren() + break + except COMError as err: + last_error = err + if ( + LocatorMethods._is_com_cantcallout_error(err) + and attempt < max_retries - 1 + ): + self.logger.debug( + "COM error 0x8001010d during GetChildren" + " (attempt %d/%d), retrying...", + attempt + 1, + max_retries, + ) + time.sleep(retry_delay) + continue + # Non-retryable error or last attempt, raise immediately + raise + if children is None and last_error is not None: + raise last_error + + if position <= len(children): + break # Child is available — proceed + + # Child not present yet; wait if the timeout allows it. + remaining = deadline - time.monotonic() + if remaining <= 0: + raise ElementNotFound( + f"Unable to retrieve child on position {position!r} under a" + f" parent with partial path {to_path(index)!r}" + ) + self.logger.debug( + "Child at position %d not yet available (%d children found)," + " retrying for %.2fs...", + position, + len(children), + remaining, ) + time.sleep(min(retry_delay, remaining)) current = children[position - 1] # Log position only to avoid deadlock from control.__repr__ - self.logger.debug( - "On child position %d found control", position - ) + self.logger.debug("On child position %d found control", position) offset = search_params.get("offset") current.robocorp_click_offset = offset From b208f64d720f44e36321a5d89e8829ef13785507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Mon, 9 Mar 2026 15:23:34 +0200 Subject: [PATCH 04/21] feat(windows): add testapp.py test fixture and robot test cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add packages/windows/resources/testapp.py — a self-contained tkinter GUI covering buttons, checkboxes (checked/unchecked/indeterminate), radio buttons, text entry, combobox, slider, spinbox, listbox, treeview and a dynamic element panel for timeout testing. Add corresponding Robot Framework test cases: - Test App Checkboxes (exercises Get Checkbox State / #1166) - Test App Radio Buttons - Test App Text Entry - Test App Combobox - Test App Slider - Test App Path Locator Timeout (exercises path: timeout fix / #1125) Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- packages/windows/resources/testapp.py | 374 ++++++++++++++++++ .../windows/tests/robot/test_windows.robot | 128 ++++++ 2 files changed, 502 insertions(+) create mode 100644 packages/windows/resources/testapp.py diff --git a/packages/windows/resources/testapp.py b/packages/windows/resources/testapp.py new file mode 100644 index 0000000000..b4bd02f2c6 --- /dev/null +++ b/packages/windows/resources/testapp.py @@ -0,0 +1,374 @@ +"""RPA.Windows test application. + +A self-contained tkinter GUI that exposes every control type exercised by +the RPA.Windows test suite: + + - Buttons (normal, disabled) + - Checkboxes (checked / unchecked / indeterminate tri-state) + - Radio buttons + - Text entry / multi-line text + - Combobox (dropdown) + - Listbox + - Slider (Scale) + - Spinner (Spinbox) + - Labels (static and dynamic status) + - Notebook (tabs) — useful for path: locator tests + - Treeview table + +Usage +----- +Run directly:: + + python testapp.py + +Or from Robot Framework test setup:: + + Start Process python ${RESOURCES}/testapp.py alias=testapp + # give it time to appear + Sleep 1s + +The window title is "RPA Windows Test App" and the app stays open until +the "Quit" button or the window close button is pressed. +""" + +import tkinter as tk +from tkinter import ttk + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _section(parent: tk.Widget, title: str) -> ttk.LabelFrame: + frame = ttk.LabelFrame(parent, text=title, padding=8) + return frame + + +# --------------------------------------------------------------------------- +# Tab builders +# --------------------------------------------------------------------------- + +def _build_tab_basic(nb: ttk.Notebook, status_var: tk.StringVar) -> None: + """Buttons, checkboxes, radio buttons.""" + tab = ttk.Frame(nb, padding=10) + nb.add(tab, text="Basic Controls") + + # --- Buttons ------------------------------------------------------------ + btn_frame = _section(tab, "Buttons") + btn_frame.pack(fill="x", pady=4) + + ttk.Button( + btn_frame, + text="Click Me", + name="btn_click_me", + command=lambda: status_var.set("Button 'Click Me' pressed"), + ).grid(row=0, column=0, padx=4, pady=4) + + ttk.Button( + btn_frame, + text="Action Button", + name="btn_action", + command=lambda: status_var.set("Button 'Action Button' pressed"), + ).grid(row=0, column=1, padx=4, pady=4) + + disabled_btn = ttk.Button(btn_frame, text="Disabled Button", name="btn_disabled") + disabled_btn.state(["disabled"]) + disabled_btn.grid(row=0, column=2, padx=4, pady=4) + + # --- Checkboxes --------------------------------------------------------- + chk_frame = _section(tab, "Checkboxes") + chk_frame.pack(fill="x", pady=4) + + chk_checked_var = tk.BooleanVar(value=True) + ttk.Checkbutton( + chk_frame, + text="Checked", + name="chk_checked", + variable=chk_checked_var, + command=lambda: status_var.set(f"Checked: {chk_checked_var.get()}"), + ).grid(row=0, column=0, sticky="w", padx=4, pady=2) + + chk_unchecked_var = tk.BooleanVar(value=False) + ttk.Checkbutton( + chk_frame, + text="Unchecked", + name="chk_unchecked", + variable=chk_unchecked_var, + command=lambda: status_var.set(f"Unchecked: {chk_unchecked_var.get()}"), + ).grid(row=1, column=0, sticky="w", padx=4, pady=2) + + # Tri-state / indeterminate checkbox — tkinter represents this with a + # StringVar where "" means indeterminate, "1" = on, "0" = off. + chk_tri_var = tk.StringVar(value="") + chk_tri = ttk.Checkbutton( + chk_frame, + text="Indeterminate (tri-state)", + name="chk_indeterminate", + variable=chk_tri_var, + onvalue="1", + offvalue="0", + command=lambda: status_var.set(f"Tri-state value: '{chk_tri_var.get()}'"), + ) + chk_tri.state(["alternate"]) # start in indeterminate visual state + chk_tri.grid(row=2, column=0, sticky="w", padx=4, pady=2) + + # --- Radio buttons ------------------------------------------------------ + radio_frame = _section(tab, "Radio Buttons") + radio_frame.pack(fill="x", pady=4) + + radio_var = tk.StringVar(value="Option A") + for col, opt in enumerate(("Option A", "Option B", "Option C")): + ttk.Radiobutton( + radio_frame, + text=opt, + name=f"radio_{opt.replace(' ', '_').lower()}", + variable=radio_var, + value=opt, + command=lambda v=opt: status_var.set(f"Radio selected: {v}"), + ).grid(row=0, column=col, padx=8) + + +def _build_tab_input(nb: ttk.Notebook, status_var: tk.StringVar) -> None: + """Entry, combobox, slider, spinbox.""" + tab = ttk.Frame(nb, padding=10) + nb.add(tab, text="Input Controls") + + # --- Text entry --------------------------------------------------------- + entry_frame = _section(tab, "Text Entry") + entry_frame.pack(fill="x", pady=4) + + ttk.Label(entry_frame, text="Single-line:").grid(row=0, column=0, sticky="w", padx=4) + entry_single = ttk.Entry(entry_frame, name="entry_single", width=30) + entry_single.insert(0, "Hello RPA") + entry_single.grid(row=0, column=1, padx=4, pady=2) + + ttk.Label(entry_frame, text="Password:").grid(row=1, column=0, sticky="w", padx=4) + entry_pwd = ttk.Entry(entry_frame, name="entry_password", show="*", width=30) + entry_pwd.grid(row=1, column=1, padx=4, pady=2) + + ttk.Label(entry_frame, text="Multi-line:").grid(row=2, column=0, sticky="nw", padx=4) + text_multi = tk.Text(entry_frame, name="text_multiline", width=30, height=4) + text_multi.insert("1.0", "Line 1\nLine 2\n") + text_multi.grid(row=2, column=1, padx=4, pady=2) + + # --- Combobox ----------------------------------------------------------- + combo_frame = _section(tab, "Combobox") + combo_frame.pack(fill="x", pady=4) + + ttk.Label(combo_frame, text="Select fruit:").grid(row=0, column=0, sticky="w", padx=4) + combo = ttk.Combobox( + combo_frame, + name="combo_fruit", + values=["Apple", "Banana", "Cherry", "Date", "Elderberry"], + state="readonly", + width=20, + ) + combo.set("Apple") + combo.grid(row=0, column=1, padx=4, pady=4) + combo.bind( + "<>", + lambda e: status_var.set(f"Combo selected: {combo.get()}"), + ) + + # --- Slider ------------------------------------------------------------- + slider_frame = _section(tab, "Slider") + slider_frame.pack(fill="x", pady=4) + + slider_var = tk.IntVar(value=50) + ttk.Label(slider_frame, text="Volume:").grid(row=0, column=0, sticky="w", padx=4) + slider = ttk.Scale( + slider_frame, + name="slider_volume", + from_=0, + to=100, + orient="horizontal", + variable=slider_var, + length=200, + command=lambda v: status_var.set(f"Slider: {int(float(v))}"), + ) + slider.grid(row=0, column=1, padx=4, pady=4) + slider_label = ttk.Label(slider_frame, textvariable=slider_var, width=4) + slider_label.grid(row=0, column=2, padx=4) + + # --- Spinbox ------------------------------------------------------------ + spin_frame = _section(tab, "Spinbox") + spin_frame.pack(fill="x", pady=4) + + ttk.Label(spin_frame, text="Quantity:").grid(row=0, column=0, sticky="w", padx=4) + spin = ttk.Spinbox( + spin_frame, + name="spin_quantity", + from_=0, + to=99, + width=8, + command=lambda: status_var.set(f"Spinbox: {spin.get()}"), + ) + spin.set(5) + spin.grid(row=0, column=1, padx=4, pady=4) + + +def _build_tab_list(nb: ttk.Notebook, status_var: tk.StringVar) -> None: + """Listbox and treeview table.""" + tab = ttk.Frame(nb, padding=10) + nb.add(tab, text="List & Table") + + # --- Listbox ------------------------------------------------------------ + list_frame = _section(tab, "Listbox") + list_frame.pack(fill="x", pady=4) + + listbox = tk.Listbox(list_frame, name="listbox_items", height=5, selectmode="single") + for item in ("Alpha", "Beta", "Gamma", "Delta", "Epsilon"): + listbox.insert(tk.END, item) + listbox.select_set(0) + listbox.pack(side="left", fill="both", expand=True, padx=4, pady=4) + listbox.bind( + "<>", + lambda e: status_var.set( + f"Listbox: {listbox.get(listbox.curselection()[0])}" + if listbox.curselection() + else "" + ), + ) + + sb = ttk.Scrollbar(list_frame, orient="vertical", command=listbox.yview) + listbox.configure(yscrollcommand=sb.set) + sb.pack(side="right", fill="y") + + # --- Treeview table ----------------------------------------------------- + tree_frame = _section(tab, "Table (Treeview)") + tree_frame.pack(fill="both", expand=True, pady=4) + + columns = ("name", "department", "salary") + tree = ttk.Treeview( + tree_frame, + name="tree_employees", + columns=columns, + show="headings", + height=5, + ) + for col, heading in zip(columns, ("Name", "Department", "Salary")): + tree.heading(col, text=heading) + tree.column(col, width=120) + + rows = [ + ("Alice", "Engineering", "90000"), + ("Bob", "Marketing", "75000"), + ("Carol", "Engineering", "95000"), + ("Dave", "HR", "65000"), + ("Eve", "Engineering", "88000"), + ] + for row in rows: + tree.insert("", tk.END, values=row) + + tree.pack(fill="both", expand=True, padx=4, pady=4) + tree.bind( + "<>", + lambda e: status_var.set( + f"Table row: {tree.item(tree.focus(), 'values')}" + ), + ) + + +def _build_tab_dynamic(nb: ttk.Notebook, status_var: tk.StringVar) -> None: + """Controls that appear/disappear on demand — useful for testing timeout.""" + tab = ttk.Frame(nb, padding=10) + nb.add(tab, text="Dynamic Controls") + + dyn_frame = _section(tab, "Delayed Element") + dyn_frame.pack(fill="x", pady=4) + + placeholder = ttk.Frame(dyn_frame, name="dynamic_placeholder", height=40) + placeholder.pack(fill="x", padx=4, pady=4) + + _hidden_label: list[ttk.Label] = [] + + def _show_label(delay_ms: int) -> None: + if _hidden_label: + _hidden_label[0].destroy() + _hidden_label.clear() + status_var.set(f"Label will appear in {delay_ms // 1000}s...") + + def _create() -> None: + lbl = ttk.Label( + placeholder, + name="dynamic_label", + text="I appeared after the delay!", + ) + lbl.pack() + _hidden_label.append(lbl) + status_var.set("Dynamic label is now visible.") + + tab.after(delay_ms, _create) + + def _hide_label() -> None: + if _hidden_label: + _hidden_label[0].destroy() + _hidden_label.clear() + status_var.set("Dynamic label hidden.") + + btn_row = ttk.Frame(dyn_frame) + btn_row.pack(padx=4, pady=4) + ttk.Button(btn_row, text="Show after 1s", + command=lambda: _show_label(1000)).pack(side="left", padx=4) + ttk.Button(btn_row, text="Show after 3s", + command=lambda: _show_label(3000)).pack(side="left", padx=4) + ttk.Button(btn_row, text="Hide", + command=_hide_label).pack(side="left", padx=4) + + info = ttk.Label( + dyn_frame, + text=( + "Use 'Show after Xs' then locate 'name:dynamic_label' with a matching\n" + "timeout to exercise the path: timeout fix (issue #1125)." + ), + foreground="gray", + ) + info.pack(padx=4, pady=4) + + +# --------------------------------------------------------------------------- +# Main window +# --------------------------------------------------------------------------- + +def build_app() -> tk.Tk: + root = tk.Tk() + root.title("RPA Windows Test App") + root.resizable(True, True) + root.minsize(520, 440) + + # Main notebook + nb = ttk.Notebook(root, name="notebook_main") + nb.pack(fill="both", expand=True, padx=8, pady=8) + + # Status bar + status_var = tk.StringVar(value="Ready") + status_bar = ttk.Label( + root, + name="lbl_status", + textvariable=status_var, + relief="sunken", + anchor="w", + padding=(4, 2), + ) + status_bar.pack(fill="x", side="bottom") + + # Build tabs + _build_tab_basic(nb, status_var) + _build_tab_input(nb, status_var) + _build_tab_list(nb, status_var) + _build_tab_dynamic(nb, status_var) + + # Quit button + ttk.Button( + root, + text="Quit", + name="btn_quit", + command=root.destroy, + ).pack(side="bottom", pady=4) + + return root + + +if __name__ == "__main__": + app = build_app() + app.mainloop() diff --git a/packages/windows/tests/robot/test_windows.robot b/packages/windows/tests/robot/test_windows.robot index 35ed9e8641..46c93b2f7c 100644 --- a/packages/windows/tests/robot/test_windows.robot +++ b/packages/windows/tests/robot/test_windows.robot @@ -16,6 +16,9 @@ ${RESULTS} ${CURDIR}${/}..${/}results ${EXE_UIDEMO} UIDemo.exe ${EXE_CALCULATOR} calc.exe ${EXE_SPOTIFY} Spotify.exe +${EXE_TESTAPP} python ${RESOURCES}${/}testapp.py + +${LOC_TESTAPP} name:"RPA Windows Test App" ${LOC_NOTEPAD} name:Notepad class:Notepad ${LOC_CALCULATOR} subname:Calc control:WindowControl @@ -484,3 +487,128 @@ Click and set values in WordPad Should Be Equal ${text} This i\rs my test text.\r\r\r2nd line text.\r\r [Teardown] Close Current Window + + +# --------------------------------------------------------------------------- +# Test App — testapp.py +# --------------------------------------------------------------------------- + +Start Test App + [Documentation] Launch the local testapp.py and return its window element. + Start Process python ${RESOURCES}${/}testapp.py alias=testapp + Sleep 1.5s + ${win} = Control Window ${LOC_TESTAPP} + RETURN ${win} + +Stop Test App + Send Signal To Process TERM handle=testapp group=${True} + Wait For Process handle=testapp timeout=5s on_timeout=kill + + +Test App Checkboxes + [Documentation] Verify Get Checkbox State returns correct values for + ... checked, unchecked and indeterminate checkboxes (issue #1166). + [Setup] Start Test App + + # Navigate to the "Basic Controls" tab (it is the first tab, already active). + Control Window ${LOC_TESTAPP} + + # Checked checkbox — should return True + ${state} = Get Checkbox State name:"RPA Windows Test App" > name:Checked + Should Be True ${state} msg=Expected 'Checked' checkbox to be True + + # Unchecked checkbox — should return False + ${state} = Get Checkbox State name:"RPA Windows Test App" > name:Unchecked + Should Be True ${state} == ${False} msg=Expected 'Unchecked' checkbox to be False + + # Indeterminate checkbox — should return None + ${state} = Get Checkbox State + ... name:"RPA Windows Test App" > name:"Indeterminate (tri-state)" + Should Be Equal ${state} ${None} msg=Expected indeterminate checkbox to be None + + [Teardown] Stop Test App + +Test App Radio Buttons + [Documentation] Verify Is Selected works on radio buttons inside the test app. + [Setup] Start Test App + + Control Window ${LOC_TESTAPP} + + # "Option A" starts selected + ${selected} = Is Selected name:"RPA Windows Test App" > name:"Option A" + Should Be True ${selected} + + # "Option B" starts unselected + ${selected} = Is Selected name:"RPA Windows Test App" > name:"Option B" + Should Be True ${selected} == ${False} + + [Teardown] Stop Test App + +Test App Text Entry + [Documentation] Set and get values in single-line text entry. + [Setup] Start Test App + + Control Window ${LOC_TESTAPP} + + # Clear and set a new value + Set Value name:"RPA Windows Test App" > name:"Single-line:" New text value + ${val} = Get Value name:"RPA Windows Test App" > name:"Single-line:" + Should Contain ${val} New text value + + [Teardown] Stop Test App + +Test App Combobox + [Documentation] Select a value from the combobox and verify the status bar. + [Setup] Start Test App + + Control Window ${LOC_TESTAPP} + + # Switch to the "Input Controls" tab + Click name:"RPA Windows Test App" > name:"Input Controls" + + Select name:"RPA Windows Test App" > name:"Select fruit:" Banana + ${status} = Get Attribute name:"RPA Windows Test App" > name:lbl_status Name + Should Contain ${status} Banana + + [Teardown] Stop Test App + +Test App Slider + [Documentation] Move the slider and verify its value changes. + [Setup] Start Test App + + Control Window ${LOC_TESTAPP} + + Click name:"RPA Windows Test App" > name:"Input Controls" + + ${slider} = Get Element name:"RPA Windows Test App" > name:slider_volume + Set Value ${slider} 75 + ${val} = Get Value ${slider} + Should Be True ${val} >= 70 + + [Teardown] Stop Test App + +Test App Path Locator Timeout + [Documentation] Trigger a delayed element (3 s) and assert that a locator with + ... timeout=5 finds it while timeout=1 raises ElementNotFound + ... (exercises the path: timeout fix, issue #1125). + [Setup] Start Test App + + Control Window ${LOC_TESTAPP} + + Click name:"RPA Windows Test App" > name:"Dynamic Controls" + + # The label should NOT exist yet — a short timeout must raise quickly. + ${result} = Run Keyword And Ignore Error + ... Get Element + ... name:"RPA Windows Test App" > name:dynamic_label + ... timeout=0.5 + Should Be Equal ${result}[0] FAIL msg=Short timeout should have failed + + # Now show after 3 s delay, then find it within 5 s timeout. + Click name:"RPA Windows Test App" > name:"Show after 3s" + ${elem} = Get Element + ... name:"RPA Windows Test App" > name:dynamic_label + ... timeout=5 + Should Not Be Equal ${elem} ${None} + + [Teardown] Stop Test App From 650a2628a215b13f8ebb05a5b561e99a30815fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Mon, 9 Mar 2026 15:27:32 +0200 Subject: [PATCH 05/21] fix(windows): fix test app robot locators and process teardown - Replace POSIX-only 'Send Signal To Process group=${True}' with 'Terminate Process' which works on Windows - Fix text entry locator to use type:EditControl within the LabelFrame group instead of the adjacent label name - Fix combobox test to locate by current value ("Apple") and verify with Get Value instead of checking status bar via widget path name - Fix slider test to use type:SliderControl within the Slider group Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- .../windows/tests/robot/test_windows.robot | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/windows/tests/robot/test_windows.robot b/packages/windows/tests/robot/test_windows.robot index 46c93b2f7c..c62758144e 100644 --- a/packages/windows/tests/robot/test_windows.robot +++ b/packages/windows/tests/robot/test_windows.robot @@ -501,7 +501,7 @@ Start Test App RETURN ${win} Stop Test App - Send Signal To Process TERM handle=testapp group=${True} + Terminate Process handle=testapp Wait For Process handle=testapp timeout=5s on_timeout=kill @@ -546,19 +546,27 @@ Test App Radio Buttons Test App Text Entry [Documentation] Set and get values in single-line text entry. + ... The entry is found by type:EditControl within the "Text Entry" + ... LabelFrame group — the tkinter widget name ("entry_single") is + ... a tk path component, not the UIAutomation Name property. [Setup] Start Test App Control Window ${LOC_TESTAPP} - # Clear and set a new value - Set Value name:"RPA Windows Test App" > name:"Single-line:" New text value - ${val} = Get Value name:"RPA Windows Test App" > name:"Single-line:" + # The LabelFrame "Text Entry" is a Group with Name="Text Entry". + # The first EditControl inside it is the single-line entry. + ${entry} = Get Element + ... name:"RPA Windows Test App" > name:"Text Entry" > type:EditControl + Set Value ${entry} New text value + ${val} = Get Value ${entry} Should Contain ${val} New text value [Teardown] Stop Test App Test App Combobox - [Documentation] Select a value from the combobox and verify the status bar. + [Documentation] Select a value from the combobox and confirm via Get Value. + ... The combobox UIAutomation Name is its currently selected item, + ... so it is located by initial value "Apple" before the Select. [Setup] Start Test App Control Window ${LOC_TESTAPP} @@ -566,21 +574,27 @@ Test App Combobox # Switch to the "Input Controls" tab Click name:"RPA Windows Test App" > name:"Input Controls" - Select name:"RPA Windows Test App" > name:"Select fruit:" Banana - ${status} = Get Attribute name:"RPA Windows Test App" > name:lbl_status Name - Should Contain ${status} Banana + # Combobox UIAutomation Name = currently selected value ("Apple" on start). + Select name:"RPA Windows Test App" > name:Apple Banana + # After selection the combobox Name changes to "Banana" — locate and confirm. + ${elem} = Get Element name:"RPA Windows Test App" > name:Banana + ${val} = Get Value ${elem} + Should Be Equal ${val} Banana [Teardown] Stop Test App Test App Slider [Documentation] Move the slider and verify its value changes. + ... The slider is found by type:SliderControl within the "Slider" + ... LabelFrame group. [Setup] Start Test App Control Window ${LOC_TESTAPP} Click name:"RPA Windows Test App" > name:"Input Controls" - ${slider} = Get Element name:"RPA Windows Test App" > name:slider_volume + ${slider} = Get Element + ... name:"RPA Windows Test App" > name:Slider > type:SliderControl Set Value ${slider} 75 ${val} = Get Value ${slider} Should Be True ${val} >= 70 From 79c0299a40838c21e8ff14fd48429a440d2c4ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Mon, 9 Mar 2026 15:39:04 +0200 Subject: [PATCH 06/21] fix(windows): remove invalid scalar variable and mark uncertain slider test manual MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove \${EXE_TESTAPP} which used two cells in a scalar variable definition — this is a parse error in Robot Framework 5 - Mark Test App Slider as manual since ttk.Scale UIAutomation name needs verification with Accessibility Insights on Windows Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- packages/windows/tests/robot/test_windows.robot | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/windows/tests/robot/test_windows.robot b/packages/windows/tests/robot/test_windows.robot index c62758144e..86f0789a81 100644 --- a/packages/windows/tests/robot/test_windows.robot +++ b/packages/windows/tests/robot/test_windows.robot @@ -16,8 +16,6 @@ ${RESULTS} ${CURDIR}${/}..${/}results ${EXE_UIDEMO} UIDemo.exe ${EXE_CALCULATOR} calc.exe ${EXE_SPOTIFY} Spotify.exe -${EXE_TESTAPP} python ${RESOURCES}${/}testapp.py - ${LOC_TESTAPP} name:"RPA Windows Test App" ${LOC_NOTEPAD} name:Notepad class:Notepad @@ -585,14 +583,17 @@ Test App Combobox Test App Slider [Documentation] Move the slider and verify its value changes. - ... The slider is found by type:SliderControl within the "Slider" - ... LabelFrame group. + ... The ttk.Scale widget is a SliderControl in UIAutomation. + ... The exact locator depends on how Windows exposes the widget name; + ... update the locator after inspecting with Accessibility Insights. + [Tags] manual [Setup] Start Test App Control Window ${LOC_TESTAPP} Click name:"RPA Windows Test App" > name:"Input Controls" + # Locate the slider within the "Slider" LabelFrame (Group) by control type. ${slider} = Get Element ... name:"RPA Windows Test App" > name:Slider > type:SliderControl Set Value ${slider} 75 From a242ab978ffc2b3a07c9cff85e84f23bf0709983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Sat, 2 May 2026 13:31:17 +0300 Subject: [PATCH 07/21] Update test_windows.robot --- packages/windows/tests/robot/test_windows.robot | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/windows/tests/robot/test_windows.robot b/packages/windows/tests/robot/test_windows.robot index 86f0789a81..a27d831b79 100644 --- a/packages/windows/tests/robot/test_windows.robot +++ b/packages/windows/tests/robot/test_windows.robot @@ -487,6 +487,9 @@ Click and set values in WordPad [Teardown] Close Current Window +*** Keywords *** + + # --------------------------------------------------------------------------- # Test App — testapp.py # --------------------------------------------------------------------------- From 7512446231937b6e77697bf00917d917f07d1d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Sat, 2 May 2026 13:33:56 +0300 Subject: [PATCH 08/21] Update test_windows.robot --- .../windows/tests/robot/test_windows.robot | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/windows/tests/robot/test_windows.robot b/packages/windows/tests/robot/test_windows.robot index a27d831b79..3fa80d9bcb 100644 --- a/packages/windows/tests/robot/test_windows.robot +++ b/packages/windows/tests/robot/test_windows.robot @@ -118,6 +118,21 @@ Close Current Window And Sleep Close Current Window Sleep 1s +# --------------------------------------------------------------------------- +# Test App — testapp.py +# --------------------------------------------------------------------------- + +Start Test App + [Documentation] Launch the local testapp.py and return its window element. + Start Process python ${RESOURCES}${/}testapp.py alias=testapp + Sleep 1.5s + ${win} = Control Window ${LOC_TESTAPP} + RETURN ${win} + +Stop Test App + Terminate Process handle=testapp + Wait For Process handle=testapp timeout=5s on_timeout=kill + *** Test Cases *** Windows search Calculator by clicking buttons @@ -486,24 +501,7 @@ Click and set values in WordPad [Teardown] Close Current Window - -*** Keywords *** - - -# --------------------------------------------------------------------------- -# Test App — testapp.py -# --------------------------------------------------------------------------- - -Start Test App - [Documentation] Launch the local testapp.py and return its window element. - Start Process python ${RESOURCES}${/}testapp.py alias=testapp - Sleep 1.5s - ${win} = Control Window ${LOC_TESTAPP} - RETURN ${win} - -Stop Test App - Terminate Process handle=testapp - Wait For Process handle=testapp timeout=5s on_timeout=kill +*** Test Cases *** Test App Checkboxes From cb49e4a2f755afdc878170b1f2e8e021c3e76c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Sat, 2 May 2026 13:49:45 +0300 Subject: [PATCH 09/21] Update locators.py --- packages/core/src/RPA/core/windows/locators.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/RPA/core/windows/locators.py b/packages/core/src/RPA/core/windows/locators.py index 5d6a9f73fa..db55db24c2 100644 --- a/packages/core/src/RPA/core/windows/locators.py +++ b/packages/core/src/RPA/core/windows/locators.py @@ -186,8 +186,8 @@ class MatchObject: } TREE_SEP = " > " PATH_SEP = "|" # path locator index separator - QUOTE = '"' # enclosing quote character - _LOCATOR_REGEX = re.compile(rf"\S*{QUOTE}[^{QUOTE}]+{QUOTE}|\S+", re.IGNORECASE) + QUOTE = '"' # enclosing quote character (double-quote; single-quote also accepted) + _LOCATOR_REGEX = re.compile(r"""\S*"[^"]+"|\S*'[^']+'|\S+""", re.IGNORECASE) _LOGGER = logging.getLogger(__name__) locators: List[Tuple] = field(default_factory=list) @@ -245,7 +245,7 @@ def handle_locator_part( default_values.append(part_text) def add_locator(self, control_strategy: str, value: str, level: int = 0) -> None: - value = value.strip(f"{self.QUOTE} ") + value = value.strip("\"' ") if not value: return From 0f886bf44e59c8745b428fe833d22759079219d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Sat, 2 May 2026 13:56:37 +0300 Subject: [PATCH 10/21] Update test_windows.robot --- .../windows/tests/robot/test_windows.robot | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/windows/tests/robot/test_windows.robot b/packages/windows/tests/robot/test_windows.robot index 3fa80d9bcb..4ea709d4b0 100644 --- a/packages/windows/tests/robot/test_windows.robot +++ b/packages/windows/tests/robot/test_windows.robot @@ -513,16 +513,15 @@ Test App Checkboxes Control Window ${LOC_TESTAPP} # Checked checkbox — should return True - ${state} = Get Checkbox State name:"RPA Windows Test App" > name:Checked + ${state} = Get Checkbox State name:Checked Should Be True ${state} msg=Expected 'Checked' checkbox to be True # Unchecked checkbox — should return False - ${state} = Get Checkbox State name:"RPA Windows Test App" > name:Unchecked + ${state} = Get Checkbox State name:Unchecked Should Be True ${state} == ${False} msg=Expected 'Unchecked' checkbox to be False # Indeterminate checkbox — should return None - ${state} = Get Checkbox State - ... name:"RPA Windows Test App" > name:"Indeterminate (tri-state)" + ${state} = Get Checkbox State name:"Indeterminate (tri-state)" Should Be Equal ${state} ${None} msg=Expected indeterminate checkbox to be None [Teardown] Stop Test App @@ -534,11 +533,11 @@ Test App Radio Buttons Control Window ${LOC_TESTAPP} # "Option A" starts selected - ${selected} = Is Selected name:"RPA Windows Test App" > name:"Option A" + ${selected} = Is Selected name:"Option A" Should Be True ${selected} # "Option B" starts unselected - ${selected} = Is Selected name:"RPA Windows Test App" > name:"Option B" + ${selected} = Is Selected name:"Option B" Should Be True ${selected} == ${False} [Teardown] Stop Test App @@ -555,7 +554,7 @@ Test App Text Entry # The LabelFrame "Text Entry" is a Group with Name="Text Entry". # The first EditControl inside it is the single-line entry. ${entry} = Get Element - ... name:"RPA Windows Test App" > name:"Text Entry" > type:EditControl + ... name:"Text Entry" > type:EditControl Set Value ${entry} New text value ${val} = Get Value ${entry} Should Contain ${val} New text value @@ -571,12 +570,12 @@ Test App Combobox Control Window ${LOC_TESTAPP} # Switch to the "Input Controls" tab - Click name:"RPA Windows Test App" > name:"Input Controls" + Click name:"Input Controls" # Combobox UIAutomation Name = currently selected value ("Apple" on start). - Select name:"RPA Windows Test App" > name:Apple Banana + Select name:Apple Banana # After selection the combobox Name changes to "Banana" — locate and confirm. - ${elem} = Get Element name:"RPA Windows Test App" > name:Banana + ${elem} = Get Element name:Banana ${val} = Get Value ${elem} Should Be Equal ${val} Banana @@ -592,11 +591,11 @@ Test App Slider Control Window ${LOC_TESTAPP} - Click name:"RPA Windows Test App" > name:"Input Controls" + Click name:"Input Controls" # Locate the slider within the "Slider" LabelFrame (Group) by control type. ${slider} = Get Element - ... name:"RPA Windows Test App" > name:Slider > type:SliderControl + ... name:Slider > type:SliderControl Set Value ${slider} 75 ${val} = Get Value ${slider} Should Be True ${val} >= 70 @@ -611,19 +610,19 @@ Test App Path Locator Timeout Control Window ${LOC_TESTAPP} - Click name:"RPA Windows Test App" > name:"Dynamic Controls" + Click name:"Dynamic Controls" # The label should NOT exist yet — a short timeout must raise quickly. ${result} = Run Keyword And Ignore Error ... Get Element - ... name:"RPA Windows Test App" > name:dynamic_label + ... name:dynamic_label ... timeout=0.5 Should Be Equal ${result}[0] FAIL msg=Short timeout should have failed # Now show after 3 s delay, then find it within 5 s timeout. - Click name:"RPA Windows Test App" > name:"Show after 3s" + Click name:"Show after 3s" ${elem} = Get Element - ... name:"RPA Windows Test App" > name:dynamic_label + ... name:dynamic_label ... timeout=5 Should Not Be Equal ${elem} ${None} From 5996d0337e70e4418c3e7c452b386b1910f0cc55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20H=C3=A4nninen?= Date: Sat, 2 May 2026 14:07:38 +0300 Subject: [PATCH 11/21] Update test_windows.robot --- packages/windows/tests/robot/test_windows.robot | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/windows/tests/robot/test_windows.robot b/packages/windows/tests/robot/test_windows.robot index 4ea709d4b0..f9626f9a01 100644 --- a/packages/windows/tests/robot/test_windows.robot +++ b/packages/windows/tests/robot/test_windows.robot @@ -10,7 +10,7 @@ Default Tags windows skip *** Variables *** -${RESOURCES} ${CURDIR}${/}..${/}resources +${RESOURCES} ${CURDIR}${/}..${/}..${/}resources ${RESULTS} ${CURDIR}${/}..${/}results ${EXE_UIDEMO} UIDemo.exe @@ -135,6 +135,16 @@ Stop Test App *** Test Cases *** +Test App Inspect Tree + [Documentation] Print the UIAutomation tree of the test app for locator discovery. + [Tags] manual + [Setup] Start Test App + + Control Window ${LOC_TESTAPP} + Print Tree log_as_warnings=${True} + + [Teardown] Stop Test App + Windows search Calculator by clicking buttons Windows Search Calculator wait_time=1 Calculator button actions From ba83ceff1b077c45eea5a7cebc3003801c3ae2a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Sat, 2 May 2026 14:09:57 +0300 Subject: [PATCH 12/21] feat(windows): add wxPython test app and mark tkinter tests as manual tkinter widgets do not expose UIAutomation accessibility attributes (all controls appear as unnamed PaneControl), making them unusable for RPA.Windows keyword testing. - Add testapp_wx.py: wxPython equivalent of testapp.py using native Win32 controls that properly expose CheckBoxControl + TogglePattern, ButtonControl, and TextControl via UIAutomation - Add Test App WX Checkboxes: automated test for Get Checkbox State against all three toggle states (True / False / None) - Add Test App WX Path Locator Timeout: automated test for the path: locator timeout fix (issue #1125) using wx.CallLater delayed element - Add wxPython>=4.2.0 (win32 only) to dev dependency group - Mark all tkinter testapp tests as manual with explanation Co-Authored-By: Claude Sonnet 4.6 --- packages/windows/pyproject.toml | 1 + packages/windows/resources/testapp_wx.py | 118 ++++++++++++++++++ .../windows/tests/robot/test_windows.robot | 87 ++++++++++++- 3 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 packages/windows/resources/testapp_wx.py diff --git a/packages/windows/pyproject.toml b/packages/windows/pyproject.toml index ed57861698..073fc5f107 100644 --- a/packages/windows/pyproject.toml +++ b/packages/windows/pyproject.toml @@ -44,6 +44,7 @@ repository = "https://github.com/robocorp/rpaframework" [dependency-groups] dev = [ "rpaframework-devutils", + "wxPython>=4.2.0; sys_platform == 'win32'", ] [tool.uv] diff --git a/packages/windows/resources/testapp_wx.py b/packages/windows/resources/testapp_wx.py new file mode 100644 index 0000000000..d14a869fba --- /dev/null +++ b/packages/windows/resources/testapp_wx.py @@ -0,0 +1,118 @@ +"""wxPython test application for RPA.Windows UIAutomation testing. + +Unlike tkinter, wxPython wraps native Win32 controls which properly expose +UIAutomation accessibility attributes (control types, names, patterns). + +Controls exposed: + + - wx.CheckBox → CheckBoxControl + TogglePattern (name = label text) + - wx.Button → ButtonControl + InvokePattern (name = label text) + - wx.StaticText → TextControl (name = label text) + +Window title: "RPA Windows Test App WX" + +Usage:: + + python testapp_wx.py +""" + +import wx + + +class TestFrame(wx.Frame): + def __init__(self): + super().__init__(None, title="RPA Windows Test App WX", size=(420, 400)) + panel = wx.Panel(self) + vbox = wx.BoxSizer(wx.VERTICAL) + + # ------------------------------------------------------------------ # + # Checkboxes + # ------------------------------------------------------------------ # + chk_box = wx.StaticBox(panel, label="Checkboxes") + chk_sizer = wx.StaticBoxSizer(chk_box, wx.VERTICAL) + + self.chk_checked = wx.CheckBox(panel, label="Checked") + self.chk_checked.SetValue(True) + + self.chk_unchecked = wx.CheckBox(panel, label="Unchecked") + self.chk_unchecked.SetValue(False) + + # wx.CHK_3STATE enables the indeterminate (middle) state. + # wx.CHK_ALLOW_3RD_STATE_FOR_USER lets the user cycle into it. + self.chk_indeterminate = wx.CheckBox( + panel, + label="Indeterminate", + style=wx.CHK_3STATE | wx.CHK_ALLOW_3RD_STATE_FOR_USER, + ) + self.chk_indeterminate.Set3StateValue(wx.CHK_UNDETERMINED) + + chk_sizer.Add(self.chk_checked, 0, wx.ALL, 6) + chk_sizer.Add(self.chk_unchecked, 0, wx.ALL, 6) + chk_sizer.Add(self.chk_indeterminate, 0, wx.ALL, 6) + vbox.Add(chk_sizer, 0, wx.EXPAND | wx.ALL, 10) + + # ------------------------------------------------------------------ # + # Dynamic element (for path-locator timeout testing) + # ------------------------------------------------------------------ # + dyn_box = wx.StaticBox(panel, label="Dynamic Controls") + dyn_sizer = wx.StaticBoxSizer(dyn_box, wx.VERTICAL) + + # Placeholder panel — the dynamic label is added/removed inside it. + self._dyn_inner = wx.Panel(panel) + self._dyn_inner_sizer = wx.BoxSizer(wx.VERTICAL) + self._dyn_inner.SetSizer(self._dyn_inner_sizer) + self._dynamic_label: wx.StaticText | None = None + + btn_row = wx.BoxSizer(wx.HORIZONTAL) + btn_show = wx.Button(panel, label="Show after 3s") + btn_show.Bind(wx.EVT_BUTTON, lambda _: self._schedule_label(3000)) + btn_hide = wx.Button(panel, label="Hide") + btn_hide.Bind(wx.EVT_BUTTON, lambda _: self._hide_label()) + btn_row.Add(btn_show, 0, wx.RIGHT, 6) + btn_row.Add(btn_hide, 0) + + dyn_sizer.Add(self._dyn_inner, 0, wx.EXPAND | wx.ALL, 4) + dyn_sizer.Add(btn_row, 0, wx.ALL, 4) + vbox.Add(dyn_sizer, 0, wx.EXPAND | wx.ALL, 10) + + # ------------------------------------------------------------------ # + # Quit + # ------------------------------------------------------------------ # + btn_quit = wx.Button(panel, label="Quit") + btn_quit.Bind(wx.EVT_BUTTON, lambda _: self.Close()) + vbox.Add(btn_quit, 0, wx.ALL | wx.ALIGN_CENTER, 10) + + panel.SetSizer(vbox) + self.Centre() + self.Show() + + # ---------------------------------------------------------------------- # + # Dynamic label helpers + # ---------------------------------------------------------------------- # + + def _schedule_label(self, delay_ms: int) -> None: + self._hide_label() + wx.CallLater(delay_ms, self._show_label) + + def _show_label(self) -> None: + if self._dynamic_label: + return + self._dynamic_label = wx.StaticText( + self._dyn_inner, label="DynamicContent" + ) + self._dyn_inner_sizer.Add(self._dynamic_label, 0, wx.ALL, 4) + self._dyn_inner.Layout() + self.Layout() + + def _hide_label(self) -> None: + if self._dynamic_label: + self._dynamic_label.Destroy() + self._dynamic_label = None + self._dyn_inner.Layout() + self.Layout() + + +if __name__ == "__main__": + app = wx.App(False) + TestFrame() + app.MainLoop() diff --git a/packages/windows/tests/robot/test_windows.robot b/packages/windows/tests/robot/test_windows.robot index f9626f9a01..fbd69d80b2 100644 --- a/packages/windows/tests/robot/test_windows.robot +++ b/packages/windows/tests/robot/test_windows.robot @@ -17,6 +17,7 @@ ${EXE_UIDEMO} UIDemo.exe ${EXE_CALCULATOR} calc.exe ${EXE_SPOTIFY} Spotify.exe ${LOC_TESTAPP} name:"RPA Windows Test App" +${LOC_TESTAPP_WX} name:"RPA Windows Test App WX" ${LOC_NOTEPAD} name:Notepad class:Notepad ${LOC_CALCULATOR} subname:Calc control:WindowControl @@ -514,9 +515,23 @@ Click and set values in WordPad *** Test Cases *** +Test App Inspect Tree + [Documentation] Print the UIAutomation tree of the test app for locator discovery. + [Tags] manual + [Setup] Start Test App + + Control Window ${LOC_TESTAPP} + Print Tree log_as_warnings=${True} + + [Teardown] Stop Test App + + Test App Checkboxes [Documentation] Verify Get Checkbox State returns correct values for ... checked, unchecked and indeterminate checkboxes (issue #1166). + ... NOTE: tkinter does not expose UIAutomation attributes — run + ... Test App WX Checkboxes for the automated version of this test. + [Tags] manual [Setup] Start Test App # Navigate to the "Basic Controls" tab (it is the first tab, already active). @@ -538,6 +553,7 @@ Test App Checkboxes Test App Radio Buttons [Documentation] Verify Is Selected works on radio buttons inside the test app. + [Tags] manual [Setup] Start Test App Control Window ${LOC_TESTAPP} @@ -554,9 +570,8 @@ Test App Radio Buttons Test App Text Entry [Documentation] Set and get values in single-line text entry. - ... The entry is found by type:EditControl within the "Text Entry" - ... LabelFrame group — the tkinter widget name ("entry_single") is - ... a tk path component, not the UIAutomation Name property. + ... NOTE: tkinter does not expose UIAutomation attributes — manual only. + [Tags] manual [Setup] Start Test App Control Window ${LOC_TESTAPP} @@ -573,8 +588,8 @@ Test App Text Entry Test App Combobox [Documentation] Select a value from the combobox and confirm via Get Value. - ... The combobox UIAutomation Name is its currently selected item, - ... so it is located by initial value "Apple" before the Select. + ... NOTE: tkinter does not expose UIAutomation attributes — manual only. + [Tags] manual [Setup] Start Test App Control Window ${LOC_TESTAPP} @@ -616,6 +631,9 @@ Test App Path Locator Timeout [Documentation] Trigger a delayed element (3 s) and assert that a locator with ... timeout=5 finds it while timeout=1 raises ElementNotFound ... (exercises the path: timeout fix, issue #1125). + ... NOTE: tkinter does not expose UIAutomation attributes — run + ... Test App WX Path Locator Timeout for the automated version. + [Tags] manual [Setup] Start Test App Control Window ${LOC_TESTAPP} @@ -637,3 +655,62 @@ Test App Path Locator Timeout Should Not Be Equal ${elem} ${None} [Teardown] Stop Test App + + +# --------------------------------------------------------------------------- +# WX Test App — testapp_wx.py (wxPython: proper UIAutomation exposure) +# --------------------------------------------------------------------------- + +Start Test App WX + [Documentation] Launch testapp_wx.py and set it as the active window. + Start Process python ${RESOURCES}${/}testapp_wx.py alias=testapp_wx + Sleep 2s + Control Window ${LOC_TESTAPP_WX} + +Stop Test App WX + Terminate Process handle=testapp_wx + Wait For Process handle=testapp_wx timeout=5s on_timeout=kill + + +*** Test Cases *** + + +Test App WX Checkboxes + [Documentation] Verify Get Checkbox State against the wxPython test app. + ... wxPython uses native Win32 controls that expose proper + ... UIAutomation TogglePattern (issue #1166). + [Setup] Start Test App WX + + # Checked checkbox — should return True + ${state} = Get Checkbox State name:Checked + Should Be True ${state} msg=Expected 'Checked' checkbox to be True + + # Unchecked checkbox — should return False + ${state} = Get Checkbox State name:Unchecked + Should Be True ${state} == ${False} msg=Expected 'Unchecked' checkbox to be False + + # Indeterminate (tri-state) checkbox — should return None + ${state} = Get Checkbox State name:Indeterminate + Should Be Equal ${state} ${None} msg=Expected indeterminate checkbox to be None + + [Teardown] Stop Test App WX + + +Test App WX Path Locator Timeout + [Documentation] Verify the path: locator timeout fix (issue #1125) using the + ... wxPython test app. "Show after 3s" creates a StaticText + ... control after a 3-second delay. A 0.5 s timeout must fail + ... fast; a 5 s timeout must succeed. + [Setup] Start Test App WX + + # Element does not exist yet — short timeout must fail quickly. + ${result} = Run Keyword And Ignore Error + ... Get Element name:DynamicContent timeout=0.5 + Should Be Equal ${result}[0] FAIL msg=Short timeout should have failed + + # Trigger the 3-second delayed element, then find it within 5 s. + Click name:"Show after 3s" + ${elem} = Get Element name:DynamicContent timeout=5 + Should Not Be Equal ${elem} ${None} + + [Teardown] Stop Test App WX From 4c54e539564d65ea4e8e84c3411ccaf17783f714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Sat, 2 May 2026 14:18:18 +0300 Subject: [PATCH 13/21] fix(windows): use uv run python to launch wxPython test app System python does not have wxPython installed; uv run python uses the venv where it is available via the dev dependency group. Co-Authored-By: Claude Sonnet 4.6 --- packages/windows/tests/robot/test_windows.robot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/windows/tests/robot/test_windows.robot b/packages/windows/tests/robot/test_windows.robot index fbd69d80b2..2d4fcbda5d 100644 --- a/packages/windows/tests/robot/test_windows.robot +++ b/packages/windows/tests/robot/test_windows.robot @@ -663,7 +663,7 @@ Test App Path Locator Timeout Start Test App WX [Documentation] Launch testapp_wx.py and set it as the active window. - Start Process python ${RESOURCES}${/}testapp_wx.py alias=testapp_wx + Start Process uv run python ${RESOURCES}${/}testapp_wx.py alias=testapp_wx Sleep 2s Control Window ${LOC_TESTAPP_WX} From 23692a18ac6e0ca141749ab2bf33949d511d8235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20H=C3=A4nninen?= Date: Sat, 2 May 2026 14:19:13 +0300 Subject: [PATCH 14/21] Update uv.lock --- packages/windows/uv.lock | 142 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/packages/windows/uv.lock b/packages/windows/uv.lock index 3c890ad498..d1a94f1025 100644 --- a/packages/windows/uv.lock +++ b/packages/windows/uv.lock @@ -1199,6 +1199,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" }, + { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +] + [[package]] name = "outcome" version = "1.3.0.post0" @@ -2134,6 +2206,8 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "rpaframework-devutils" }, + { name = "wxpython", version = "4.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "wxpython", version = "4.2.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, ] [package.metadata] @@ -2151,7 +2225,10 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "rpaframework-devutils", editable = "../devutils" }] +dev = [ + { name = "rpaframework-devutils", editable = "../devutils" }, + { name = "wxpython", marker = "sys_platform == 'win32'", specifier = ">=4.2.0" }, +] [[package]] name = "ruff" @@ -2595,6 +2672,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, ] +[[package]] +name = "wxpython" +version = "4.2.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/6e/b70e6dbdd7cb4f154b7ca424b4c7799f7b067f7a9f4204b8d16d6464648f/wxpython-4.2.4.tar.gz", hash = "sha256:2eb123979c87bcb329e8a2452269d60ff8f9f651e9bf25c67579e53c4ebbae3c", size = 58583054, upload-time = "2025-10-29T13:22:37.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/db/eb0c6734ebf244e31cbb041d5b9f8de98ba2c3aac2c106b583d67756d2c9/wxpython-4.2.4-cp310-cp310-win32.whl", hash = "sha256:2d020c0b0db1869d58ee646afd9cc1276520a77a9d800d407927bfee35678970", size = 14791506, upload-time = "2025-10-29T04:02:00.388Z" }, + { url = "https://files.pythonhosted.org/packages/11/56/a29cf6bfb3f049983697945dc2c1b576b0d10612e1f53bcec69204f72df3/wxpython-4.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:95d390e0bf4bd8c4c1d77e2205887a5bc84e950e4f44719d66f8d14f59c777eb", size = 16795426, upload-time = "2025-10-29T04:02:02.957Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/2c34ec1796592f4dc393a7153131a770e117054cd07e7ba1e5594c6078b2/wxpython-4.2.4-cp311-cp311-win32.whl", hash = "sha256:b86b0258074f4ec16b234274fa0c32c42e039124c9f6f36529091c9a00ebff4d", size = 14497499, upload-time = "2025-10-29T04:02:10.604Z" }, + { url = "https://files.pythonhosted.org/packages/4a/13/20311125881142802ca3f2fc743e82696dee562f26d7e35da649c26de7cc/wxpython-4.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:f48fe7b9f22c7733a06b5901559b5b7e4835fa852cbef0f620ffd0a0030aaf73", size = 16538730, upload-time = "2025-10-29T04:02:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/0c/35/b25b712097115ba734adafc26ac543840da90dc2f1b0c67257fed3d3ba63/wxpython-4.2.4-cp311-cp311-win_arm64.whl", hash = "sha256:fe83813241dfb94780f18dff1678d1ffd097116a8b8fea0272a332c387f69ef8", size = 15524815, upload-time = "2025-10-29T04:02:14.805Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1f/ddbb597c3d821d4206f85234736a4f19ef1b8875eefbf8d072ffcc01c1a4/wxpython-4.2.4-cp312-cp312-win32.whl", hash = "sha256:83e45e4d5d139638260c2f23108a94cb8d40bd5eb714d41b1009452e4ec4229a", size = 14507902, upload-time = "2025-10-29T04:02:22.309Z" }, + { url = "https://files.pythonhosted.org/packages/90/c3/dfe74d7eb046612a3e475dce8ffda70341516129a7cb17fd60c8ae143304/wxpython-4.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:c333113be1fcb4e4252890b3e66af016f03432b2fe15b370f38f2a505f5daa74", size = 16549996, upload-time = "2025-10-29T04:02:24.792Z" }, + { url = "https://files.pythonhosted.org/packages/bb/29/5286f960de8079264e7a89ca4b4beae86844520b9521a1497c4908a8cae6/wxpython-4.2.4-cp312-cp312-win_arm64.whl", hash = "sha256:88b7e8cbdb141ebb4e361cd6f3d5595160764f0c05d2d7e03506b53860eb01ab", size = 15534326, upload-time = "2025-10-29T04:02:27.421Z" }, + { url = "https://files.pythonhosted.org/packages/26/a4/57b0110944f62ce37c4e3e5f9f1a116b6df2b7b8a3406a90b95272c211ad/wxpython-4.2.4-cp313-cp313-win32.whl", hash = "sha256:b0920fe193e2bbd90c6d3fbb9a8101f1276229638aff29f66bfe521901c3a0b8", size = 14506941, upload-time = "2025-10-29T04:02:34.513Z" }, + { url = "https://files.pythonhosted.org/packages/77/d4/e1f63f1b4fe97a12f2dbe76feba678b2a35fd2d9d9c8c60ba94a271f01e8/wxpython-4.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:a0a532ee6e6a3ce0d1fb2e223159d4b8ec42f8a844c852da936fb23cba6b9335", size = 16549467, upload-time = "2025-10-29T04:02:36.666Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/df43d6fcbde2389894a068e7d41d44820a19f1bb7a78daa8fb687cd8f1a8/wxpython-4.2.4-cp313-cp313-win_arm64.whl", hash = "sha256:e82ab099df0b6c55bcfad198822648a6275bbb575ce09502a9085b290ec9852d", size = 15533173, upload-time = "2025-10-29T04:02:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/72/6e/62f3c7c2f509057888ad87d141c4c77d375cfb8e4ac820654d3b972b529f/wxpython-4.2.4-cp314-cp314-win32.whl", hash = "sha256:7e7c3baa2aab55f1c0a14fdd1b371f8ec0cff1cc2f6171497643f977b243f750", size = 14848210, upload-time = "2025-10-29T04:02:45.812Z" }, + { url = "https://files.pythonhosted.org/packages/27/e0/6d6762d2857963abc06f56fc14a605d1c69d83b0abf3a0702fcffb78bf3a/wxpython-4.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:dae6d80067f64498e530d2c5f47c81f5f1a6f32e820b3b59f05062c1629ff97d", size = 16918656, upload-time = "2025-10-29T04:02:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/d437e56efe7e1ec5bb7af59d6651913e18e3fa9ecdacdec7a129d5285baa/wxpython-4.2.4-cp314-cp314-win_arm64.whl", hash = "sha256:f7b649542969c2221d2963695de11c74c12060e83d412f4017ba03e7787c9c5d", size = 15807097, upload-time = "2025-10-29T04:02:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/a9/aa/37897138e3703c5560c0fbc225787faa1b08128f46f7d5e60f8f0102025c/wxpython-4.2.4-cp39-cp39-win32.whl", hash = "sha256:e31e645ed4f0b048954f230f9a3d8dce84054fe9161f2f3ceeb2bbf176126424", size = 14789341, upload-time = "2025-10-29T04:02:58.8Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/b0bd12d10041b063fa39c8adb2e35137e0c50c97e3c742f0061fead65725/wxpython-4.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:1771079f39cfe1fd52a2302bf7d59fad716b71cb5369c9423480b1a518a72afd", size = 16795520, upload-time = "2025-10-29T04:03:01.056Z" }, +] + +[[package]] +name = "wxpython" +version = "4.2.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/43/81657a6b126ffc19163500a8184d683cec08eb4e1d06905cd0c371c702d0/wxpython-4.2.5.tar.gz", hash = "sha256:44e836d1bccd99c38790bb034b6ecf70d9060f6734320560f7c4b0d006144793", size = 58732217, upload-time = "2026-02-08T20:40:42.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/4e/e6fb441868988c8f1eff9903fc3943387699e6cb4f03f09c37f935bb955c/wxpython-4.2.5-cp310-cp310-win32.whl", hash = "sha256:d7e3b69a1f2ad383e8305efee11130dbe7542b6f6d8d5ba02fdce3cfc4c67ad6", size = 14804965, upload-time = "2026-02-08T20:39:34.763Z" }, + { url = "https://files.pythonhosted.org/packages/6c/48/9d861888d3f9ff6d0559fe6578020d65fb5f5f8821be384a33d9331cfb4e/wxpython-4.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:74fc76f8d22226e607d7072278cc09052da0cda508dea14a9933b014a4e5aeeb", size = 16823187, upload-time = "2026-02-08T20:39:37.834Z" }, + { url = "https://files.pythonhosted.org/packages/26/b3/06405cbaf7168648c913c4737b810bccb6b434f0693832cbe621d0852827/wxpython-4.2.5-cp311-cp311-win32.whl", hash = "sha256:ade4d6b39c39770146ce1010df7d7a5a767d148b478133d49c36dffb0880a174", size = 14508676, upload-time = "2026-02-08T20:39:48.17Z" }, + { url = "https://files.pythonhosted.org/packages/dd/95/5b4928819f161fb4cb38a5b5a001abb8d66500b2399bf20f914215e8e17e/wxpython-4.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:508de82ac2b64aa575a543949c44dc7f83a734622bed01d8501474ab454f7bb7", size = 16566588, upload-time = "2026-02-08T20:39:51.49Z" }, + { url = "https://files.pythonhosted.org/packages/42/e3/64a16f064b188903c5ade786468bb1221879f500e0aae98acc70be9c0f7d/wxpython-4.2.5-cp311-cp311-win_arm64.whl", hash = "sha256:9bf5704eedd21bd1fe3143d8ed1cfe8250e1fbb651eb6c17a73533136745fe5a", size = 15530728, upload-time = "2026-02-08T20:39:54.533Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3a/5136bf39877640c8a254f1370279943366d04d321461a450fcef53722f38/wxpython-4.2.5-cp312-cp312-win32.whl", hash = "sha256:e7079d9a7374b3fd5896bdea7c73faa8da52e1fbcce5368796b5c22d7de747a6", size = 14519633, upload-time = "2026-02-08T20:40:03.577Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/2c2c9dbf78f9524daf79014337e193531332c3598b16ccc11290dad9c17f/wxpython-4.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:c54962f0524662d16591a03c786cd4d71bc43c70ede8244e0a5a59aa3979d124", size = 16577057, upload-time = "2026-02-08T20:40:06.002Z" }, + { url = "https://files.pythonhosted.org/packages/b4/10/a7ed092d0426cc98ab1a907c9e6161d8846e0de0450447b877048a8ef4e3/wxpython-4.2.5-cp312-cp312-win_arm64.whl", hash = "sha256:cda1fb351caa4555bd18717f610c9a3b03d25e64db4a22e004b141f91d02fa8c", size = 15537110, upload-time = "2026-02-08T20:40:09.022Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ea/a69ad0a1e7b01876619982b6cf8db0e26d0f3776b9be73b61d9f0662a2ce/wxpython-4.2.5-cp313-cp313-win32.whl", hash = "sha256:0985f190565b94635f146989886196a7e9faced8911800910460919cb72668cc", size = 14520419, upload-time = "2026-02-08T20:40:17.401Z" }, + { url = "https://files.pythonhosted.org/packages/a8/bd/d2698369dbc43aa5c9324c23fdd5cd3b23c245861e334b1d976209913f90/wxpython-4.2.5-cp313-cp313-win_amd64.whl", hash = "sha256:3fd3649fc4752f1a02776b7057073c932e5229bbab2031762b01532bcc6bd074", size = 16576741, upload-time = "2026-02-08T20:40:20.646Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4e/4181734a2bc05940ba4feb3feb2474416b1dc12c329a2ac164632582c4d6/wxpython-4.2.5-cp313-cp313-win_arm64.whl", hash = "sha256:b794d9912464990ea1fd3744fb73fbd7446149e230e5a611ba40eb4ac74755a1", size = 15537287, upload-time = "2026-02-08T20:40:24.658Z" }, + { url = "https://files.pythonhosted.org/packages/51/a7/261bf54686192ebefc42b494da97ba3312bd01c1d37a964f0cb83f271cf0/wxpython-4.2.5-cp314-cp314-win32.whl", hash = "sha256:230ecb4de65a8d2f8bc30bccd4d64366ac3a7cf53759b77920de927d156ad9c5", size = 14859697, upload-time = "2026-02-08T20:40:33.108Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9a/73f12041178db3728a809ce37c2b64409291cb45567b2918df478f0ceb20/wxpython-4.2.5-cp314-cp314-win_amd64.whl", hash = "sha256:eb1c228f0c20ed93f2799ebd81780abc7fd65cfa8f6b65e989b68c0c18c52707", size = 16947346, upload-time = "2026-02-08T20:40:35.583Z" }, + { url = "https://files.pythonhosted.org/packages/b6/49/3a39f5fe78a7194c848919c4b681f432fe937006e2e5182a17dd519f8c91/wxpython-4.2.5-cp314-cp314-win_arm64.whl", hash = "sha256:d4439bf4b18ac720afbcf51c37d7822ba62ab6999501e96cce1dfc2f55a19344", size = 15809574, upload-time = "2026-02-08T20:40:38.859Z" }, +] + [[package]] name = "xmltodict" version = "1.0.4" From 9aeff21f08e32b7e80d5d8d407fa2728cae9895f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20H=C3=A4nninen?= Date: Sat, 2 May 2026 14:20:10 +0300 Subject: [PATCH 15/21] Update test_windows.robot --- .../windows/tests/robot/test_windows.robot | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/windows/tests/robot/test_windows.robot b/packages/windows/tests/robot/test_windows.robot index 2d4fcbda5d..4604eb2509 100644 --- a/packages/windows/tests/robot/test_windows.robot +++ b/packages/windows/tests/robot/test_windows.robot @@ -135,6 +135,22 @@ Stop Test App Wait For Process handle=testapp timeout=5s on_timeout=kill +# --------------------------------------------------------------------------- +# WX Test App — testapp_wx.py (wxPython: proper UIAutomation exposure) +# --------------------------------------------------------------------------- + +Start Test App WX + [Documentation] Launch testapp_wx.py and set it as the active window. + Start Process uv run python ${RESOURCES}${/}testapp_wx.py alias=testapp_wx + Sleep 2s + Control Window ${LOC_TESTAPP_WX} + +Stop Test App WX + Terminate Process handle=testapp_wx + Wait For Process handle=testapp_wx timeout=5s on_timeout=kill + + + *** Test Cases *** Test App Inspect Tree [Documentation] Print the UIAutomation tree of the test app for locator discovery. @@ -657,22 +673,6 @@ Test App Path Locator Timeout [Teardown] Stop Test App -# --------------------------------------------------------------------------- -# WX Test App — testapp_wx.py (wxPython: proper UIAutomation exposure) -# --------------------------------------------------------------------------- - -Start Test App WX - [Documentation] Launch testapp_wx.py and set it as the active window. - Start Process uv run python ${RESOURCES}${/}testapp_wx.py alias=testapp_wx - Sleep 2s - Control Window ${LOC_TESTAPP_WX} - -Stop Test App WX - Terminate Process handle=testapp_wx - Wait For Process handle=testapp_wx timeout=5s on_timeout=kill - - -*** Test Cases *** Test App WX Checkboxes From ff737d958574469f4d07ca706061d9f87f0dc3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Sat, 2 May 2026 14:24:03 +0300 Subject: [PATCH 16/21] fix(windows): increase path timeout test margin to 8s wx.CallLater fires after 3s but UIAutomation may take a moment to pick up the new StaticText control. Add 0.5s sleep post-click and raise the Get Element timeout to 8s to give enough headroom. Co-Authored-By: Claude Sonnet 4.6 --- packages/windows/tests/robot/test_windows.robot | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/windows/tests/robot/test_windows.robot b/packages/windows/tests/robot/test_windows.robot index 4604eb2509..75b6261e9c 100644 --- a/packages/windows/tests/robot/test_windows.robot +++ b/packages/windows/tests/robot/test_windows.robot @@ -708,9 +708,10 @@ Test App WX Path Locator Timeout ... Get Element name:DynamicContent timeout=0.5 Should Be Equal ${result}[0] FAIL msg=Short timeout should have failed - # Trigger the 3-second delayed element, then find it within 5 s. + # Trigger the 3-second delayed element, then find it within 8 s. Click name:"Show after 3s" - ${elem} = Get Element name:DynamicContent timeout=5 + Sleep 0.5s + ${elem} = Get Element name:DynamicContent timeout=8 Should Not Be Equal ${elem} ${None} [Teardown] Stop Test App WX From f3d23449580be7b75445a23bcb38aeb393befc14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Sat, 2 May 2026 14:52:54 +0300 Subject: [PATCH 17/21] fix(windows): use disabled Button for dynamic element in wx test app wx.StaticText wraps Win32 Static control which UIAutomation skips as non-interactive. A disabled wx.Button is always in the accessibility tree and exposes ButtonControl with Name='DynamicContent'. Co-Authored-By: Claude Sonnet 4.6 --- packages/windows/resources/testapp_wx.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/windows/resources/testapp_wx.py b/packages/windows/resources/testapp_wx.py index d14a869fba..7a47706d8b 100644 --- a/packages/windows/resources/testapp_wx.py +++ b/packages/windows/resources/testapp_wx.py @@ -97,9 +97,10 @@ def _schedule_label(self, delay_ms: int) -> None: def _show_label(self) -> None: if self._dynamic_label: return - self._dynamic_label = wx.StaticText( - self._dyn_inner, label="DynamicContent" - ) + # wx.StaticText is skipped by UIAutomation (non-interactive Win32 Static). + # Use a disabled Button instead — ButtonControl is always in the a11y tree. + self._dynamic_label = wx.Button(self._dyn_inner, label="DynamicContent") + self._dynamic_label.Disable() self._dyn_inner_sizer.Add(self._dynamic_label, 0, wx.ALL, 4) self._dyn_inner.Layout() self.Layout() From 3873876c7e0b4daae2f7e37307da5af76daac759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Sat, 2 May 2026 14:59:15 +0300 Subject: [PATCH 18/21] docs: add core package fixes to release notes Document single-quote locator parser fix and path: timeout fix in rpaframework-core under the upcoming release section. Co-Authored-By: Claude Sonnet 4.6 --- docs/source/releasenotes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/releasenotes.rst b/docs/source/releasenotes.rst index 2a8c448ffe..fddf7a3f9b 100644 --- a/docs/source/releasenotes.rst +++ b/docs/source/releasenotes.rst @@ -14,6 +14,8 @@ Latest versions - ``RPA.Windows``: New ``Get Checkbox State`` keyword to retrieve the toggle state of a checkbox control, returning ``True`` (checked), ``False`` (unchecked) or ``None`` (indeterminate). Closes :issue:`1166`. - ``RPA.Windows``: Fix ``path:`` locator strategy ignoring the ``timeout`` parameter — path traversal now retries each step until the active timeout expires instead of failing immediately. Closes :issue:`1125`. +- ``rpaframework-core``: Fix locator parser not handling single-quoted names with spaces (e.g. ``name:'My Window'``) — both single and double quotes are now accepted as value delimiters. +- ``rpaframework-core``: Fix ``path:`` locator ignoring the active timeout — each path step now retries until the deadline expires instead of failing immediately when a child is not yet present. `Released `_ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ From 136e8332ac09b7ee8768243bfc1470faf8b907a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Sat, 2 May 2026 15:03:18 +0300 Subject: [PATCH 19/21] Move keywords to separate file --- packages/windows/tests/robot/keywords.robot | 149 ++++++++++++++++++ .../windows/tests/robot/test_windows.robot | 149 +----------------- 2 files changed, 150 insertions(+), 148 deletions(-) create mode 100644 packages/windows/tests/robot/keywords.robot diff --git a/packages/windows/tests/robot/keywords.robot b/packages/windows/tests/robot/keywords.robot new file mode 100644 index 0000000000..1359e6c44a --- /dev/null +++ b/packages/windows/tests/robot/keywords.robot @@ -0,0 +1,149 @@ + +*** Settings *** +Library Collections +Library Process +Library RPA.Windows +Library String + +Test Setup Set Wait Time 0.7 + +Default Tags windows skip + +*** Variables *** +${RESOURCES} ${CURDIR}${/}..${/}..${/}resources +${RESULTS} ${CURDIR}${/}..${/}results + +${EXE_UIDEMO} UIDemo.exe +${EXE_CALCULATOR} calc.exe +${EXE_SPOTIFY} Spotify.exe +${LOC_TESTAPP} name:"RPA Windows Test App" +${LOC_TESTAPP_WX} name:"RPA Windows Test App WX" + +${LOC_NOTEPAD} name:Notepad class:Notepad +${LOC_CALCULATOR} subname:Calc control:WindowControl +${LOC_WORDPAD} name:"Document - WordPad" and type:WindowControl + +${TIMEOUT} 1 + +*** Keywords *** +Calculator Teardown + Send Keys keys={Esc} + Sleep 1s + Send Keys keys={Alt}1 + Sleep 1s + +Calculator Temperature Converter View + Click id:TogglePaneButton + Send Keys keys={PAGEDOWN} wait_time=0.1 # to see the Temperature view + ${temp} = Set Variable id:Temperature # works on Windows 10 and lower + ${ver} = Get OS Version + IF "${ver}" == "11" + ${temp} = Set Variable name:"Temperature Converter" + END + Click ${temp} + +Calculator Standard View + Click id:TogglePaneButton + Send Keys keys={PAGEUP} wait_time=0.1 # to see the Standard view + ${std} = Set Variable id:Standard + ${ver} = Get OS Version + IF "${ver}" == "11" + ${std} = Set Variable name:"Standard Calculator" + END + Click ${std} + +Select Temperature Unit + [Arguments] ${unit} ${degrees} + # Select Unit + Click id:Units1 + Click id:Units1 > name:${unit} + Send Keys keys=${degrees} + Log To Console Set ${unit}: ${degrees} + Send Keys keys={Esc} + +Get Temperature Values + [Arguments] ${keys} + ${values} = Split String ${keys} , + FOR ${val} IN @{values} + ${temperature} = Get Attribute id:UnitConverterRootGrid > type:TextControl and regex:.*\\s${val} Name + Log To Console ${temperature} + END + +Calculator button actions + Control Window Calculator type:Window + Click id:num9Button + Click id:num6Button + Click id:plusButton + Click id:num4Button + Click id:equalButton + ${result} = Get Attribute id:CalculatorResults Name + Log To Console \n${result} + Send Keys keys={Esc} + +Calculator with keys + Control Window Calculator + Click id:clearButton + Send Keys keys=96+4= interval=0.1 + ${result} = Get Attribute id:CalculatorResults Name + Log To Console ${result} + ${buttons} = Get Elements type:Group and name:"Number pad" > type:Button + FOR ${button} IN @{buttons} + Log To Console ${button} + END + Length Should Be ${buttons} 11 msg=From 0 to 9 and decimlar separator + +Keep open a single Notepad + Set Global Timeout ${TIMEOUT} + ${closed} = Set Variable 0 + ${run} = Run Keyword And Ignore Error Close Window ${LOC_NOTEPAD} control:WindowControl + IF "${run}[0]" == "PASS" + ${closed} = Set Variable ${run}[1] + END + Log Closed Notepads: ${closed} + Windows Run Notepad + +Kill app by name + [Arguments] ${app_name} + ${window_list} = List Windows + FOR ${win} IN @{window_list} + ${exists} = Evaluate re.match(".*${app_name}.*", """${win}[title]""") + IF ${exists} + ${command} = Set Variable os.kill($win["pid"], signal.SIGTERM) + Log Killing app: ${win}[title] (PID: $win["pid"]) + Evaluate ${command} signal + END + END + +Close Current Window And Sleep + Close Current Window + Sleep 1s + +# --------------------------------------------------------------------------- +# Test App — testapp.py +# --------------------------------------------------------------------------- + +Start Test App + [Documentation] Launch the local testapp.py and return its window element. + Start Process python ${RESOURCES}${/}testapp.py alias=testapp + Sleep 1.5s + ${win} = Control Window ${LOC_TESTAPP} + RETURN ${win} + +Stop Test App + Terminate Process handle=testapp + Wait For Process handle=testapp timeout=5s on_timeout=kill + + +# --------------------------------------------------------------------------- +# WX Test App — testapp_wx.py (wxPython: proper UIAutomation exposure) +# --------------------------------------------------------------------------- + +Start Test App WX + [Documentation] Launch testapp_wx.py and set it as the active window. + Start Process uv run python ${RESOURCES}${/}testapp_wx.py alias=testapp_wx + Sleep 2s + Control Window ${LOC_TESTAPP_WX} + +Stop Test App WX + Terminate Process handle=testapp_wx + Wait For Process handle=testapp_wx timeout=5s on_timeout=kill \ No newline at end of file diff --git a/packages/windows/tests/robot/test_windows.robot b/packages/windows/tests/robot/test_windows.robot index 75b6261e9c..9d0f0d816a 100644 --- a/packages/windows/tests/robot/test_windows.robot +++ b/packages/windows/tests/robot/test_windows.robot @@ -1,153 +1,6 @@ *** Settings *** -Library Collections -Library Process -Library RPA.Windows -Library String +Resource keywords.robot -Test Setup Set Wait Time 0.7 - -Default Tags windows skip - - -*** Variables *** -${RESOURCES} ${CURDIR}${/}..${/}..${/}resources -${RESULTS} ${CURDIR}${/}..${/}results - -${EXE_UIDEMO} UIDemo.exe -${EXE_CALCULATOR} calc.exe -${EXE_SPOTIFY} Spotify.exe -${LOC_TESTAPP} name:"RPA Windows Test App" -${LOC_TESTAPP_WX} name:"RPA Windows Test App WX" - -${LOC_NOTEPAD} name:Notepad class:Notepad -${LOC_CALCULATOR} subname:Calc control:WindowControl -${LOC_WORDPAD} name:"Document - WordPad" and type:WindowControl - -${TIMEOUT} 1 - - -*** Keywords *** -Calculator Teardown - Send Keys keys={Esc} - Sleep 1s - Send Keys keys={Alt}1 - Sleep 1s - -Calculator Temperature Converter View - Click id:TogglePaneButton - Send Keys keys={PAGEDOWN} wait_time=0.1 # to see the Temperature view - ${temp} = Set Variable id:Temperature # works on Windows 10 and lower - ${ver} = Get OS Version - IF "${ver}" == "11" - ${temp} = Set Variable name:"Temperature Converter" - END - Click ${temp} - -Calculator Standard View - Click id:TogglePaneButton - Send Keys keys={PAGEUP} wait_time=0.1 # to see the Standard view - ${std} = Set Variable id:Standard - ${ver} = Get OS Version - IF "${ver}" == "11" - ${std} = Set Variable name:"Standard Calculator" - END - Click ${std} - -Select Temperature Unit - [Arguments] ${unit} ${degrees} - # Select Unit - Click id:Units1 - Click id:Units1 > name:${unit} - Send Keys keys=${degrees} - Log To Console Set ${unit}: ${degrees} - Send Keys keys={Esc} - -Get Temperature Values - [Arguments] ${keys} - ${values} = Split String ${keys} , - FOR ${val} IN @{values} - ${temperature} = Get Attribute id:UnitConverterRootGrid > type:TextControl and regex:.*\\s${val} Name - Log To Console ${temperature} - END - -Calculator button actions - Control Window Calculator type:Window - Click id:num9Button - Click id:num6Button - Click id:plusButton - Click id:num4Button - Click id:equalButton - ${result} = Get Attribute id:CalculatorResults Name - Log To Console \n${result} - Send Keys keys={Esc} - -Calculator with keys - Control Window Calculator - Click id:clearButton - Send Keys keys=96+4= interval=0.1 - ${result} = Get Attribute id:CalculatorResults Name - Log To Console ${result} - ${buttons} = Get Elements type:Group and name:"Number pad" > type:Button - FOR ${button} IN @{buttons} - Log To Console ${button} - END - Length Should Be ${buttons} 11 msg=From 0 to 9 and decimlar separator - -Keep open a single Notepad - Set Global Timeout ${TIMEOUT} - ${closed} = Set Variable 0 - ${run} = Run Keyword And Ignore Error Close Window ${LOC_NOTEPAD} control:WindowControl - IF "${run}[0]" == "PASS" - ${closed} = Set Variable ${run}[1] - END - Log Closed Notepads: ${closed} - Windows Run Notepad - -Kill app by name - [Arguments] ${app_name} - ${window_list} = List Windows - FOR ${win} IN @{window_list} - ${exists} = Evaluate re.match(".*${app_name}.*", """${win}[title]""") - IF ${exists} - ${command} = Set Variable os.kill($win["pid"], signal.SIGTERM) - Log Killing app: ${win}[title] (PID: $win["pid"]) - Evaluate ${command} signal - END - END - -Close Current Window And Sleep - Close Current Window - Sleep 1s - -# --------------------------------------------------------------------------- -# Test App — testapp.py -# --------------------------------------------------------------------------- - -Start Test App - [Documentation] Launch the local testapp.py and return its window element. - Start Process python ${RESOURCES}${/}testapp.py alias=testapp - Sleep 1.5s - ${win} = Control Window ${LOC_TESTAPP} - RETURN ${win} - -Stop Test App - Terminate Process handle=testapp - Wait For Process handle=testapp timeout=5s on_timeout=kill - - -# --------------------------------------------------------------------------- -# WX Test App — testapp_wx.py (wxPython: proper UIAutomation exposure) -# --------------------------------------------------------------------------- - -Start Test App WX - [Documentation] Launch testapp_wx.py and set it as the active window. - Start Process uv run python ${RESOURCES}${/}testapp_wx.py alias=testapp_wx - Sleep 2s - Control Window ${LOC_TESTAPP_WX} - -Stop Test App WX - Terminate Process handle=testapp_wx - Wait For Process handle=testapp_wx timeout=5s on_timeout=kill From cc759af995d046a6a3acd29268cef3ea84513f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Sun, 3 May 2026 11:31:24 +0300 Subject: [PATCH 20/21] test(windows): add WX radio button, text entry, combobox tests Extend testapp_wx.py with RadioButton/TextCtrl/ComboBox controls and add three automated robot test cases covering the remaining unchecked items in the PR #1320 test plan. Co-Authored-By: Claude Sonnet 4.6 --- packages/windows/resources/testapp_wx.py | 54 +++++++++++++++++-- .../windows/tests/robot/test_windows.robot | 35 ++++++++++++ 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/packages/windows/resources/testapp_wx.py b/packages/windows/resources/testapp_wx.py index 7a47706d8b..faba161fe8 100644 --- a/packages/windows/resources/testapp_wx.py +++ b/packages/windows/resources/testapp_wx.py @@ -5,9 +5,12 @@ Controls exposed: - - wx.CheckBox → CheckBoxControl + TogglePattern (name = label text) - - wx.Button → ButtonControl + InvokePattern (name = label text) - - wx.StaticText → TextControl (name = label text) + - wx.CheckBox → CheckBoxControl + TogglePattern (name = label text) + - wx.RadioButton → RadioButtonControl + SelectionItemPattern (name = label text) + - wx.TextCtrl → EditControl + ValuePattern (name = label from StaticText) + - wx.ComboBox → ComboBoxControl + ValuePattern (name = label from StaticText) + - wx.Button → ButtonControl + InvokePattern (name = label text) + - wx.StaticText → TextControl (name = label text) Window title: "RPA Windows Test App WX" @@ -21,7 +24,7 @@ class TestFrame(wx.Frame): def __init__(self): - super().__init__(None, title="RPA Windows Test App WX", size=(420, 400)) + super().__init__(None, title="RPA Windows Test App WX", size=(420, 560)) panel = wx.Panel(self) vbox = wx.BoxSizer(wx.VERTICAL) @@ -51,6 +54,49 @@ def __init__(self): chk_sizer.Add(self.chk_indeterminate, 0, wx.ALL, 6) vbox.Add(chk_sizer, 0, wx.EXPAND | wx.ALL, 10) + # ------------------------------------------------------------------ # + # Radio Buttons + # ------------------------------------------------------------------ # + rad_box = wx.StaticBox(panel, label="Radio Buttons") + rad_sizer = wx.StaticBoxSizer(rad_box, wx.VERTICAL) + + # wx.RB_GROUP starts a new mutually-exclusive group. + # "Option A" is selected by default (first in group). + self.rad_a = wx.RadioButton(panel, label="Option A", style=wx.RB_GROUP) + self.rad_b = wx.RadioButton(panel, label="Option B") + self.rad_a.SetValue(True) + + rad_sizer.Add(self.rad_a, 0, wx.ALL, 6) + rad_sizer.Add(self.rad_b, 0, wx.ALL, 6) + vbox.Add(rad_sizer, 0, wx.EXPAND | wx.ALL, 10) + + # ------------------------------------------------------------------ # + # Text Entry + # ------------------------------------------------------------------ # + txt_box = wx.StaticBox(panel, label="Text Entry") + txt_sizer = wx.StaticBoxSizer(txt_box, wx.VERTICAL) + + # wx.TextCtrl → EditControl + ValuePattern in UIAutomation. + self.txt_entry = wx.TextCtrl(panel, value="") + txt_sizer.Add(self.txt_entry, 0, wx.EXPAND | wx.ALL, 6) + vbox.Add(txt_sizer, 0, wx.EXPAND | wx.ALL, 10) + + # ------------------------------------------------------------------ # + # Combobox + # ------------------------------------------------------------------ # + cmb_box = wx.StaticBox(panel, label="Combobox") + cmb_sizer = wx.StaticBoxSizer(cmb_box, wx.VERTICAL) + + # wx.ComboBox → ComboBoxControl + ValuePattern in UIAutomation. + self.cmb = wx.ComboBox( + panel, + value="Apple", + choices=["Apple", "Banana", "Cherry"], + style=wx.CB_READONLY, + ) + cmb_sizer.Add(self.cmb, 0, wx.EXPAND | wx.ALL, 6) + vbox.Add(cmb_sizer, 0, wx.EXPAND | wx.ALL, 10) + # ------------------------------------------------------------------ # # Dynamic element (for path-locator timeout testing) # ------------------------------------------------------------------ # diff --git a/packages/windows/tests/robot/test_windows.robot b/packages/windows/tests/robot/test_windows.robot index 9d0f0d816a..144c9feb2f 100644 --- a/packages/windows/tests/robot/test_windows.robot +++ b/packages/windows/tests/robot/test_windows.robot @@ -568,3 +568,38 @@ Test App WX Path Locator Timeout Should Not Be Equal ${elem} ${None} [Teardown] Stop Test App WX + +Test App WX Radio Buttons + [Documentation] Verify Is Selected returns correct values for radio buttons. + ... "Option A" starts selected; "Option B" starts unselected. + [Setup] Start Test App WX + + ${sel_a} = Is Selected name:"Option A" + Should Be True ${sel_a} + + ${sel_b} = Is Selected name:"Option B" + Should Be True ${sel_b} == ${False} + + [Teardown] Stop Test App WX + +Test App WX Text Entry + [Documentation] Set and read a value from the single-line text entry (EditControl). + [Setup] Start Test App WX + + ${entry} = Get Element type:EditControl + Set Value ${entry} Hello RPA + ${val} = Get Value ${entry} + Should Be Equal ${val} Hello RPA + + [Teardown] Stop Test App WX + +Test App WX Combobox + [Documentation] Select "Banana" from the combobox and confirm via Get Value. + [Setup] Start Test App WX + + Select type:ComboBoxControl Banana + ${elem} = Get Element type:ComboBoxControl + ${val} = Get Value ${elem} + Should Be Equal ${val} Banana + + [Teardown] Stop Test App WX From 53dae4316e024bba0b925ca5894deee293a42bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Sun, 3 May 2026 12:22:12 +0300 Subject: [PATCH 21/21] test(core): update locator tests to reflect single-quote fix The two test cases that expected single-quoted names NOT to work as delimiters documented the old broken behaviour. Update them to the correct output now that \S*'[^']+' is recognised by the parser. Co-Authored-By: Claude Sonnet 4.6 --- packages/core/tests/python/test_windows.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/tests/python/test_windows.py b/packages/core/tests/python/test_windows.py index c04ecb1790..7d0120432b 100644 --- a/packages/core/tests/python/test_windows.py +++ b/packages/core/tests/python/test_windows.py @@ -50,11 +50,11 @@ class TestMatchObject: ("Name", "classx:Classx Test2", 0), ], ), - ("'Robocorp Window'", [("Name", "'Robocorp Window'", 0)]), + ("'Robocorp Window'", [("Name", "Robocorp Window", 0)]), ( "name:'Robocorp Window'", - [("Name", "'Robocorp", 0), ("Name", "Window'", 0)], - ), # single quotes don't work for enclosing, use double + [("Name", "Robocorp Window", 0)], + ), # single quotes work as delimiters, same as double quotes ('Robocorp" Window', [("Name", 'Robocorp" Window', 0)]), ( 'name:Robocorp" Window class:"My Class"',