Skip to content

Commit bcaa342

Browse files
nmetulevCopilot
andcommitted
Fix FindAll missing elements after WebView2 controls
UIA FindAll(TreeScope_Descendants) can stall in the WebView2 Chromium UIA provider subtree, causing sibling elements after the WebView to be silently skipped. This caused 'search StatusBar' and 'get-value StatusBar' to return 0 results even though 'inspect' (which uses TreeWalker) found the element. Added ManualTreeSearch fallback that uses TreeWalker (GetFirstChildElement/GetNextSiblingElement) to walk the tree sibling-by-sibling. This is the same traversal method inspect uses. The fallback activates when FindAll returns 0 results. Applied to both SearchAsync and FindSingleElementAsync. Root cause verified with repro app: StatusBar Edit at depth 3 was a sibling after PreviewWebView Pane. FindAll found elements before the WebView but not after. Manual tree walk found it reliably. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3800ed3 commit bcaa342

1 file changed

Lines changed: 77 additions & 0 deletions

File tree

src/winapp-CLI/WinApp.Cli/Services/UiAutomationService.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,27 @@ public Task<UiElement[]> SearchAsync(UiSessionInfo session, SelectorExpression s
367367
}
368368
}
369369

370+
// If FindAll missed elements (WebView2 can stall UIA tree traversal), try manual tree walk
371+
if (mainResults.Count == 0 && selector.Query is not null)
372+
{
373+
_logger.LogDebug("FindAll returned 0 results, trying manual tree walk fallback");
374+
var manualResults = ManualTreeSearch(root, selector.Query, maxResults);
375+
foreach (var el in manualResults)
376+
{
377+
var uiEl = ToUiElement(el, "", ref nextElementId);
378+
uiEl.WindowHandle = session.WindowHandle;
379+
if (!IsInvokable(el))
380+
{
381+
var ancestor = FindInvokableAncestor(el, root);
382+
if (ancestor is not null)
383+
{
384+
uiEl.InvokableAncestor = ToUiElement(ancestor, "", ref nextElementId);
385+
}
386+
}
387+
mainResults.Add(uiEl);
388+
}
389+
}
390+
370391
// If no results on main window, search popup/owned windows
371392
if (mainResults.Count == 0)
372393
{
@@ -489,6 +510,20 @@ public Task<UiElement[]> SearchAsync(UiSessionInfo session, SelectorExpression s
489510

490511
if (found is null || found.get_Length() == 0)
491512
{
513+
// FindAll may miss elements after WebView2 controls — try manual tree walk
514+
if (selector.Query is not null)
515+
{
516+
_logger.LogDebug("FindAll returned 0 results, trying manual tree walk fallback");
517+
var manualResults = ManualTreeSearch(root, selector.Query, 1);
518+
if (manualResults.Count > 0)
519+
{
520+
var nextId = 0;
521+
var manualResult = ToUiElement(manualResults[0], "", ref nextId);
522+
manualResult.WindowHandle = session.WindowHandle;
523+
return Task.FromResult<UiElement?>(manualResult);
524+
}
525+
}
526+
492527
// Element not found on main window — search popup/owned windows
493528
var otherResult = FindElementOnOtherWindows(session, selector);
494529
if (otherResult is not null)
@@ -1570,6 +1605,48 @@ private static bool IsInternalWindow(string? className) =>
15701605
return null;
15711606
}
15721607

1608+
/// <summary>
1609+
/// Manual tree walk search using TreeWalker. Slower than FindAll but reliable —
1610+
/// works around UIA FindAll bugs where WebView2 controls stall the tree traversal
1611+
/// and cause sibling elements after the WebView to be skipped.
1612+
/// </summary>
1613+
private List<IUIAutomationElement> ManualTreeSearch(IUIAutomationElement root, string query, int maxResults, int maxDepth = 25)
1614+
{
1615+
var walker = _automation.get_ControlViewWalker();
1616+
var results = new List<IUIAutomationElement>();
1617+
ManualTreeSearchRecursive(walker, root, query, maxResults, maxDepth, 0, results);
1618+
return results;
1619+
}
1620+
1621+
private static void ManualTreeSearchRecursive(IUIAutomationTreeWalker walker, IUIAutomationElement element,
1622+
string query, int maxResults, int maxDepth, int depth, List<IUIAutomationElement> results)
1623+
{
1624+
if (depth > maxDepth || results.Count >= maxResults) { return; }
1625+
1626+
IUIAutomationElement? child;
1627+
try { child = walker.GetFirstChildElement(element); }
1628+
catch { return; }
1629+
1630+
while (child is not null && results.Count < maxResults)
1631+
{
1632+
try
1633+
{
1634+
var name = SafeGetBstr(() => child.get_CurrentName());
1635+
var aid = SafeGetBstr(() => child.get_CurrentAutomationId());
1636+
1637+
if ((aid is not null && aid.Contains(query, StringComparison.OrdinalIgnoreCase)) ||
1638+
(name is not null && name.Contains(query, StringComparison.OrdinalIgnoreCase)))
1639+
{
1640+
results.Add(child);
1641+
}
1642+
1643+
ManualTreeSearchRecursive(walker, child, query, maxResults, maxDepth, depth + 1, results);
1644+
child = walker.GetNextSiblingElement(child);
1645+
}
1646+
catch { break; }
1647+
}
1648+
}
1649+
15731650
private IUIAutomationCondition? BuildCondition(SelectorExpression selector)
15741651
{
15751652
if (selector.Query is not null)

0 commit comments

Comments
 (0)