Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ef7fda8
feat(windows): add Get Checkbox State keyword for toggle state detection
mikahanninen Mar 9, 2026
52e04bc
fix(windows): remove redundant IS_WINDOWS guard in get_checkbox_state
mikahanninen Mar 9, 2026
3710bb4
fix(windows): respect timeout in path: locator strategy
mikahanninen Mar 9, 2026
b208f64
feat(windows): add testapp.py test fixture and robot test cases
mikahanninen Mar 9, 2026
650a262
fix(windows): fix test app robot locators and process teardown
mikahanninen Mar 9, 2026
79c0299
fix(windows): remove invalid scalar variable and mark uncertain slide…
mikahanninen Mar 9, 2026
9fe2c06
Merge branch 'master' into feature/windows-get-checkbox-state
mikahanninen May 2, 2026
a242ab9
Update test_windows.robot
mikahanninen May 2, 2026
7512446
Update test_windows.robot
mikahanninen May 2, 2026
cb49e4a
Update locators.py
mikahanninen May 2, 2026
0f886bf
Update test_windows.robot
mikahanninen May 2, 2026
5996d03
Update test_windows.robot
mikahanninen May 2, 2026
ba83cef
feat(windows): add wxPython test app and mark tkinter tests as manual
mikahanninen May 2, 2026
4c54e53
fix(windows): use uv run python to launch wxPython test app
mikahanninen May 2, 2026
23692a1
Update uv.lock
mikahanninen May 2, 2026
9aeff21
Update test_windows.robot
mikahanninen May 2, 2026
ff737d9
fix(windows): increase path timeout test margin to 8s
mikahanninen May 2, 2026
f3d2344
fix(windows): use disabled Button for dynamic element in wx test app
mikahanninen May 2, 2026
3873876
docs: add core package fixes to release notes
mikahanninen May 2, 2026
136e833
Move keywords to separate file
mikahanninen May 2, 2026
cc759af
test(windows): add WX radio button, text entry, combobox tests
mikahanninen May 3, 2026
53dae43
test(core): update locator tests to reflect single-quote fix
mikahanninen May 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/source/releasenotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Latest versions
`Upcoming release <https://github.com/robocorp/rpaframework/projects/3#column-16713994>`_
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

- ``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 <https://pypi.org/project/rpaframework/#history>`_
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Expand Down
89 changes: 55 additions & 34 deletions packages/core/src/RPA/core/windows/locators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions packages/core/tests/python/test_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"',
Expand Down
1 change: 1 addition & 0 deletions packages/windows/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ repository = "https://github.com/robocorp/rpaframework"
[dependency-groups]
dev = [
"rpaframework-devutils",
"wxPython>=4.2.0; sys_platform == 'win32'",
]

[tool.uv]
Expand Down
Loading
Loading