Files
orca/native/computer-use-windows/runtime-native-controls.test.ps1
Kelvin Amoaba a406364654 fix(computer): click, set and scroll Windows buttons, text boxes, combo boxes and lists without bringing the window forward (#42237)
* fix(computer): press, set and scroll Windows native controls without activating the window

Measured on Windows CI: UI Automation's Invoke, Toggle, ValuePattern and
ScrollPattern focus a background WinForms control first, which activates its
window. Native controls now go through their own messages instead, each
owner-checked: BM_CLICK for buttons, check boxes and radio buttons, select-all
plus EM_REPLACESEL for set-value on edits, CB_SETCURSEL with CBN_SELCHANGE for
combo boxes (exact-text match, read back after the app's handler runs) and
LB_SETTOPINDEX for list box pages.

* fix(computer): refuse read-only Windows documents before typing keys, document the caret fallback

* fix(computer): keep Windows native routes to measured controls and refuse partial writes

BM_CLICK only for WinForms buttons, multi-column lists stay on ScrollPattern, a
list already at its end reports success, edits refuse text past their limit,
the combo's owner is checked right before the selection, a cleared selection
reads as a mismatch and a disabled control is element_not_enabled.

* fix(computer): hold Windows edit limits without a readback, require a complete combo search

* fix(computer): read a long Windows edit's length from the field for its limit check

* test(computer): share the Windows fixture action helpers
2026-11-14 02:45:10 +00:00

118 lines
6.1 KiB
PowerShell

param([string]$RuntimePath = (Join-Path $PSScriptRoot "runtime.ps1"))
$ErrorActionPreference = "Stop"
function Assert-TestEqual($Actual, $Expected, [string]$Label) {
if ($Actual -cne $Expected) { throw "$Label expected '$Expected', received '$Actual'" }
}
Add-Type -TypeDefinition @"
using System;
using System.Collections.Generic;
// A combo box and a list box in a fake process 4242, answering the messages the runtime sends.
public static class OrcaDesktopWin32 {
public static readonly List<string> Calls = new List<string>();
public static string[] Items;
public static int Selected;
public static int Top;
public static int ListCount;
public static int ClientHeight;
public static void Reset(string[] items) {
Calls.Clear();
Items = items;
Selected = 0;
Top = 0;
ListCount = 100;
ClientHeight = 60;
}
public static bool IsWindow(IntPtr hwnd) { return true; }
public static uint GetWindowThreadProcessId(IntPtr hwnd, out uint processId) { processId = 4242; return 1; }
public static int GetWindowLong(IntPtr hwnd, int index) { return 0; }
public static IntPtr GetParent(IntPtr hwnd) { return new IntPtr(101); }
public static int GetDlgCtrlID(IntPtr hwnd) { return 7; }
public static int GetClientHeight(IntPtr hwnd) { return ClientHeight; }
// CB_FINDSTRINGEXACT: case-insensitive, searching after start and wrapping.
public static long? FindControlString(IntPtr hwnd, uint message, int start, string name) {
for (var step = 1; step <= Items.Length; step++) {
var index = (start + step + Items.Length) % Items.Length;
if (string.Equals(Items[index], name, StringComparison.OrdinalIgnoreCase)) { return index; }
}
return -1;
}
public static string GetControlItemText(IntPtr hwnd, uint lengthMessage, uint textMessage, int index) { return Items[index]; }
public static long? SendControlMessage(IntPtr hwnd, uint message, IntPtr wParam, IntPtr lParam) {
switch (message) {
case 0x0146: return Items.Length;
case 0x0147: return Selected;
case 0x014E: Selected = wParam.ToInt32(); Calls.Add("select:" + Selected); return Selected;
case 0x0111: Calls.Add("command:" + hwnd + ":" + wParam.ToInt64().ToString("X")); return 0;
case 0x018E: return Top;
case 0x018B: return ListCount;
case 0x01A1: return 15;
case 0x0197: Top = wParam.ToInt32(); Calls.Add("top:" + Top); return 0;
default: return 0;
}
}
}
"@
$tokens = $null
$errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile($RuntimePath, [ref]$tokens, [ref]$errors)
if ($errors.Count) { throw ($errors | Out-String) }
$functions = @(
"Test-OrcaNativeClass", "Test-OrcaEditClass", "Get-OrcaNativeControl", "Assert-OrcaWindowOwner", "Get-OrcaProperty",
"Set-OrcaNativeComboValue", "Set-OrcaElementValue", "Invoke-OrcaNativeListScroll", "Test-OrcaSameRuntimeId"
)
foreach ($definition in $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false)) {
if ($definition.Name -in $functions) { . ([scriptblock]::Create($definition.Extent.Text)) }
}
$process = [pscustomobject]@{ Id = 4242 }
function Get-OrcaRuntimeId($Element) { @($Element.id) }
function Test-OrcaElementDisabled($Element) { -not $Element.Current.IsEnabled }
function Set-OrcaNativeEditValue { $null }
function New-TestControl([string]$ClassName, [bool]$Enabled = $true) {
[pscustomobject]@{ id = 5; Current = [pscustomobject]@{ ClassName = $ClassName; NativeWindowHandle = 303; IsEnabled = $Enabled } }
}
$combo = New-TestControl "WindowsForms10.COMBOBOX.app.0.1"
# The exact-case option is picked among case-insensitive hits, and the app's change handler is told.
[OrcaDesktopWin32]::Reset(@("Alpha", "gamma", "Gamma"))
$written = Set-OrcaElementValue $combo $process 5 "Gamma"
Assert-TestEqual ([OrcaDesktopWin32]::Calls -join "|") "select:2|command:101:10007" "combo selection messages"
Assert-TestEqual $written.verification.state "verified" "combo verification"
# Two options with the exact name can't be told apart, so nothing is selected.
[OrcaDesktopWin32]::Reset(@("Gamma", "Beta", "Gamma"))
$message = try { Set-OrcaElementValue $combo $process 5 "Gamma" | Out-Null; $null } catch { $_.Exception.Message }
if ($message -notlike "*more than one option is named 'Gamma'*") { throw "ambiguous combo, received '$message'" }
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "ambiguous combo sends nothing"
# A disabled combo box is locked against the user, so it is refused before any message.
[OrcaDesktopWin32]::Reset(@("Alpha", "Gamma"))
$message = try { Set-OrcaElementValue (New-TestControl "ComboBox" $false) $process 5 "Gamma" | Out-Null; $null } catch { $_.Exception.Message }
Assert-TestEqual $message "element_not_enabled: element 5 is disabled, so no input was sent; do what enables it first, then run get-app-state and retry once its line no longer lists disabled" "disabled combo"
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "disabled combo sends nothing"
# A page is the rows the list shows: 60 px at 15 px a row is 4 rows.
$list = New-TestControl "ListBox"
[OrcaDesktopWin32]::Reset(@())
Assert-TestEqual (Invoke-OrcaNativeListScroll $list $list $process "down" 2) $true "list scroll"
Assert-TestEqual ([OrcaDesktopWin32]::Calls -join "|") "top:8" "list scroll top row"
# At the end already, there is nothing to move and nothing falls back to ScrollPattern.
[OrcaDesktopWin32]::Reset(@())
[OrcaDesktopWin32]::Top = 96
Assert-TestEqual (Invoke-OrcaNativeListScroll $list $list $process "down" 1) $true "list at its end"
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "list at its end sends nothing"
# A list that shows all its rows leaves the scroll to an enclosing ScrollPattern.
[OrcaDesktopWin32]::Reset(@())
[OrcaDesktopWin32]::ListCount = 3
Assert-TestEqual (Invoke-OrcaNativeListScroll $list $list $process "down" 1) $false "short list"
Assert-TestEqual ([OrcaDesktopWin32]::Calls.Count) 0 "short list sends nothing"
Write-Output "windows-native-controls-tests-ok"