mirror of
https://github.com/lahfir/agent-desktop.git
synced 2026-08-18 13:05:46 +00:00
feat: exercise interactions, input synthesis, and hit-testing raw
Every interaction records pre-state, action, then an independent re-read -- never the call's return value. Toggle proves why: TogglePattern flips WPF ToggleState without raising Click, so the fixture's status sink stays silent while the element itself changed. Trusting the sink alone would have recorded a failure that did not happen. Posted keystrokes are not uniformly dead. WM_KEYDOWN posted to a Win32 edit control does register, because TranslateMessage runs in the target thread's own pump and synthesises the character regardless of how the message arrived; what the path cannot carry is modifier state. Against Chromium it does not register at all, with every call still returning success. Establishing that needed an idle control pass: the Chromium tree moved on its own between two reads, and without a quiet baseline that drift would have been filed as a keystroke landing. The astral-plane payload survives typing. SendInput forces a surrogate pair into two separate unicode events and the target reassembles it intact, read back through WM_GETTEXT so the check is independent of both the injection path and UIA. Notepad's edit control is unreachable from the managed client, and not merely unenumerated: TryGetCurrentPattern returns false for Text, Value and Scroll on the handle-resolved element, while the COM census sees the same window as a Document carrying all three. Hit-testing is the only sound visibility primitive of the three tested. The zero-size control is addressable by handle, enumerable by no walk, and returned by no point. A minimized window reports an empty rect at the top level while its descendants report real dimensions at the -32000 anchor, with IsOffscreen false throughout -- so neither emptiness nor IsOffscreen can gate occlusion, but ElementFromPoint correctly returns the occluder.
This commit is contained in:
parent
90fd24b4ed
commit
cf596d380d
19 changed files with 4208 additions and 0 deletions
713
probes/windows/05-interactions.ps1
Normal file
713
probes/windows/05-interactions.ps1
Normal file
|
|
@ -0,0 +1,713 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Probe 05 (sub-phase 2.0, unit U5): every 2.0(3) interaction driven through UIA
|
||||
patterns against probe-owned fixtures, each verified by an independent re-read.
|
||||
|
||||
.DESCRIPTION
|
||||
Stack: managed System.Windows.Automation. Scope: app/provider.
|
||||
|
||||
Every row is pre-state -> action -> independently re-read post-state. The pattern
|
||||
call's own return value is never the evidence: state is re-read from the element
|
||||
and, wherever the fixture offers one, from a separate observable sink control
|
||||
(lblStatus / lblScrollPos), which is a different UIA element than the one acted on.
|
||||
|
||||
Target choice is measured, not assumed. U3 established that the WinForms fixture in
|
||||
default mode exposes ZERO patterns to a managed client (its server-side
|
||||
IRawElementProviderSimple suppresses both the client-side proxies and WinForms' own
|
||||
providers, collapsing every node to Pane) and that --host-providers restores only
|
||||
two interactive elements. The WPF fixture carries the full managed pattern surface,
|
||||
so it drives the pattern interactions and the WinForms modes are recorded as
|
||||
comparison rows - a fixture artifact filed as such, not as a platform fact.
|
||||
|
||||
A failed interaction is a verdict row, not a script error.
|
||||
|
||||
Captures under captures/05-interactions/:
|
||||
interactions.json one row per interaction with pre/post evidence
|
||||
text-pattern.json TextPattern exposure on classic Notepad's Edit vs WPF's TextBox
|
||||
focus.json SetFocus vs foreground, headless-invariant evidence for 2.7
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. "$PSScriptRoot\common.ps1"
|
||||
|
||||
Add-Type -AssemblyName UIAutomationClient | Out-Null
|
||||
Add-Type -AssemblyName UIAutomationTypes | Out-Null
|
||||
|
||||
$Probe = '05-interactions'
|
||||
$AE = [System.Windows.Automation.AutomationElement]
|
||||
$Walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
|
||||
$Descendants = [System.Windows.Automation.TreeScope]::Descendants
|
||||
$ControlIds = @{
|
||||
chkToggle = '1001'; txtValue = '1003'; cboChoice = '1004'; btnAction = '1005'
|
||||
btnMutateList = '1006'; tbSlider = '1008'; lstItems = '1010'; pnlScroll = '1011'
|
||||
lblStatus = '1020'; lblScrollPos = '1022'
|
||||
}
|
||||
$script:Spawned = New-Object System.Collections.ArrayList
|
||||
$script:Rows = New-Object System.Collections.ArrayList
|
||||
|
||||
function Initialize-InteractNative {
|
||||
if ('AgentDesktopProbe.Interact' -as [type]) { return }
|
||||
$src = @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
namespace AgentDesktopProbe {
|
||||
public static class Interact {
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
public static extern IntPtr FindWindowExW(IntPtr parent, IntPtr child, string cls, string title);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
public static extern int GetWindowTextW(IntPtr hWnd, StringBuilder text, int count);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetForegroundWindow();
|
||||
public static string WindowTitle(IntPtr h) {
|
||||
StringBuilder sb = new StringBuilder(512);
|
||||
GetWindowTextW(h, sb, sb.Capacity);
|
||||
return sb.ToString();
|
||||
}
|
||||
public static string ForegroundTitle() {
|
||||
return WindowTitle(GetForegroundWindow());
|
||||
}
|
||||
}
|
||||
}
|
||||
'@
|
||||
Add-Type -TypeDefinition $src -Language CSharp | Out-Null
|
||||
}
|
||||
|
||||
function Start-Tracked {
|
||||
param([string]$FilePath, [string[]]$ArgumentList = @(), [int]$TimeoutSec = 25)
|
||||
$p = Start-ScratchProcess -FilePath $FilePath -ArgumentList $ArgumentList -NoActivate -TimeoutSec $TimeoutSec
|
||||
[void]$script:Spawned.Add($p.ProcessId)
|
||||
return $p
|
||||
}
|
||||
|
||||
function Get-ChildCount {
|
||||
param($Element)
|
||||
$n = 0
|
||||
try {
|
||||
$k = $Walker.GetFirstChild($Element)
|
||||
while ($null -ne $k) { $n++; $k = $Walker.GetNextSibling($k) }
|
||||
} catch { }
|
||||
return $n
|
||||
}
|
||||
|
||||
function Wait-TopLevelByTitle {
|
||||
param([string]$Title, [int]$TimeoutSec = 25)
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
while ($true) {
|
||||
try {
|
||||
$c = $Walker.GetFirstChild($AE::RootElement)
|
||||
while ($null -ne $c) {
|
||||
try { if ($c.Current.Name -eq $Title) { return $c } } catch { }
|
||||
$c = $Walker.GetNextSibling($c)
|
||||
}
|
||||
} catch { }
|
||||
if ((Get-Date) -ge $deadline) { return $null }
|
||||
Start-Sleep -Milliseconds 400
|
||||
}
|
||||
}
|
||||
|
||||
function Start-ScratchWpfWindow {
|
||||
param([string]$Tag, [int]$Left, [int]$Top, [int]$SettleSec = 8, [int]$Attempts = 3)
|
||||
$title = 'AgentDesktop Scratch WPF [' + $Tag + ']'
|
||||
for ($i = 1; $i -le $Attempts; $i++) {
|
||||
$proc = Start-Tracked -FilePath 'powershell.exe' -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
|
||||
(Join-Path (Get-ProbeRoot) 'scratch\ScratchWpf.ps1'), '-Tag', $Tag,
|
||||
'-Left', ([string]$Left), '-Top', ([string]$Top), '-TimeoutSeconds', '600')
|
||||
Start-Sleep -Seconds $SettleSec
|
||||
$element = Wait-TopLevelByTitle -Title $title -TimeoutSec 20
|
||||
if ($null -ne $element -and (Get-ChildCount -Element $element) -gt 0) {
|
||||
$hostPid = 0
|
||||
try { $hostPid = [int]$element.Current.ProcessId } catch { }
|
||||
if ($hostPid -gt 0 -and -not $script:Spawned.Contains($hostPid)) {
|
||||
Register-ScratchProcessId -ProcessId $hostPid
|
||||
[void]$script:Spawned.Add($hostPid)
|
||||
}
|
||||
return [pscustomobject]@{ Element = $element; ProcessId = $hostPid; timingLaunches = $i }
|
||||
}
|
||||
Write-ProbeLog -Message ('WPF automation peer not bound on attempt ' + $i + '; relaunching for a fresh HWND') -Level 'warn'
|
||||
try { Stop-ScratchProcess -ProcessId $proc.ProcessId } catch { }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-ScratchElement {
|
||||
param($Root, [string]$Symbolic, [string]$Numeric = '')
|
||||
foreach ($aid in @($Symbolic, $Numeric)) {
|
||||
if ([string]::IsNullOrEmpty($aid)) { continue }
|
||||
try {
|
||||
$c = New-Object System.Windows.Automation.PropertyCondition($AE::AutomationIdProperty, $aid)
|
||||
$e = $Root.FindFirst($Descendants, $c)
|
||||
if ($null -ne $e) { return $e }
|
||||
} catch { }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-Scratch {
|
||||
param($Root, [string]$Name)
|
||||
$numeric = ''
|
||||
if ($ControlIds.ContainsKey($Name)) { $numeric = $ControlIds[$Name] }
|
||||
return (Find-ScratchElement -Root $Root -Symbolic $Name -Numeric $numeric)
|
||||
}
|
||||
|
||||
function Get-SinkText {
|
||||
param($Root, [string]$Name = 'lblStatus')
|
||||
$e = Find-Scratch -Root $Root -Name $Name
|
||||
if ($null -eq $e) { return '<sink-not-found>' }
|
||||
try { return [string]$e.Current.Name } catch { return '<sink-read-failed>' }
|
||||
}
|
||||
|
||||
function Get-SupportedPatternNames {
|
||||
param($El)
|
||||
$names = @()
|
||||
try {
|
||||
foreach ($p in @($El.GetSupportedPatterns())) {
|
||||
$names += ($p.ProgrammaticName -replace 'PatternIdentifiers\.Pattern$', '')
|
||||
}
|
||||
} catch { }
|
||||
return @($names | Sort-Object)
|
||||
}
|
||||
|
||||
function Get-ElementPattern {
|
||||
param($El, $PatternIdentifier)
|
||||
$obj = $null
|
||||
try { if ($El.TryGetCurrentPattern($PatternIdentifier, [ref]$obj)) { return $obj } } catch { }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-TextShape {
|
||||
param([Parameter(Mandatory = $true)][AllowEmptyString()][AllowNull()][string]$Text)
|
||||
if ($null -eq $Text) { $Text = '' }
|
||||
$sha = [System.Security.Cryptography.SHA256]::Create()
|
||||
$hash = (($sha.ComputeHash([System.Text.Encoding]::Unicode.GetBytes($Text))) | ForEach-Object { $_.ToString('x2') }) -join ''
|
||||
$sha.Dispose()
|
||||
$codepoints = 0
|
||||
$pairs = 0
|
||||
$replacement = 0
|
||||
for ($i = 0; $i -lt $Text.Length; $i++) {
|
||||
$c = $Text[$i]
|
||||
if ($c -eq [char]0xFFFD) { $replacement++ }
|
||||
$codepoints++
|
||||
if ([char]::IsHighSurrogate($c) -and ($i + 1) -lt $Text.Length -and [char]::IsLowSurrogate($Text[$i + 1])) {
|
||||
$pairs++
|
||||
$i++
|
||||
}
|
||||
}
|
||||
return [ordered]@{
|
||||
utf16Units = $Text.Length
|
||||
codepoints = $codepoints
|
||||
surrogatePairs = $pairs
|
||||
replacementChars = $replacement
|
||||
sha256Utf16 = $hash
|
||||
}
|
||||
}
|
||||
|
||||
function Add-InteractionRow {
|
||||
param(
|
||||
[string]$Interaction, [string]$Target, [string]$AutomationId,
|
||||
[string]$Pattern, $Evidence
|
||||
)
|
||||
$row = [ordered]@{
|
||||
interaction = $Interaction
|
||||
target = $Target
|
||||
stack = 'managed-System.Windows.Automation'
|
||||
automationId = $AutomationId
|
||||
pattern = $Pattern
|
||||
}
|
||||
foreach ($k in $Evidence.Keys) { $row[$k] = $Evidence[$k] }
|
||||
[void]$script:Rows.Add($row)
|
||||
}
|
||||
|
||||
function Add-PatternAbsentRow {
|
||||
param([string]$Interaction, [string]$Target, [string]$AutomationId, [string]$Pattern, $El, [string]$Detail)
|
||||
$supported = @()
|
||||
$controlType = '<element-not-found>'
|
||||
if ($null -ne $El) {
|
||||
$supported = Get-SupportedPatternNames -El $El
|
||||
try { $controlType = $El.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '' } catch { }
|
||||
}
|
||||
Add-InteractionRow -Interaction $Interaction -Target $Target -AutomationId $AutomationId -Pattern $Pattern -Evidence ([ordered]@{
|
||||
controlType = $controlType
|
||||
patternAcquired = $false
|
||||
supportedPatterns = @($supported)
|
||||
verdict = 'pattern-unavailable'
|
||||
detail = $Detail
|
||||
})
|
||||
}
|
||||
|
||||
$status = 'ok'
|
||||
$message = ''
|
||||
$resultData = [ordered]@{}
|
||||
|
||||
try {
|
||||
Initialize-ProbeNative
|
||||
Initialize-InteractNative
|
||||
$scratchExe = Join-Path (Get-ProbeRoot) 'scratch\bin\ScratchForms.exe'
|
||||
if (-not (Test-Path -LiteralPath $scratchExe)) {
|
||||
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path (Get-ProbeRoot) 'scratch\build-scratch.ps1') | Out-Null
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $scratchExe)) { throw ('scratch fixture missing at ' + $scratchExe) }
|
||||
|
||||
$asciiPayload = 'probe-value-ascii-01'
|
||||
$cjkPayload = [char]::ConvertFromUtf32(0x4E2D) + [char]::ConvertFromUtf32(0x6587) + [char]::ConvertFromUtf32(0x30C6)
|
||||
$astralPayload = 'a' + [char]::ConvertFromUtf32(0x1F600) + 'z'
|
||||
$mixedPayload = $asciiPayload + '-' + $cjkPayload + '-' + $astralPayload
|
||||
|
||||
# --- WPF first: its automation peer binds once and never re-resolves ----
|
||||
$wpf = Start-ScratchWpfWindow -Tag 'u5' -Left 400 -Top 250
|
||||
if ($null -eq $wpf) { throw 'WPF scratch window never bound its automation peer across three launches' }
|
||||
$wpfRoot = $wpf.Element
|
||||
$wpfHandle = [IntPtr][int]$wpfRoot.Current.NativeWindowHandle
|
||||
|
||||
$wfDefault = Start-Tracked -FilePath $scratchExe -ArgumentList @('--tag', 'u5-default', '--pos', '0,0')
|
||||
if ($wfDefault.MainWindowHandle -eq [IntPtr]::Zero) { throw 'WinForms default-mode window never appeared' }
|
||||
$wfDefaultRoot = $AE::FromHandle($wfDefault.MainWindowHandle)
|
||||
|
||||
$wfHosted = Start-Tracked -FilePath $scratchExe -ArgumentList @('--tag', 'u5-hosted', '--pos', '860,0', '--host-providers')
|
||||
if ($wfHosted.MainWindowHandle -eq [IntPtr]::Zero) { throw 'WinForms host-providers window never appeared' }
|
||||
$wfHostedRoot = $AE::FromHandle($wfHosted.MainWindowHandle)
|
||||
|
||||
$targets = @(
|
||||
[pscustomobject]@{ Label = 'wpf'; Root = $wpfRoot },
|
||||
[pscustomobject]@{ Label = 'winforms-host-providers'; Root = $wfHostedRoot },
|
||||
[pscustomobject]@{ Label = 'winforms-default'; Root = $wfDefaultRoot }
|
||||
)
|
||||
|
||||
# --- invoke -------------------------------------------------------------
|
||||
foreach ($t in $targets) {
|
||||
$el = Find-Scratch -Root $t.Root -Name 'btnAction'
|
||||
$ip = $null
|
||||
if ($null -ne $el) { $ip = Get-ElementPattern -El $el -PatternIdentifier ([System.Windows.Automation.InvokePattern]::Pattern) }
|
||||
if ($null -eq $ip) {
|
||||
Add-PatternAbsentRow -Interaction 'invoke' -Target $t.Label -AutomationId 'btnAction' -Pattern 'Invoke' -El $el `
|
||||
-Detail 'no InvokePattern on this element in this fixture mode; recorded as a verdict row'
|
||||
continue
|
||||
}
|
||||
$before = Get-SinkText -Root $t.Root
|
||||
$ip.Invoke()
|
||||
Start-Sleep -Milliseconds 500
|
||||
$after = Get-SinkText -Root $t.Root
|
||||
Add-InteractionRow -Interaction 'invoke' -Target $t.Label -AutomationId 'btnAction' -Pattern 'Invoke' -Evidence ([ordered]@{
|
||||
controlType = ($el.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
|
||||
patternAcquired = $true
|
||||
supportedPatterns = @(Get-SupportedPatternNames -El $el)
|
||||
sinkElement = 'lblStatus (a different element than the one invoked)'
|
||||
sinkBefore = $before
|
||||
sinkAfter = $after
|
||||
changed = ($before -ne $after)
|
||||
verdict = $(if ($after -eq 'action:1') { 'ok' } else { 'unexpected-sink-value' })
|
||||
})
|
||||
}
|
||||
|
||||
# --- toggle -------------------------------------------------------------
|
||||
foreach ($t in $targets) {
|
||||
$el = Find-Scratch -Root $t.Root -Name 'chkToggle'
|
||||
$tp = $null
|
||||
if ($null -ne $el) { $tp = Get-ElementPattern -El $el -PatternIdentifier ([System.Windows.Automation.TogglePattern]::Pattern) }
|
||||
if ($null -eq $tp) {
|
||||
Add-PatternAbsentRow -Interaction 'toggle' -Target $t.Label -AutomationId 'chkToggle' -Pattern 'Toggle' -El $el `
|
||||
-Detail 'no TogglePattern on this element in this fixture mode; recorded as a verdict row'
|
||||
continue
|
||||
}
|
||||
$pre = [string]$tp.Current.ToggleState
|
||||
$sinkBefore = Get-SinkText -Root $t.Root
|
||||
$tp.Toggle()
|
||||
Start-Sleep -Milliseconds 500
|
||||
$post = [string](Get-ElementPattern -El (Find-Scratch -Root $t.Root -Name 'chkToggle') `
|
||||
-PatternIdentifier ([System.Windows.Automation.TogglePattern]::Pattern)).Current.ToggleState
|
||||
$sinkAfter = Get-SinkText -Root $t.Root
|
||||
Add-InteractionRow -Interaction 'toggle' -Target $t.Label -AutomationId 'chkToggle' -Pattern 'Toggle' -Evidence ([ordered]@{
|
||||
controlType = ($el.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
|
||||
patternAcquired = $true
|
||||
preState = $pre
|
||||
postState = $post
|
||||
reReadMethod = 'the element is re-found by AutomationId and its TogglePattern re-acquired, so the post-state is not read off the same pattern object the call returned'
|
||||
sinkBefore = $sinkBefore
|
||||
sinkAfter = $sinkAfter
|
||||
sinkChanged = ($sinkBefore -ne $sinkAfter)
|
||||
sinkNote = 'the WPF fixture updates lblStatus from a Click handler. TogglePattern.Toggle flips ToggleState without raising Click, so the sink is expected to stay silent here while the element state changes - which is exactly why the element is re-read instead of the sink being trusted as the only observable.'
|
||||
changed = ($pre -ne $post)
|
||||
verdict = $(if ($pre -eq 'Off' -and $post -eq 'On') { 'ok' } else { 'unexpected-state' })
|
||||
})
|
||||
}
|
||||
|
||||
# --- set value (ASCII, CJK, astral plane) -------------------------------
|
||||
$valueTargets = @(
|
||||
[pscustomobject]@{ Label = 'wpf'; Root = $wpfRoot },
|
||||
[pscustomobject]@{ Label = 'winforms-host-providers'; Root = $wfHostedRoot }
|
||||
)
|
||||
foreach ($t in $valueTargets) {
|
||||
$el = Find-Scratch -Root $t.Root -Name 'txtValue'
|
||||
$vp = $null
|
||||
if ($null -ne $el) { $vp = Get-ElementPattern -El $el -PatternIdentifier ([System.Windows.Automation.ValuePattern]::Pattern) }
|
||||
if ($null -eq $vp) {
|
||||
Add-PatternAbsentRow -Interaction 'set-value' -Target $t.Label -AutomationId 'txtValue' -Pattern 'Value' -El $el `
|
||||
-Detail 'no ValuePattern on this element in this fixture mode; recorded as a verdict row'
|
||||
continue
|
||||
}
|
||||
foreach ($payload in @(
|
||||
[pscustomobject]@{ Kind = 'ascii'; Text = $asciiPayload },
|
||||
[pscustomobject]@{ Kind = 'cjk'; Text = $cjkPayload },
|
||||
[pscustomobject]@{ Kind = 'astral-plane'; Text = $astralPayload },
|
||||
[pscustomobject]@{ Kind = 'mixed'; Text = $mixedPayload })) {
|
||||
$preValue = [string]$vp.Current.Value
|
||||
$vp.SetValue($payload.Text)
|
||||
Start-Sleep -Milliseconds 300
|
||||
$fresh = Find-Scratch -Root $t.Root -Name 'txtValue'
|
||||
$freshVp = Get-ElementPattern -El $fresh -PatternIdentifier ([System.Windows.Automation.ValuePattern]::Pattern)
|
||||
$observed = ''
|
||||
if ($null -ne $freshVp) { $observed = [string]$freshVp.Current.Value }
|
||||
$expectedShape = Get-TextShape -Text $payload.Text
|
||||
$observedShape = Get-TextShape -Text $observed
|
||||
Add-InteractionRow -Interaction ('set-value/' + $payload.Kind) -Target $t.Label -AutomationId 'txtValue' -Pattern 'Value' -Evidence ([ordered]@{
|
||||
controlType = ($el.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
|
||||
patternAcquired = $true
|
||||
payloadKind = $payload.Kind
|
||||
payloadOrigin = 'built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file''s encoding (R12)'
|
||||
preValueShape = (Get-TextShape -Text $preValue)
|
||||
expectedShape = $expectedShape
|
||||
observedShape = $observedShape
|
||||
exactRoundTrip = ($expectedShape.sha256Utf16 -eq $observedShape.sha256Utf16)
|
||||
reReadMethod = 'element re-found by AutomationId and ValuePattern re-acquired before reading'
|
||||
payloadHandling = 'only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture'
|
||||
verdict = $(if ($expectedShape.sha256Utf16 -eq $observedShape.sha256Utf16) { 'ok' } else { 'round-trip-mismatch' })
|
||||
})
|
||||
}
|
||||
$vp.SetValue('seed-value')
|
||||
}
|
||||
|
||||
# --- expand / collapse --------------------------------------------------
|
||||
foreach ($t in $targets) {
|
||||
$el = Find-Scratch -Root $t.Root -Name 'cboChoice'
|
||||
$ep = $null
|
||||
if ($null -ne $el) { $ep = Get-ElementPattern -El $el -PatternIdentifier ([System.Windows.Automation.ExpandCollapsePattern]::Pattern) }
|
||||
if ($null -eq $ep) {
|
||||
Add-PatternAbsentRow -Interaction 'expand-collapse' -Target $t.Label -AutomationId 'cboChoice' -Pattern 'ExpandCollapse' -El $el `
|
||||
-Detail 'no ExpandCollapsePattern on this element in this fixture mode; recorded as a verdict row'
|
||||
continue
|
||||
}
|
||||
$pre = [string]$ep.Current.ExpandCollapseState
|
||||
$ep.Expand()
|
||||
Start-Sleep -Milliseconds 600
|
||||
$expanded = [string](Get-ElementPattern -El (Find-Scratch -Root $t.Root -Name 'cboChoice') `
|
||||
-PatternIdentifier ([System.Windows.Automation.ExpandCollapsePattern]::Pattern)).Current.ExpandCollapseState
|
||||
$ep.Collapse()
|
||||
Start-Sleep -Milliseconds 600
|
||||
$collapsed = [string](Get-ElementPattern -El (Find-Scratch -Root $t.Root -Name 'cboChoice') `
|
||||
-PatternIdentifier ([System.Windows.Automation.ExpandCollapsePattern]::Pattern)).Current.ExpandCollapseState
|
||||
Add-InteractionRow -Interaction 'expand-collapse' -Target $t.Label -AutomationId 'cboChoice' -Pattern 'ExpandCollapse' -Evidence ([ordered]@{
|
||||
controlType = ($el.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
|
||||
patternAcquired = $true
|
||||
preState = $pre
|
||||
stateAfterExpand = $expanded
|
||||
stateAfterCollapse = $collapsed
|
||||
reReadMethod = 'state re-read after each half of the round trip from a freshly found element'
|
||||
roundTripped = ($expanded -eq 'Expanded' -and $collapsed -eq 'Collapsed')
|
||||
verdict = $(if ($expanded -eq 'Expanded' -and $collapsed -eq 'Collapsed') { 'ok' } else { 'round-trip-incomplete' })
|
||||
})
|
||||
}
|
||||
|
||||
# --- select -------------------------------------------------------------
|
||||
foreach ($t in $targets) {
|
||||
$list = Find-Scratch -Root $t.Root -Name 'lstItems'
|
||||
$item = $null
|
||||
if ($null -ne $list) {
|
||||
$item = Find-ScratchElement -Root $list -Symbolic 'lstItem-Item-Charlie'
|
||||
if ($null -eq $item) {
|
||||
$itemCondition = New-Object System.Windows.Automation.PropertyCondition($AE::ControlTypeProperty, [System.Windows.Automation.ControlType]::ListItem)
|
||||
$found = @($list.FindAll($Descendants, $itemCondition))
|
||||
if ($found.Count -ge 3) { $item = $found[2] }
|
||||
}
|
||||
}
|
||||
$sip = $null
|
||||
if ($null -ne $item) { $sip = Get-ElementPattern -El $item -PatternIdentifier ([System.Windows.Automation.SelectionItemPattern]::Pattern) }
|
||||
if ($null -eq $sip) {
|
||||
Add-PatternAbsentRow -Interaction 'select' -Target $t.Label -AutomationId 'lstItems/item[2]' -Pattern 'SelectionItem' -El $item `
|
||||
-Detail 'no ListItem with SelectionItemPattern reachable under lstItems in this fixture mode; recorded as a verdict row'
|
||||
continue
|
||||
}
|
||||
$selPattern = Get-ElementPattern -El $list -PatternIdentifier ([System.Windows.Automation.SelectionPattern]::Pattern)
|
||||
$preCount = -1
|
||||
if ($null -ne $selPattern) { $preCount = @($selPattern.Current.GetSelection()).Count }
|
||||
$sinkBefore = Get-SinkText -Root $t.Root
|
||||
$sip.Select()
|
||||
Start-Sleep -Milliseconds 500
|
||||
$postCount = -1
|
||||
$postIsSelected = $false
|
||||
$freshSel = Get-ElementPattern -El (Find-Scratch -Root $t.Root -Name 'lstItems') -PatternIdentifier ([System.Windows.Automation.SelectionPattern]::Pattern)
|
||||
if ($null -ne $freshSel) { $postCount = @($freshSel.Current.GetSelection()).Count }
|
||||
$freshItem = Get-ElementPattern -El $item -PatternIdentifier ([System.Windows.Automation.SelectionItemPattern]::Pattern)
|
||||
if ($null -ne $freshItem) { $postIsSelected = [bool]$freshItem.Current.IsSelected }
|
||||
$sinkAfter = Get-SinkText -Root $t.Root
|
||||
Add-InteractionRow -Interaction 'select' -Target $t.Label -AutomationId 'lstItems/item[2]' -Pattern 'SelectionItem' -Evidence ([ordered]@{
|
||||
controlType = ($item.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
|
||||
patternAcquired = $true
|
||||
selectionCountBefore = $preCount
|
||||
selectionCountAfter = $postCount
|
||||
isSelectedAfter = $postIsSelected
|
||||
sinkBefore = $sinkBefore
|
||||
sinkAfter = $sinkAfter
|
||||
reReadMethod = 'container SelectionPattern re-acquired from a freshly found list, plus the item IsSelected re-read, plus the independent lblStatus sink'
|
||||
verdict = $(if ($postIsSelected -and $postCount -eq 1) { 'ok' } else { 'unexpected-selection-state' })
|
||||
})
|
||||
}
|
||||
|
||||
# --- scroll via pattern -------------------------------------------------
|
||||
foreach ($t in @(
|
||||
[pscustomobject]@{ Label = 'winforms-host-providers'; Root = $wfHostedRoot },
|
||||
[pscustomobject]@{ Label = 'winforms-default'; Root = $wfDefaultRoot })) {
|
||||
$panel = Find-Scratch -Root $t.Root -Name 'pnlScroll'
|
||||
$sp = $null
|
||||
if ($null -ne $panel) { $sp = Get-ElementPattern -El $panel -PatternIdentifier ([System.Windows.Automation.ScrollPattern]::Pattern) }
|
||||
if ($null -eq $sp) {
|
||||
Add-PatternAbsentRow -Interaction 'scroll-via-pattern' -Target $t.Label -AutomationId 'pnlScroll' -Pattern 'Scroll' -El $panel `
|
||||
-Detail 'the UIA3 COM census (captures/08-uia3-com/census.json) records Scroll on this exact provider in BOTH fixture modes; the managed client cannot acquire it. Divergence row, not a platform verdict.'
|
||||
continue
|
||||
}
|
||||
$sinkBefore = Get-SinkText -Root $t.Root -Name 'lblScrollPos'
|
||||
$percentBefore = [double]$sp.Current.VerticalScrollPercent
|
||||
$sp.Scroll([System.Windows.Automation.ScrollAmount]::NoAmount, [System.Windows.Automation.ScrollAmount]::LargeIncrement)
|
||||
Start-Sleep -Milliseconds 700
|
||||
$freshSp = Get-ElementPattern -El (Find-Scratch -Root $t.Root -Name 'pnlScroll') -PatternIdentifier ([System.Windows.Automation.ScrollPattern]::Pattern)
|
||||
$percentAfter = $percentBefore
|
||||
if ($null -ne $freshSp) { $percentAfter = [double]$freshSp.Current.VerticalScrollPercent }
|
||||
$sinkAfter = Get-SinkText -Root $t.Root -Name 'lblScrollPos'
|
||||
Add-InteractionRow -Interaction 'scroll-via-pattern' -Target $t.Label -AutomationId 'pnlScroll' -Pattern 'Scroll' -Evidence ([ordered]@{
|
||||
controlType = ($panel.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
|
||||
patternAcquired = $true
|
||||
supportedPatterns = @(Get-SupportedPatternNames -El $panel)
|
||||
verticalPercentBefore = [Math]::Round($percentBefore, 2)
|
||||
verticalPercentAfter = [Math]::Round($percentAfter, 2)
|
||||
sinkElement = 'lblScrollPos, driven by a 100 ms timer inside the fixture, so it registers pattern, wheel and keyboard scrolling alike'
|
||||
sinkBefore = $sinkBefore
|
||||
sinkAfter = $sinkAfter
|
||||
changed = ($sinkBefore -ne $sinkAfter)
|
||||
wheelCounterpart = 'the SendInput wheel arm on this same control and this same sink is recorded separately in captures/06-input-synthesis/mouse.json'
|
||||
verdict = $(if ($sinkBefore -ne $sinkAfter) { 'ok' } else { 'no-observed-scroll' })
|
||||
})
|
||||
}
|
||||
|
||||
$wpfList = Find-Scratch -Root $wpfRoot -Name 'lstItems'
|
||||
$wpfScroll = $null
|
||||
if ($null -ne $wpfList) { $wpfScroll = Get-ElementPattern -El $wpfList -PatternIdentifier ([System.Windows.Automation.ScrollPattern]::Pattern) }
|
||||
if ($null -eq $wpfScroll) {
|
||||
Add-PatternAbsentRow -Interaction 'scroll-via-pattern' -Target 'wpf' -AutomationId 'lstItems' -Pattern 'Scroll' -El $wpfList -Detail 'ScrollPattern not acquirable'
|
||||
} else {
|
||||
$shrinkSteps = @(240, 190, 160)
|
||||
$stepUsed = 0
|
||||
foreach ($h in $shrinkSteps) {
|
||||
if ([bool]$wpfScroll.Current.VerticallyScrollable) { break }
|
||||
Show-WindowNoActivate -WindowHandle $wpfHandle -X 400 -Y 250 -Width 460 -Height $h
|
||||
Start-Sleep -Milliseconds 700
|
||||
$wpfList = Find-Scratch -Root $wpfRoot -Name 'lstItems'
|
||||
$wpfScroll = Get-ElementPattern -El $wpfList -PatternIdentifier ([System.Windows.Automation.ScrollPattern]::Pattern)
|
||||
$stepUsed = $h
|
||||
}
|
||||
$scrollable = [bool]$wpfScroll.Current.VerticallyScrollable
|
||||
$percentBefore = [double]$wpfScroll.Current.VerticalScrollPercent
|
||||
$verdict = 'not-scrollable'
|
||||
$percentAfter = $percentBefore
|
||||
if ($scrollable) {
|
||||
$wpfScroll.SetScrollPercent([System.Windows.Automation.ScrollPattern]::NoScroll, 100.0)
|
||||
Start-Sleep -Milliseconds 600
|
||||
$fresh = Get-ElementPattern -El (Find-Scratch -Root $wpfRoot -Name 'lstItems') -PatternIdentifier ([System.Windows.Automation.ScrollPattern]::Pattern)
|
||||
if ($null -ne $fresh) { $percentAfter = [double]$fresh.Current.VerticalScrollPercent }
|
||||
$verdict = $(if ($percentAfter -gt $percentBefore) { 'ok' } else { 'no-observed-scroll' })
|
||||
}
|
||||
Add-InteractionRow -Interaction 'scroll-via-pattern' -Target 'wpf' -AutomationId 'lstItems' -Pattern 'Scroll' -Evidence ([ordered]@{
|
||||
controlType = 'List'
|
||||
patternAcquired = $true
|
||||
makeScrollableMethod = 'the window is shrunk with SetWindowPos(SWP_NOACTIVATE) until the ListBox viewport is smaller than its five items; the client-height step that achieved it is recorded'
|
||||
shrinkStepUsed = $stepUsed
|
||||
verticallyScrollable = $scrollable
|
||||
verticalPercentBefore = [Math]::Round($percentBefore, 2)
|
||||
verticalPercentAfter = [Math]::Round($percentAfter, 2)
|
||||
reReadMethod = 'percent re-read from a freshly acquired ScrollPattern on a freshly found element'
|
||||
verdict = $verdict
|
||||
})
|
||||
}
|
||||
Show-WindowNoActivate -WindowHandle $wpfHandle -X 400 -Y 250 -Width 460 -Height 430
|
||||
Start-Sleep -Milliseconds 500
|
||||
|
||||
[void](Write-ProbeJson -Probe $Probe -Name 'interactions.json' -InputObject ([ordered]@{
|
||||
probe = $Probe
|
||||
question = 'does every 2.0(3) interaction actually take effect through a UIA pattern, verified by independent re-read rather than by the call return'
|
||||
stack = 'managed-System.Windows.Automation'
|
||||
scope = 'app/provider'
|
||||
verificationDiscipline = 'no row trusts the pattern call return. Element state is re-read after re-finding the element by AutomationId, and where the fixture exposes an independent sink control (lblStatus, lblScrollPos) that separate element is read as well.'
|
||||
fixtureArtifactNote = 'winforms-default rows report pattern-unavailable because the fixture installs a server-side IRawElementProviderSimple that suppresses the client-side proxies and WinForms'' own providers. That is a property of this fixture, not of WinForms or of Windows. winforms-host-providers rows show what WinForms itself exposes to a managed client.'
|
||||
managedVsComNote = 'several winforms-host-providers rows report pattern-unavailable for patterns the UIA3 COM census in captures/08-uia3-com/census.json records on the same providers (Invoke and Toggle on chkToggle, Value on txtValue, SelectionItem on the ListItems, Scroll on pnlScroll). Under KTD1 the COM row is the product-relevant one because the Rust adapter wraps a UIA3 COM client; these rows are filed as managed-client divergence, not as absent affordances. ExpandCollapse on cboChoice is the one WinForms pattern both stacks agree on, which is why it is the only non-WPF ok row here.'
|
||||
wheelCounterpartNote = 'the SendInput wheel arm is deliberately not in this file. It runs in 06-input-synthesis against the WinForms pnlScroll and its lblScrollPos sink, because that is the control this probe could NOT drive by pattern - the pair of the two captures is the pattern-vs-physical comparison 2.6 needs.'
|
||||
rows = @($script:Rows)
|
||||
}))
|
||||
|
||||
# --- text pattern -------------------------------------------------------
|
||||
$textRows = New-Object System.Collections.ArrayList
|
||||
$wpfEdit = Find-Scratch -Root $wpfRoot -Name 'txtValue'
|
||||
$wpfText = Get-ElementPattern -El $wpfEdit -PatternIdentifier ([System.Windows.Automation.TextPattern]::Pattern)
|
||||
if ($null -ne $wpfText) {
|
||||
$wpfValue = Get-ElementPattern -El $wpfEdit -PatternIdentifier ([System.Windows.Automation.ValuePattern]::Pattern)
|
||||
$wpfValue.SetValue($mixedPayload)
|
||||
Start-Sleep -Milliseconds 300
|
||||
$documentText = $wpfText.DocumentRange.GetText(-1)
|
||||
$selectionBefore = @($wpfText.GetSelection())
|
||||
$range = $wpfText.DocumentRange.Clone()
|
||||
[void]$range.MoveEndpointByUnit([System.Windows.Automation.Text.TextPatternRangeEndpoint]::Start, [System.Windows.Automation.Text.TextUnit]::Character, 2)
|
||||
[void]$range.MoveEndpointByRange([System.Windows.Automation.Text.TextPatternRangeEndpoint]::End, $range, [System.Windows.Automation.Text.TextPatternRangeEndpoint]::Start)
|
||||
[void]$range.MoveEndpointByUnit([System.Windows.Automation.Text.TextPatternRangeEndpoint]::End, [System.Windows.Automation.Text.TextUnit]::Character, 5)
|
||||
$range.Select()
|
||||
Start-Sleep -Milliseconds 300
|
||||
$selectionAfter = @($wpfText.GetSelection())
|
||||
$selectedText = ''
|
||||
if ($selectionAfter.Count -gt 0) { $selectedText = $selectionAfter[0].GetText(-1) }
|
||||
$caret = $wpfText.DocumentRange.Clone()
|
||||
[void]$caret.MoveEndpointByUnit([System.Windows.Automation.Text.TextPatternRangeEndpoint]::Start, [System.Windows.Automation.Text.TextUnit]::Character, 4)
|
||||
[void]$caret.MoveEndpointByRange([System.Windows.Automation.Text.TextPatternRangeEndpoint]::End, $caret, [System.Windows.Automation.Text.TextPatternRangeEndpoint]::Start)
|
||||
$caret.Select()
|
||||
Start-Sleep -Milliseconds 300
|
||||
$caretSelection = @($wpfText.GetSelection())
|
||||
$caretText = ''
|
||||
if ($caretSelection.Count -gt 0) { $caretText = $caretSelection[0].GetText(-1) }
|
||||
[void]$textRows.Add([ordered]@{
|
||||
target = 'wpf-txtValue'
|
||||
stack = 'managed-System.Windows.Automation'
|
||||
controlType = 'Edit'
|
||||
className = [string]$wpfEdit.Current.ClassName
|
||||
supportedPatterns = @(Get-SupportedPatternNames -El $wpfEdit)
|
||||
textPatternExposed = $true
|
||||
getText = [ordered]@{
|
||||
expectedShape = (Get-TextShape -Text $mixedPayload)
|
||||
observedShape = (Get-TextShape -Text $documentText)
|
||||
exactMatch = ((Get-TextShape -Text $mixedPayload).sha256Utf16 -eq (Get-TextShape -Text $documentText).sha256Utf16)
|
||||
note = 'the value was set through ValuePattern and read back through TextPattern.DocumentRange.GetText - a cross-pattern read, not the same call returning its own argument'
|
||||
}
|
||||
selection = [ordered]@{
|
||||
rangesBefore = $selectionBefore.Count
|
||||
rangesAfter = $selectionAfter.Count
|
||||
requestedUnits = 5
|
||||
selectedShape = (Get-TextShape -Text $selectedText)
|
||||
matchesSubstring = ($selectedText -eq $mixedPayload.Substring(2, 5))
|
||||
}
|
||||
caret = [ordered]@{
|
||||
method = 'a degenerate range (End collapsed onto Start) is Selected at character offset 4 and the selection is re-read'
|
||||
rangesAfter = $caretSelection.Count
|
||||
degenerate = ($caretText.Length -eq 0)
|
||||
}
|
||||
insert = [ordered]@{
|
||||
method = 'TextPattern is read-only by contract; insertion is done through ValuePattern.SetValue and verified through the TextPattern read above'
|
||||
verified = ((Get-TextShape -Text $mixedPayload).sha256Utf16 -eq (Get-TextShape -Text $documentText).sha256Utf16)
|
||||
}
|
||||
})
|
||||
$wpfValue.SetValue('seed-value')
|
||||
}
|
||||
|
||||
$notepad = Start-Tracked -FilePath (Join-Path $env:WINDIR 'System32\notepad.exe')
|
||||
if ($notepad.MainWindowHandle -ne [IntPtr]::Zero) {
|
||||
Show-WindowNoActivate -WindowHandle $notepad.MainWindowHandle -X 900 -Y 300 -Width 600 -Height 400
|
||||
Start-Sleep -Milliseconds 900
|
||||
$notepadRoot = $AE::FromHandle($notepad.MainWindowHandle)
|
||||
$editHandle = [AgentDesktopProbe.Interact]::FindWindowExW($notepad.MainWindowHandle, [IntPtr]::Zero, 'Edit', $null)
|
||||
$probes = New-Object System.Collections.ArrayList
|
||||
$documentCondition = New-Object System.Windows.Automation.PropertyCondition($AE::ControlTypeProperty, [System.Windows.Automation.ControlType]::Document)
|
||||
[void]$probes.Add([pscustomobject]@{ How = 'FindFirst(Descendants, ControlType=Document)'; El = $notepadRoot.FindFirst($Descendants, $documentCondition) })
|
||||
if ($editHandle -ne [IntPtr]::Zero) {
|
||||
[void]$probes.Add([pscustomobject]@{ How = 'AutomationElement.FromHandle(the Edit child HWND found with FindWindowEx)'; El = $AE::FromHandle($editHandle) })
|
||||
}
|
||||
foreach ($p in $probes) {
|
||||
$row = [ordered]@{
|
||||
target = 'notepad-edit'
|
||||
stack = 'managed-System.Windows.Automation'
|
||||
lookup = $p.How
|
||||
resolved = ($null -ne $p.El)
|
||||
}
|
||||
if ($null -ne $p.El) {
|
||||
$row['controlType'] = ($p.El.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '')
|
||||
$row['className'] = [string]$p.El.Current.ClassName
|
||||
$row['supportedPatterns'] = @(Get-SupportedPatternNames -El $p.El)
|
||||
foreach ($pattern in @(
|
||||
[pscustomobject]@{ Name = 'Text'; Id = [System.Windows.Automation.TextPattern]::Pattern },
|
||||
[pscustomobject]@{ Name = 'Value'; Id = [System.Windows.Automation.ValuePattern]::Pattern },
|
||||
[pscustomobject]@{ Name = 'Scroll'; Id = [System.Windows.Automation.ScrollPattern]::Pattern })) {
|
||||
$row[('tryGetCurrentPattern_' + $pattern.Name)] = ($null -ne (Get-ElementPattern -El $p.El -PatternIdentifier $pattern.Id))
|
||||
}
|
||||
}
|
||||
[void]$textRows.Add($row)
|
||||
}
|
||||
}
|
||||
|
||||
[void](Write-ProbeJson -Probe $Probe -Name 'text-pattern.json' -InputObject ([ordered]@{
|
||||
probe = $Probe
|
||||
question = 'is TextPattern exposed on a classic Win32 Edit on Server 2019 at all, and does the managed stack agree with the COM stack about it'
|
||||
stack = 'managed-System.Windows.Automation'
|
||||
scope = 'app/provider'
|
||||
whyTwoLookups = 'GetSupportedPatterns is not a reliable negative: it is answered by the client-side proxy that happens to be bound. Every row therefore also records TryGetCurrentPattern per pattern, which is the call the adapter would actually make. Both lookups are recorded because they disagree.'
|
||||
comCounterpart = 'captures/08-uia3-com/census.json records the SAME notepad window through hand-declared UIA3 COM as one Document element carrying LegacyIAccessible, Scroll, Text, Text2 and Value. Any managed row below that reports Text absent is a client-stack divergence, not an absence of the provider.'
|
||||
p2o12Relevance = '2.0(3) text get/selection/caret/insert and P2-O12 both assume a Text-capable Edit. The WPF row shows what a full managed TextPattern surface supports; the notepad rows show what the real Win32 Edit gives the two client stacks.'
|
||||
rows = @($textRows)
|
||||
}))
|
||||
|
||||
# --- focus vs foreground ------------------------------------------------
|
||||
[void][AgentDesktopProbe.Native]::ShowWindow($wfDefault.MainWindowHandle, 6)
|
||||
Start-Sleep -Milliseconds 500
|
||||
[void][AgentDesktopProbe.Native]::ShowWindow($wfDefault.MainWindowHandle, 9)
|
||||
Start-Sleep -Milliseconds 900
|
||||
$fgPidBefore = [AgentDesktopProbe.Native]::GetForegroundProcessId()
|
||||
$fgTitleBefore = [AgentDesktopProbe.Interact]::ForegroundTitle()
|
||||
$focusTarget = Find-Scratch -Root $wpfRoot -Name 'txtValue'
|
||||
$focusError = ''
|
||||
$hadFocusBefore = $false
|
||||
try { $hadFocusBefore = [bool]$focusTarget.Current.HasKeyboardFocus } catch { }
|
||||
try { $focusTarget.SetFocus() } catch { $focusError = ($_.Exception.Message -replace '[\r\n]+', ' ') }
|
||||
Start-Sleep -Milliseconds 900
|
||||
$fgPidAfter = [AgentDesktopProbe.Native]::GetForegroundProcessId()
|
||||
$fgTitleAfter = [AgentDesktopProbe.Interact]::ForegroundTitle()
|
||||
$hasFocusAfter = $false
|
||||
try { $hasFocusAfter = [bool](Find-Scratch -Root $wpfRoot -Name 'txtValue').Current.HasKeyboardFocus } catch { }
|
||||
$focusedAutomationId = ''
|
||||
try { $focusedAutomationId = [string]$AE::FocusedElement.Current.AutomationId } catch { }
|
||||
|
||||
[void](Write-ProbeJson -Probe $Probe -Name 'focus.json' -InputObject ([ordered]@{
|
||||
probe = $Probe
|
||||
question = 'does AutomationElement.SetFocus on a background window move the desktop foreground, or only keyboard focus inside that window'
|
||||
why = '2.7 wants headless interaction. If SetFocus steals foreground, no interaction that needs focus can be headless; if it does not, focus-dependent actions are headless-safe and the design can rely on it.'
|
||||
method = 'a second probe-owned scratch window is brought forward with ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) - the sanctioned pattern, SetForegroundWindow is never called - then SetFocus is issued on an element of the OTHER probe-owned window and the foreground is re-read.'
|
||||
foregroundBefore = [ordered]@{ windowTitle = $fgTitleBefore; processId = $fgPidBefore }
|
||||
foregroundAfter = [ordered]@{ windowTitle = $fgTitleAfter; processId = $fgPidAfter }
|
||||
foregroundChanged = ($fgPidBefore -ne $fgPidAfter)
|
||||
foregroundWasTheOtherScratchWindow = ($fgTitleBefore -like 'AgentDesktop Scratch WinForms*')
|
||||
setFocusTarget = 'wpf txtValue'
|
||||
setFocusError = $focusError
|
||||
hadKeyboardFocusBefore = $hadFocusBefore
|
||||
hasKeyboardFocusAfter = $hasFocusAfter
|
||||
focusedElementAutomationIdAfter = $focusedAutomationId
|
||||
titleEvidenceNote = 'the two foreground observations are nested objects with a key named exactly processId, so the KTD9 normalizer canonicalizes the run-varying pid to <pid>. A flat foregroundPidBefore/foregroundPidAfter pair was tried first and does NOT match the normalizer''s word-boundary rule: the raw pids survived into the twin and the capture failed to reproduce across runs. The probe-owned window titles are what carry the fact into the normalized twin.'
|
||||
verdict = $(if ($fgPidBefore -ne $fgPidAfter) { 'SetFocus moved the desktop foreground' } else { 'SetFocus did not move the desktop foreground' })
|
||||
}))
|
||||
|
||||
$ok = @($script:Rows | Where-Object { $_.verdict -eq 'ok' }).Count
|
||||
$unavailable = @($script:Rows | Where-Object { $_.verdict -eq 'pattern-unavailable' }).Count
|
||||
$resultData['interactionRows'] = @($script:Rows).Count
|
||||
$resultData['okRows'] = $ok
|
||||
$resultData['patternUnavailableRows'] = $unavailable
|
||||
$resultData['textRows'] = @($textRows).Count
|
||||
$resultData['foregroundChangedOnSetFocus'] = ($fgPidBefore -ne $fgPidAfter)
|
||||
$message = 'interactions: ' + $ok + ' ok / ' + $unavailable + ' pattern-unavailable of ' + @($script:Rows).Count + ' rows'
|
||||
} catch {
|
||||
$status = 'fail'
|
||||
$message = ($_.Exception.Message -replace '[\r\n]+', ' ')
|
||||
Write-ProbeLog -Message ('probe failed: ' + $message) -Level 'error'
|
||||
} finally {
|
||||
foreach ($id in @($script:Spawned)) {
|
||||
try { Stop-ScratchProcess -ProcessId $id } catch { Write-ProbeLog -Message ('teardown: ' + $_.Exception.Message) -Level 'warn' }
|
||||
}
|
||||
foreach ($f in @(Get-ChildItem -LiteralPath (Get-CaptureDir -Probe $Probe) -File -ErrorAction SilentlyContinue)) {
|
||||
if ($f.Name -like '*.normalized') { continue }
|
||||
if (-not (Test-CaptureRedaction -Path $f.FullName)) { $status = 'fail'; $message = ('redaction residue in ' + $f.Name) }
|
||||
}
|
||||
}
|
||||
|
||||
Write-ProbeResult -Probe $Probe -Status $status -Message $message -Data $resultData
|
||||
if ($status -eq 'fail') { exit 1 }
|
||||
exit 0
|
||||
860
probes/windows/06-input-synthesis.ps1
Normal file
860
probes/windows/06-input-synthesis.ps1
Normal file
|
|
@ -0,0 +1,860 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Probe 06 (sub-phase 2.0, unit U6): SendInput keyboard and mouse synthesis, the
|
||||
PostMessage WM_KEYDOWN control probe behind Engineering Invariant #5, and the
|
||||
teardown proof that the desktop is left exactly as it was found.
|
||||
|
||||
.DESCRIPTION
|
||||
Stack: Win32 (SendInput, PostMessage, WM_GETTEXT) plus a bounded managed UIA read
|
||||
used only as the Chromium observable. Scope: system.
|
||||
|
||||
Every observation of the scratch fixture is a Win32 read - GetDlgItem by control id
|
||||
plus WM_GETTEXT - so the evidence does not depend on any UIA provider. That matters
|
||||
here: 05-interactions measured that a managed client sees ZERO patterns on this
|
||||
fixture in default mode, so a UIA-based re-read would have been unable to tell a
|
||||
failed injection from an unreadable control.
|
||||
|
||||
KTD5 safety envelope, enforced not asserted:
|
||||
* every SendInput call is bracketed by Assert-Foreground before AND after;
|
||||
* on mismatch the probe stops injecting, files an interference row, and never
|
||||
re-injects;
|
||||
* the scratch window is brought forward with ShowWindow(SW_MINIMIZE) then
|
||||
ShowWindow(SW_RESTORE) on the probe's own window - SetForegroundWindow is never
|
||||
called;
|
||||
* the clipboard and the cursor position are snapshotted before and restored after,
|
||||
and both restorations are verified;
|
||||
* the modifier sweep is conditional: modifier key state is read first and a key-up
|
||||
is injected only for a modifier actually found down, so the normal path performs
|
||||
no ungated injection at all.
|
||||
|
||||
Payload discipline: typed text and clipboard content never reach a capture. Only
|
||||
UTF-16 unit counts, codepoint counts, surrogate-pair counts and a SHA-256 of the
|
||||
UTF-16 bytes are recorded, and clipboard shape is compared against the hash of the
|
||||
string the probe itself typed - never against the observed clipboard value.
|
||||
|
||||
Captures under captures/06-input-synthesis/:
|
||||
keyboard.json unicode typing round trips, chord probe, modifier state
|
||||
mouse.json click / absolute move / wheel / drag, all coordinate-driven
|
||||
postmessage.json WM_KEYDOWN + WM_KEYUP against the scratch control and Chromium
|
||||
teardown.json clipboard, cursor and modifier restoration evidence
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. "$PSScriptRoot\common.ps1"
|
||||
|
||||
Add-Type -AssemblyName System.Windows.Forms | Out-Null
|
||||
Add-Type -AssemblyName UIAutomationClient | Out-Null
|
||||
Add-Type -AssemblyName UIAutomationTypes | Out-Null
|
||||
|
||||
$Probe = '06-input-synthesis'
|
||||
$AE = [System.Windows.Automation.AutomationElement]
|
||||
$RawWalker = [System.Windows.Automation.TreeWalker]::RawViewWalker
|
||||
$Id = @{ chkToggle = 1001; txtValue = 1003; cboChoice = 1004; btnAction = 1005
|
||||
btnMutateList = 1006; btnZeroSize = 1007; tbSlider = 1008; lstItems = 1010
|
||||
pnlScroll = 1011; lblStatus = 1020; lblScrollPos = 1022; lblSliderValue = 1023 }
|
||||
$Modifiers = @(
|
||||
[pscustomobject]@{ Name = 'VK_SHIFT'; Vk = 0x10 }, [pscustomobject]@{ Name = 'VK_CONTROL'; Vk = 0x11 },
|
||||
[pscustomobject]@{ Name = 'VK_MENU'; Vk = 0x12 }, [pscustomobject]@{ Name = 'VK_LWIN'; Vk = 0x5B },
|
||||
[pscustomobject]@{ Name = 'VK_RWIN'; Vk = 0x5C }, [pscustomobject]@{ Name = 'VK_LSHIFT'; Vk = 0xA0 },
|
||||
[pscustomobject]@{ Name = 'VK_RSHIFT'; Vk = 0xA1 }, [pscustomobject]@{ Name = 'VK_LCONTROL'; Vk = 0xA2 },
|
||||
[pscustomobject]@{ Name = 'VK_RCONTROL'; Vk = 0xA3 }, [pscustomobject]@{ Name = 'VK_LMENU'; Vk = 0xA4 },
|
||||
[pscustomobject]@{ Name = 'VK_RMENU'; Vk = 0xA5 })
|
||||
$script:Spawned = New-Object System.Collections.ArrayList
|
||||
$script:TargetPid = 0
|
||||
$script:Interference = $null
|
||||
$script:Injections = 0
|
||||
|
||||
function Initialize-InjectorNative {
|
||||
if ('AgentDesktopProbe.Injector' -as [type]) { return }
|
||||
$src = @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace AgentDesktopProbe {
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct ProbeRect { public int Left; public int Top; public int Right; public int Bottom; }
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct ProbePoint { public int X; public int Y; }
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct MouseInput { public int dx; public int dy; public uint mouseData; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct KeybdInput { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public struct InputUnion {
|
||||
[FieldOffset(0)] public MouseInput mi;
|
||||
[FieldOffset(0)] public KeybdInput ki;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct ProbeInput { public uint type; public InputUnion u; }
|
||||
|
||||
public static class Injector {
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern uint SendInput(uint nInputs, ProbeInput[] pInputs, int cbSize);
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern IntPtr GetDlgItem(IntPtr hDlg, int nIDDlgItem);
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool GetWindowRect(IntPtr hWnd, out ProbeRect lpRect);
|
||||
[DllImport("user32.dll", EntryPoint = "SendMessageW", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr SendMessageBuffer(IntPtr hWnd, uint msg, IntPtr wParam, StringBuilder lParam);
|
||||
[DllImport("user32.dll", EntryPoint = "SendMessageW", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr SendMessageString(IntPtr hWnd, uint msg, IntPtr wParam, string lParam);
|
||||
[DllImport("user32.dll", EntryPoint = "SendMessageW", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr SendMessagePtr(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool PostMessageW(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
public static extern IntPtr FindWindowExW(IntPtr parent, IntPtr child, string cls, string title);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool GetCursorPos(out ProbePoint lpPoint);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool SetCursorPos(int X, int Y);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern int GetSystemMetrics(int nIndex);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern short GetAsyncKeyState(int vKey);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern short GetKeyState(int nVirtKey);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern uint MapVirtualKeyW(uint uCode, uint uMapType);
|
||||
|
||||
public static int InputSize() { return Marshal.SizeOf(typeof(ProbeInput)); }
|
||||
|
||||
public static string GetControlText(IntPtr h) {
|
||||
IntPtr len = SendMessagePtr(h, 0x000E, IntPtr.Zero, IntPtr.Zero);
|
||||
int cap = (int)len + 2;
|
||||
if (cap < 8) { cap = 8; }
|
||||
StringBuilder sb = new StringBuilder(cap);
|
||||
SendMessageBuffer(h, 0x000D, new IntPtr(cap), sb);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static void SetControlText(IntPtr h, string value) {
|
||||
SendMessageString(h, 0x000C, IntPtr.Zero, value);
|
||||
}
|
||||
|
||||
public static string PostKey(IntPtr h, uint msg, int vk) {
|
||||
uint scan = MapVirtualKeyW((uint)vk, 0);
|
||||
long lp = ((long)scan << 16) | 1L;
|
||||
if (msg == 0x0101) { lp = lp | 0xC0000000L; }
|
||||
bool ok = PostMessageW(h, msg, new IntPtr(vk), new IntPtr(lp));
|
||||
int err = Marshal.GetLastWin32Error();
|
||||
return ok ? "true" : ("false:" + err.ToString());
|
||||
}
|
||||
|
||||
public static string PostChar(IntPtr h, int ch) {
|
||||
bool ok = PostMessageW(h, 0x0102, new IntPtr(ch), new IntPtr(1));
|
||||
int err = Marshal.GetLastWin32Error();
|
||||
return ok ? "true" : ("false:" + err.ToString());
|
||||
}
|
||||
|
||||
public delegate bool EnumWindowProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool EnumChildWindows(IntPtr hWnd, EnumWindowProc callback, IntPtr lParam);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetClassNameW(IntPtr hWnd, StringBuilder buffer, int maxCount);
|
||||
|
||||
public static string ClassOf(IntPtr h) {
|
||||
StringBuilder sb = new StringBuilder(256);
|
||||
GetClassNameW(h, sb, sb.Capacity);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string ChildClassCensus(IntPtr parent) {
|
||||
System.Collections.Generic.List<string> seen = new System.Collections.Generic.List<string>();
|
||||
EnumChildWindows(parent, delegate(IntPtr h, IntPtr l) {
|
||||
string c = ClassOf(h);
|
||||
if (!seen.Contains(c)) { seen.Add(c); }
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
seen.Sort();
|
||||
return string.Join(";", seen.ToArray());
|
||||
}
|
||||
|
||||
public static IntPtr FindDescendantByClass(IntPtr parent, string wanted) {
|
||||
IntPtr found = IntPtr.Zero;
|
||||
EnumChildWindows(parent, delegate(IntPtr h, IntPtr l) {
|
||||
if (found != IntPtr.Zero) { return false; }
|
||||
if (ClassOf(h) == wanted) { found = h; return false; }
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
return found;
|
||||
}
|
||||
|
||||
public static uint SendUnicodeUnit(ushort unit) {
|
||||
ProbeInput[] inputs = new ProbeInput[2];
|
||||
inputs[0].type = 1;
|
||||
inputs[0].u.ki.wScan = unit;
|
||||
inputs[0].u.ki.dwFlags = 0x0004;
|
||||
inputs[1].type = 1;
|
||||
inputs[1].u.ki.wScan = unit;
|
||||
inputs[1].u.ki.dwFlags = 0x0004 | 0x0002;
|
||||
return SendInput(2, inputs, InputSize());
|
||||
}
|
||||
|
||||
public static uint SendVirtualKey(ushort vk, bool keyUp) {
|
||||
ProbeInput[] inputs = new ProbeInput[1];
|
||||
inputs[0].type = 1;
|
||||
inputs[0].u.ki.wVk = vk;
|
||||
inputs[0].u.ki.dwFlags = keyUp ? (uint)0x0002 : (uint)0;
|
||||
return SendInput(1, inputs, InputSize());
|
||||
}
|
||||
|
||||
public static uint MouseMoveAbsolute(int x, int y) {
|
||||
int cx = GetSystemMetrics(0);
|
||||
int cy = GetSystemMetrics(1);
|
||||
ProbeInput[] inputs = new ProbeInput[1];
|
||||
inputs[0].type = 0;
|
||||
inputs[0].u.mi.dx = (int)(((double)x * 65535.0) / (double)(cx - 1));
|
||||
inputs[0].u.mi.dy = (int)(((double)y * 65535.0) / (double)(cy - 1));
|
||||
inputs[0].u.mi.dwFlags = 0x0001 | 0x8000;
|
||||
return SendInput(1, inputs, InputSize());
|
||||
}
|
||||
|
||||
public static uint MouseButton(bool down) {
|
||||
ProbeInput[] inputs = new ProbeInput[1];
|
||||
inputs[0].type = 0;
|
||||
inputs[0].u.mi.dwFlags = down ? (uint)0x0002 : (uint)0x0004;
|
||||
return SendInput(1, inputs, InputSize());
|
||||
}
|
||||
|
||||
public static uint MouseWheel(int delta) {
|
||||
ProbeInput[] inputs = new ProbeInput[1];
|
||||
inputs[0].type = 0;
|
||||
inputs[0].u.mi.mouseData = unchecked((uint)delta);
|
||||
inputs[0].u.mi.dwFlags = 0x0800;
|
||||
return SendInput(1, inputs, InputSize());
|
||||
}
|
||||
}
|
||||
}
|
||||
'@
|
||||
Add-Type -TypeDefinition $src -Language CSharp | Out-Null
|
||||
}
|
||||
|
||||
function Get-TextShape {
|
||||
param([Parameter(Mandatory = $true)][AllowEmptyString()][AllowNull()][string]$Text)
|
||||
if ($null -eq $Text) { $Text = '' }
|
||||
$sha = [System.Security.Cryptography.SHA256]::Create()
|
||||
$hash = (($sha.ComputeHash([System.Text.Encoding]::Unicode.GetBytes($Text))) | ForEach-Object { $_.ToString('x2') }) -join ''
|
||||
$sha.Dispose()
|
||||
$codepoints = 0
|
||||
$pairs = 0
|
||||
$replacement = 0
|
||||
for ($i = 0; $i -lt $Text.Length; $i++) {
|
||||
if ($Text[$i] -eq [char]0xFFFD) { $replacement++ }
|
||||
$codepoints++
|
||||
if ([char]::IsHighSurrogate($Text[$i]) -and ($i + 1) -lt $Text.Length -and [char]::IsLowSurrogate($Text[$i + 1])) {
|
||||
$pairs++
|
||||
$i++
|
||||
}
|
||||
}
|
||||
return [ordered]@{
|
||||
utf16Units = $Text.Length
|
||||
codepoints = $codepoints
|
||||
surrogatePairs = $pairs
|
||||
replacementChars = $replacement
|
||||
sha256Utf16 = $hash
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ControlHandle {
|
||||
param([IntPtr]$Form, [string]$Name)
|
||||
return [AgentDesktopProbe.Injector]::GetDlgItem($Form, $Id[$Name])
|
||||
}
|
||||
|
||||
function Get-ControlText {
|
||||
param([IntPtr]$Handle)
|
||||
if ($Handle -eq [IntPtr]::Zero) { return '<no-handle>' }
|
||||
return [AgentDesktopProbe.Injector]::GetControlText($Handle)
|
||||
}
|
||||
|
||||
function Get-ControlCenter {
|
||||
param([IntPtr]$Handle)
|
||||
$r = New-Object AgentDesktopProbe.ProbeRect
|
||||
[void][AgentDesktopProbe.Injector]::GetWindowRect($Handle, [ref]$r)
|
||||
return [pscustomobject]@{
|
||||
CenterX = [int](($r.Left + $r.Right) / 2)
|
||||
CenterY = [int](($r.Top + $r.Bottom) / 2)
|
||||
Rect = ([string]$r.Left + ',' + $r.Top + ',' + ($r.Right - $r.Left) + ',' + ($r.Bottom - $r.Top))
|
||||
Left = $r.Left
|
||||
Top = $r.Top
|
||||
Right = $r.Right
|
||||
Bottom = $r.Bottom
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Injected {
|
||||
param([Parameter(Mandatory = $true)][string]$Stage, [Parameter(Mandatory = $true)][scriptblock]$Action)
|
||||
if ($null -ne $script:Interference) { return $false }
|
||||
try { Assert-Foreground -ExpectedProcessId $script:TargetPid -Stage ($Stage + ':pre') }
|
||||
catch {
|
||||
$script:Interference = [ordered]@{ stage = ($Stage + ':pre'); detail = ($_.Exception.Message -replace '[\r\n]+', ' ') }
|
||||
Write-ProbeLog -Message ('interference before ' + $Stage + '; no further injection will be attempted') -Level 'warn'
|
||||
return $false
|
||||
}
|
||||
& $Action | Out-Null
|
||||
$script:Injections++
|
||||
Start-Sleep -Milliseconds 120
|
||||
try { Assert-Foreground -ExpectedProcessId $script:TargetPid -Stage ($Stage + ':post') }
|
||||
catch {
|
||||
$script:Interference = [ordered]@{ stage = ($Stage + ':post'); detail = ($_.Exception.Message -replace '[\r\n]+', ' ') }
|
||||
Write-ProbeLog -Message ('interference after ' + $Stage + '; no further injection will be attempted') -Level 'warn'
|
||||
return $false
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
function Send-TypedText {
|
||||
param([string]$Stage, [string]$Text)
|
||||
$units = 0
|
||||
for ($i = 0; $i -lt $Text.Length; $i++) {
|
||||
$unit = [uint16][int][char]$Text[$i]
|
||||
$ok = Invoke-Injected -Stage ($Stage + ':unit' + $i) -Action ([scriptblock]::Create('[AgentDesktopProbe.Injector]::SendUnicodeUnit(' + $unit + ')'))
|
||||
if (-not $ok) { break }
|
||||
$units++
|
||||
Start-Sleep -Milliseconds 25
|
||||
}
|
||||
return $units
|
||||
}
|
||||
|
||||
function Get-ModifierState {
|
||||
$rows = New-Object System.Collections.ArrayList
|
||||
foreach ($m in $Modifiers) {
|
||||
$async = [AgentDesktopProbe.Injector]::GetAsyncKeyState($m.Vk)
|
||||
$sync = [AgentDesktopProbe.Injector]::GetKeyState($m.Vk)
|
||||
[void]$rows.Add([ordered]@{
|
||||
key = $m.Name
|
||||
asyncKeyIsDown = (($async -band 0x8000) -ne 0)
|
||||
keyStateIsDown = (($sync -band 0x8000) -ne 0)
|
||||
})
|
||||
}
|
||||
return @($rows)
|
||||
}
|
||||
|
||||
function Get-SubtreeFingerprint {
|
||||
param($Root, [int]$Budget = 250, [int]$MaxDepth = 10)
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
$count = 0
|
||||
$stack = New-Object System.Collections.Stack
|
||||
$stack.Push(@{ El = $Root; Depth = 0 })
|
||||
while ($stack.Count -gt 0 -and $count -lt $Budget) {
|
||||
$item = $stack.Pop()
|
||||
$cur = $null
|
||||
try { $cur = $item.El.Current } catch { }
|
||||
if ($null -eq $cur) { continue }
|
||||
$count++
|
||||
try { [void]$sb.Append($cur.ControlType.ProgrammaticName).Append('|').Append([string]$cur.Name).Append("`n") } catch { }
|
||||
if ($item.Depth -ge $MaxDepth) { continue }
|
||||
try {
|
||||
$k = $RawWalker.GetFirstChild($item.El)
|
||||
$kids = New-Object System.Collections.ArrayList
|
||||
while ($null -ne $k) { [void]$kids.Add($k); $k = $RawWalker.GetNextSibling($k) }
|
||||
for ($i = $kids.Count - 1; $i -ge 0; $i--) { $stack.Push(@{ El = $kids[$i]; Depth = ($item.Depth + 1) }) }
|
||||
} catch { }
|
||||
}
|
||||
$shape = Get-TextShape -Text $sb.ToString()
|
||||
return [ordered]@{ nodes = $count; sha256Utf16 = $shape.sha256Utf16 }
|
||||
}
|
||||
|
||||
$status = 'ok'
|
||||
$message = ''
|
||||
$resultData = [ordered]@{}
|
||||
$clipHadText = $false
|
||||
$clipOriginal = ''
|
||||
$clipKinds = @()
|
||||
$clipSnapshotTaken = $false
|
||||
$cursorOrigin = $null
|
||||
$teardown = [ordered]@{}
|
||||
|
||||
try {
|
||||
Initialize-ProbeNative
|
||||
Initialize-InjectorNative
|
||||
$scratchExe = Join-Path (Get-ProbeRoot) 'scratch\bin\ScratchForms.exe'
|
||||
if (-not (Test-Path -LiteralPath $scratchExe)) {
|
||||
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path (Get-ProbeRoot) 'scratch\build-scratch.ps1') | Out-Null
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $scratchExe)) { throw ('scratch fixture missing at ' + $scratchExe) }
|
||||
|
||||
$cursorOrigin = New-Object AgentDesktopProbe.ProbePoint
|
||||
[void][AgentDesktopProbe.Injector]::GetCursorPos([ref]$cursorOrigin)
|
||||
|
||||
$clipKinds = @()
|
||||
try {
|
||||
if ([System.Windows.Forms.Clipboard]::ContainsText()) { $clipKinds += 'text' }
|
||||
if ([System.Windows.Forms.Clipboard]::ContainsImage()) { $clipKinds += 'image' }
|
||||
if ([System.Windows.Forms.Clipboard]::ContainsFileDropList()) { $clipKinds += 'filedroplist' }
|
||||
if ([System.Windows.Forms.Clipboard]::ContainsAudio()) { $clipKinds += 'audio' }
|
||||
$clipHadText = [System.Windows.Forms.Clipboard]::ContainsText()
|
||||
if ($clipHadText) { $clipOriginal = [System.Windows.Forms.Clipboard]::GetText() }
|
||||
$clipSnapshotTaken = $true
|
||||
} catch {
|
||||
Write-ProbeLog -Message ('clipboard snapshot failed: ' + $_.Exception.Message) -Level 'warn'
|
||||
}
|
||||
$clipOriginalShape = Get-TextShape -Text $clipOriginal
|
||||
|
||||
$scratch = Start-ScratchProcess -FilePath $scratchExe -ArgumentList @('--tag', 'u6', '--pos', '100,100') -NoActivate -TimeoutSec 25
|
||||
[void]$script:Spawned.Add($scratch.ProcessId)
|
||||
if ($scratch.MainWindowHandle -eq [IntPtr]::Zero) { throw 'scratch window never appeared' }
|
||||
$script:TargetPid = $scratch.ProcessId
|
||||
$form = $scratch.MainWindowHandle
|
||||
|
||||
[void][AgentDesktopProbe.Native]::ShowWindow($form, 6)
|
||||
Start-Sleep -Milliseconds 500
|
||||
[void][AgentDesktopProbe.Native]::ShowWindow($form, 9)
|
||||
Start-Sleep -Milliseconds 900
|
||||
$foregroundAcquired = ([AgentDesktopProbe.Native]::GetForegroundProcessId() -eq $script:TargetPid)
|
||||
if (-not $foregroundAcquired) {
|
||||
$script:Interference = [ordered]@{
|
||||
stage = 'foreground-acquisition'
|
||||
detail = 'ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) on the probe-owned window did not make it foreground; observed pid ' + [AgentDesktopProbe.Native]::GetForegroundProcessId()
|
||||
}
|
||||
}
|
||||
|
||||
$hTxt = Get-ControlHandle -Form $form -Name 'txtValue'
|
||||
$hStatus = Get-ControlHandle -Form $form -Name 'lblStatus'
|
||||
$hScrollPos = Get-ControlHandle -Form $form -Name 'lblScrollPos'
|
||||
$hSliderLabel = Get-ControlHandle -Form $form -Name 'lblSliderValue'
|
||||
$hSlider = Get-ControlHandle -Form $form -Name 'tbSlider'
|
||||
$hPanel = Get-ControlHandle -Form $form -Name 'pnlScroll'
|
||||
$hAction = Get-ControlHandle -Form $form -Name 'btnAction'
|
||||
|
||||
# --- keyboard: unicode typing round trips -------------------------------
|
||||
$typedRows = New-Object System.Collections.ArrayList
|
||||
$payloads = @(
|
||||
[pscustomobject]@{ Kind = 'ascii'; Text = 'probe-typed-01' },
|
||||
[pscustomobject]@{ Kind = 'cjk'; Text = ([char]::ConvertFromUtf32(0x4E2D) + [char]::ConvertFromUtf32(0x6587) + [char]::ConvertFromUtf32(0x30C6)) },
|
||||
[pscustomobject]@{ Kind = 'astral-plane'; Text = ('a' + [char]::ConvertFromUtf32(0x1F600) + 'z') },
|
||||
[pscustomobject]@{ Kind = 'mixed'; Text = ('x' + [char]::ConvertFromUtf32(0x4E2D) + [char]::ConvertFromUtf32(0x1F600) + 'y') })
|
||||
|
||||
$editCenter = Get-ControlCenter -Handle $hTxt
|
||||
[void](Invoke-Injected -Stage 'focus-edit:move' -Action { [AgentDesktopProbe.Injector]::MouseMoveAbsolute($editCenter.CenterX, $editCenter.CenterY) })
|
||||
[void](Invoke-Injected -Stage 'focus-edit:down' -Action { [AgentDesktopProbe.Injector]::MouseButton($true) })
|
||||
[void](Invoke-Injected -Stage 'focus-edit:up' -Action { [AgentDesktopProbe.Injector]::MouseButton($false) })
|
||||
Start-Sleep -Milliseconds 300
|
||||
|
||||
foreach ($p in $payloads) {
|
||||
[AgentDesktopProbe.Injector]::SetControlText($hTxt, '')
|
||||
Start-Sleep -Milliseconds 150
|
||||
$cleared = Get-ControlText -Handle $hTxt
|
||||
$unitsSent = Send-TypedText -Stage ('type-' + $p.Kind) -Text $p.Text
|
||||
Start-Sleep -Milliseconds 400
|
||||
$observed = Get-ControlText -Handle $hTxt
|
||||
$expectedShape = Get-TextShape -Text $p.Text
|
||||
$observedShape = Get-TextShape -Text $observed
|
||||
[void]$typedRows.Add([ordered]@{
|
||||
payloadKind = $p.Kind
|
||||
payloadOrigin = 'built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks'
|
||||
clearedBeforeTyping = ($cleared.Length -eq 0)
|
||||
utf16UnitsSent = $unitsSent
|
||||
expectedShape = $expectedShape
|
||||
observedShape = $observedShape
|
||||
exactRoundTrip = ($expectedShape.sha256Utf16 -eq $observedShape.sha256Utf16)
|
||||
surrogatePairSurvived = ($expectedShape.surrogatePairs -eq $observedShape.surrogatePairs)
|
||||
reReadMethod = 'WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA'
|
||||
verdict = $(if ($expectedShape.sha256Utf16 -eq $observedShape.sha256Utf16) { 'ok' } elseif ($null -ne $script:Interference) { 'aborted-on-interference' } else { 'chunk-boundary-loss' })
|
||||
})
|
||||
}
|
||||
|
||||
# --- keyboard: modifier chord (Ctrl+A, Ctrl+C) --------------------------
|
||||
$chordPayload = 'chord-probe-' + [char]::ConvertFromUtf32(0x4E2D) + '-42'
|
||||
[AgentDesktopProbe.Injector]::SetControlText($hTxt, '')
|
||||
Start-Sleep -Milliseconds 150
|
||||
[void](Send-TypedText -Stage 'type-chord-source' -Text $chordPayload)
|
||||
Start-Sleep -Milliseconds 300
|
||||
$chordSourceObserved = Get-ControlText -Handle $hTxt
|
||||
$chordExpectedShape = Get-TextShape -Text $chordPayload
|
||||
$chordSourceShape = Get-TextShape -Text $chordSourceObserved
|
||||
|
||||
$chordSent = $true
|
||||
foreach ($step in @(
|
||||
[pscustomobject]@{ Stage = 'ctrl-down-a'; Vk = 0x11; Up = $false },
|
||||
[pscustomobject]@{ Stage = 'a-down'; Vk = 0x41; Up = $false },
|
||||
[pscustomobject]@{ Stage = 'a-up'; Vk = 0x41; Up = $true },
|
||||
[pscustomobject]@{ Stage = 'c-down'; Vk = 0x43; Up = $false },
|
||||
[pscustomobject]@{ Stage = 'c-up'; Vk = 0x43; Up = $true },
|
||||
[pscustomobject]@{ Stage = 'ctrl-up'; Vk = 0x11; Up = $true })) {
|
||||
$action = [scriptblock]::Create('[AgentDesktopProbe.Injector]::SendVirtualKey(' + $step.Vk + ', $' + $step.Up.ToString().ToLower() + ')')
|
||||
if (-not (Invoke-Injected -Stage ('chord:' + $step.Stage) -Action $action)) { $chordSent = $false; break }
|
||||
Start-Sleep -Milliseconds 80
|
||||
}
|
||||
Start-Sleep -Milliseconds 400
|
||||
$clipObserved = ''
|
||||
$clipReadOk = $false
|
||||
try {
|
||||
if ([System.Windows.Forms.Clipboard]::ContainsText()) { $clipObserved = [System.Windows.Forms.Clipboard]::GetText(); $clipReadOk = $true }
|
||||
} catch { Write-ProbeLog -Message ('clipboard read failed: ' + $_.Exception.Message) -Level 'warn' }
|
||||
$clipObservedShape = Get-TextShape -Text $clipObserved
|
||||
|
||||
$modifierAfterChord = Get-ModifierState
|
||||
|
||||
[void](Write-ProbeJson -Probe $Probe -Name 'keyboard.json' -InputObject ([ordered]@{
|
||||
probe = $Probe
|
||||
question = 'does SendInput deliver non-BMP text intact through the UTF-16 chunking the API forces, and does a modifier chord register'
|
||||
stack = 'win32-SendInput'
|
||||
scope = 'system'
|
||||
why = "2.8's type_text has to decide how to chunk a string into KEYEVENTF_UNICODE events. A surrogate pair cannot be sent as one event, so the chunk boundary is forced by the API; whether the target reassembles it is the measurement."
|
||||
foregroundAcquisitionMethod = 'ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) on the probe-owned scratch window. SetForegroundWindow is never called.'
|
||||
foregroundAcquired = $foregroundAcquired
|
||||
injectionGate = 'every SendInput call below is bracketed by Assert-Foreground before and after; the first mismatch stops all further injection and is recorded in interference'
|
||||
interference = $script:Interference
|
||||
typingRows = @($typedRows)
|
||||
chord = [ordered]@{
|
||||
chordSent = $chordSent
|
||||
keys = 'Ctrl down, A down, A up, C down, C up, Ctrl up - six discrete SendInput calls, each individually foreground-gated'
|
||||
sourceTypedShape = $chordExpectedShape
|
||||
sourceObservedShape = $chordSourceShape
|
||||
sourceRoundTrip = ($chordExpectedShape.sha256Utf16 -eq $chordSourceShape.sha256Utf16)
|
||||
clipboardReadOk = $clipReadOk
|
||||
clipboardObservedShape = $clipObservedShape
|
||||
verifiedByShape = ($chordSourceShape.sha256Utf16 -eq $clipObservedShape.sha256Utf16)
|
||||
verificationRule = 'the clipboard is verified by SHAPE against the hash of the string the probe itself typed into the control, never against the observed clipboard value. The operator clipboard could hold a secret; comparing hashes means a non-probe value simply fails to match and nothing about it is recorded beyond its length and hash.'
|
||||
verdict = $(if ($chordSourceShape.sha256Utf16 -eq $clipObservedShape.sha256Utf16) { 'ok - Ctrl+A then Ctrl+C put exactly the typed string on the clipboard' } else { 'chord-not-registered-or-clipboard-differs' })
|
||||
}
|
||||
modifierStateAfterChord = @($modifierAfterChord)
|
||||
modifierStateAfterChordVerdict = $(if (@($modifierAfterChord | Where-Object { $_.asyncKeyIsDown -or $_.keyStateIsDown }).Count -eq 0) { 'no modifier left down by the chord' } else { 'a modifier is still down after the chord' })
|
||||
}))
|
||||
|
||||
# --- mouse: click, absolute move, wheel, drag ---------------------------
|
||||
$actionCenter = Get-ControlCenter -Handle $hAction
|
||||
$statusBeforeClick = Get-ControlText -Handle $hStatus
|
||||
[void](Invoke-Injected -Stage 'click-btnAction:move' -Action { [AgentDesktopProbe.Injector]::MouseMoveAbsolute($actionCenter.CenterX, $actionCenter.CenterY) })
|
||||
Start-Sleep -Milliseconds 150
|
||||
$landed = New-Object AgentDesktopProbe.ProbePoint
|
||||
[void][AgentDesktopProbe.Injector]::GetCursorPos([ref]$landed)
|
||||
[void](Invoke-Injected -Stage 'click-btnAction:down' -Action { [AgentDesktopProbe.Injector]::MouseButton($true) })
|
||||
[void](Invoke-Injected -Stage 'click-btnAction:up' -Action { [AgentDesktopProbe.Injector]::MouseButton($false) })
|
||||
Start-Sleep -Milliseconds 400
|
||||
$statusAfterClick = Get-ControlText -Handle $hStatus
|
||||
|
||||
$panelCenter = Get-ControlCenter -Handle $hPanel
|
||||
$scrollBeforeWheel = Get-ControlText -Handle $hScrollPos
|
||||
[void](Invoke-Injected -Stage 'wheel:move' -Action { [AgentDesktopProbe.Injector]::MouseMoveAbsolute($panelCenter.CenterX, $panelCenter.CenterY) })
|
||||
Start-Sleep -Milliseconds 200
|
||||
for ($w = 0; $w -lt 3; $w++) {
|
||||
[void](Invoke-Injected -Stage ('wheel:tick' + $w) -Action { [AgentDesktopProbe.Injector]::MouseWheel(-120) })
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
Start-Sleep -Milliseconds 400
|
||||
$scrollAfterWheel = Get-ControlText -Handle $hScrollPos
|
||||
$scrollPixelsBefore = 0
|
||||
$scrollPixelsAfter = 0
|
||||
if ($scrollBeforeWheel -match 'scroll:(-?\d+)') { $scrollPixelsBefore = [int]$Matches[1] }
|
||||
if ($scrollAfterWheel -match 'scroll:(-?\d+)') { $scrollPixelsAfter = [int]$Matches[1] }
|
||||
|
||||
$sliderRect = Get-ControlCenter -Handle $hSlider
|
||||
$sliderBefore = Get-ControlText -Handle $hSliderLabel
|
||||
$dragSamples = New-Object System.Collections.ArrayList
|
||||
$thumbY = $sliderRect.Top + 14
|
||||
[void](Invoke-Injected -Stage 'drag:move-to-thumb' -Action { [AgentDesktopProbe.Injector]::MouseMoveAbsolute(($sliderRect.Left + 16), $thumbY) })
|
||||
Start-Sleep -Milliseconds 200
|
||||
[void](Invoke-Injected -Stage 'drag:down' -Action { [AgentDesktopProbe.Injector]::MouseButton($true) })
|
||||
Start-Sleep -Milliseconds 200
|
||||
for ($s = 1; $s -le 6; $s++) {
|
||||
$x = $sliderRect.Left + 16 + ($s * 44)
|
||||
[void](Invoke-Injected -Stage ('drag:step' + $s) -Action ([scriptblock]::Create('[AgentDesktopProbe.Injector]::MouseMoveAbsolute(' + $x + ', ' + $thumbY + ')')))
|
||||
Start-Sleep -Milliseconds 250
|
||||
$label = Get-ControlText -Handle $hSliderLabel
|
||||
$v = -1
|
||||
if ($label -match 'slider:(-?\d+)') { $v = [int]$Matches[1] }
|
||||
[void]$dragSamples.Add([ordered]@{ step = $s; sliderValue = $v })
|
||||
}
|
||||
[void](Invoke-Injected -Stage 'drag:up' -Action { [AgentDesktopProbe.Injector]::MouseButton($false) })
|
||||
Start-Sleep -Milliseconds 400
|
||||
$sliderAfter = Get-ControlText -Handle $hSliderLabel
|
||||
$values = @($dragSamples | ForEach-Object { $_.sliderValue })
|
||||
$monotonic = $true
|
||||
for ($i = 1; $i -lt $values.Count; $i++) { if ($values[$i] -lt $values[$i - 1]) { $monotonic = $false } }
|
||||
$sliderValueBefore = 0
|
||||
$sliderValueAfter = 0
|
||||
if ($sliderBefore -match 'slider:(-?\d+)') { $sliderValueBefore = [int]$Matches[1] }
|
||||
if ($sliderAfter -match 'slider:(-?\d+)') { $sliderValueAfter = [int]$Matches[1] }
|
||||
|
||||
[void](Write-ProbeJson -Probe $Probe -Name 'mouse.json' -InputObject ([ordered]@{
|
||||
probe = $Probe
|
||||
question = 'do SendInput absolute moves land where asked, and do click, wheel and drag register as real input on the target'
|
||||
stack = 'win32-SendInput'
|
||||
scope = 'system'
|
||||
coordinateModel = 'MOUSEEVENTF_ABSOLUTE normalises against GetSystemMetrics(SM_CXSCREEN/SM_CYSCREEN), i.e. the PRIMARY monitor only. This box has one display; a multi-monitor adapter must use MOUSEEVENTF_VIRTUALDESK instead, and the single-display limit here is why that is a DEFERRED row rather than a measured one.'
|
||||
interference = $script:Interference
|
||||
click = [ordered]@{
|
||||
targetControl = 'btnAction (control id 1005)'
|
||||
targetRect = $actionCenter.Rect
|
||||
requestedPoint = ([string]$actionCenter.CenterX + ',' + $actionCenter.CenterY)
|
||||
cursorLandedPoint = ([string]$landed.X + ',' + $landed.Y)
|
||||
absoluteMoveExact = ($landed.X -eq $actionCenter.CenterX -and $landed.Y -eq $actionCenter.CenterY)
|
||||
absoluteMoveNote = 'the landing point is read back with GetCursorPos. The 0..65535 normalisation is lossy at odd screen widths, so an off-by-one landing is expected and is recorded rather than asserted away.'
|
||||
statusBefore = $statusBeforeClick
|
||||
statusAfter = $statusAfterClick
|
||||
registered = ($statusBeforeClick -ne $statusAfterClick)
|
||||
reReadMethod = 'WM_GETTEXT on lblStatus (control id 1020), a different control than the one clicked'
|
||||
}
|
||||
wheel = [ordered]@{
|
||||
targetControl = 'pnlScroll (control id 1011)'
|
||||
targetRect = $panelCenter.Rect
|
||||
ticks = 3
|
||||
deltaPerTick = -120
|
||||
sinkBefore = $scrollBeforeWheel
|
||||
sinkAfter = $scrollAfterWheel
|
||||
scrollPixelsBefore = $scrollPixelsBefore
|
||||
scrollPixelsAfter = $scrollPixelsAfter
|
||||
deltaScrollPixels = ($scrollPixelsAfter - $scrollPixelsBefore)
|
||||
registered = ($scrollPixelsAfter -gt $scrollPixelsBefore)
|
||||
patternCounterpart = 'the pattern-scroll arm is in captures/05-interactions/interactions.json. It could NOT be run on this control: the managed client cannot acquire ScrollPattern on pnlScroll in either fixture mode even though the UIA3 COM census records Scroll on that exact provider. The wheel path drives the control the pattern path could not, and both are observed through the same lblScrollPos sink.'
|
||||
measuredFactNaming = 'the observed delta is named deltaScrollPixels, not "y" or "height", so the KTD9 normalizer - which buckets any key named x/y/left/top/right/bottom/width/height to 8 px - cannot canonicalize the measurement away.'
|
||||
}
|
||||
drag = [ordered]@{
|
||||
targetControl = 'tbSlider (control id 1008), Minimum 0 Maximum 100'
|
||||
targetRect = $sliderRect.Rect
|
||||
method = 'mouse-down on the thumb at the left end, six absolute moves rightwards, mouse-up; the fixture label lblSliderValue is read after every step'
|
||||
sliderValueBefore = $sliderValueBefore
|
||||
sliderValueAfter = $sliderValueAfter
|
||||
samples = @($dragSamples)
|
||||
monotonicNonDecreasing = $monotonic
|
||||
increased = ($sliderValueAfter -gt $sliderValueBefore)
|
||||
verdict = $(if ($monotonic -and $sliderValueAfter -gt $sliderValueBefore) { 'ok - monotonic increase under drag' } else { 'drag did not produce a monotonic increase' })
|
||||
}
|
||||
}))
|
||||
|
||||
# --- PostMessage control probe ------------------------------------------
|
||||
$postRows = New-Object System.Collections.ArrayList
|
||||
[AgentDesktopProbe.Injector]::SetControlText($hTxt, '')
|
||||
Start-Sleep -Milliseconds 200
|
||||
$scratchBefore = Get-ControlText -Handle $hTxt
|
||||
$downResult = [AgentDesktopProbe.Injector]::PostKey($hTxt, 0x0100, 0x5A)
|
||||
Start-Sleep -Milliseconds 500
|
||||
$scratchAfterDown = Get-ControlText -Handle $hTxt
|
||||
$upResult = [AgentDesktopProbe.Injector]::PostKey($hTxt, 0x0101, 0x5A)
|
||||
Start-Sleep -Milliseconds 500
|
||||
$scratchAfterKey = Get-ControlText -Handle $hTxt
|
||||
$charResult = [AgentDesktopProbe.Injector]::PostChar($hTxt, 0x5A)
|
||||
Start-Sleep -Milliseconds 500
|
||||
$scratchAfterChar = Get-ControlText -Handle $hTxt
|
||||
[void]$postRows.Add([ordered]@{
|
||||
target = 'scratch WinForms Edit (control id 1003), same session, same integrity level as the probe'
|
||||
stack = 'win32-PostMessage'
|
||||
integrityRelation = 'High -> High. 09-elevation-uipi measured the Medium -> High case for PostMessage(WM_CHAR) and got false with error 5 (ERROR_ACCESS_DENIED). This row is the different question: with UIPI out of the way entirely, does a posted key message register at all?'
|
||||
keyDownPostResult = $downResult
|
||||
keyUpPostResult = $upResult
|
||||
charPostResult = $charResult
|
||||
resultFormat = '"true" on success, "false:<GetLastError>" on failure. The error code is only meaningful when the call failed, so it is only recorded then.'
|
||||
textBeforeShape = (Get-TextShape -Text $scratchBefore)
|
||||
textAfterKeyDownShape = (Get-TextShape -Text $scratchAfterDown)
|
||||
textAfterKeyUpShape = (Get-TextShape -Text $scratchAfterKey)
|
||||
textAfterCharShape = (Get-TextShape -Text $scratchAfterChar)
|
||||
keyDownRegistered = ($scratchAfterDown -ne $scratchBefore)
|
||||
keyUpAddedAnything = ($scratchAfterKey -ne $scratchAfterDown)
|
||||
charMessageRegistered = ($scratchAfterChar -ne $scratchAfterKey)
|
||||
measuredResult = 'the posted WM_KEYDOWN alone inserted one character into the Edit. WM_KEYUP added nothing, and a subsequently posted WM_CHAR inserted a second character.'
|
||||
interpretation = 'this contradicts the usual reading that a posted WM_KEYDOWN is inert because it carries a virtual key rather than a character. TranslateMessage runs inside the TARGET thread''s own message pump and does not care whether the message it retrieved was posted or came from the input queue, so it synthesises the WM_CHAR itself. The message-posting path is therefore alive - not dead - for a classic Win32 Edit at equal integrity. What it cannot do is carry modifier state: TranslateMessage reads the target thread''s keyboard state, which the poster cannot set, so a posted chord or a shifted character is unreachable by this path even where a plain character is not.'
|
||||
invariantRelevance = 'Engineering Invariant #5 is about Chromium and UWP, and this row does not weaken it. It does remove the convenient generalisation that message posting never types anything: on classic Win32 controls it does, which is exactly why the Chromium row below has to be measured rather than inferred from the same mechanism.'
|
||||
})
|
||||
|
||||
$obsidianExe = Join-Path $env:LOCALAPPDATA 'Programs\Obsidian\Obsidian.exe'
|
||||
$obsidianLaunched = $false
|
||||
if (-not (Test-Path -LiteralPath $obsidianExe)) {
|
||||
[void]$postRows.Add([ordered]@{ target = 'chromium/electron'; verdict = 'SKIPPED - Obsidian is not installed on this box' })
|
||||
} elseif (@(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue).Count -gt 0) {
|
||||
[void]$postRows.Add([ordered]@{
|
||||
target = 'chromium/electron'
|
||||
verdict = 'SKIPPED - an Obsidian instance the probe did not launch was already running. KTD5 confines the probe to windows it launched itself, and posting synthetic keystrokes into an operator session is exactly what that rule exists to prevent.'
|
||||
})
|
||||
} else {
|
||||
[void](Start-ScratchProcess -FilePath $obsidianExe -NoActivate -TimeoutSec 40)
|
||||
$obsidianLaunched = $true
|
||||
Start-Sleep -Seconds 6
|
||||
foreach ($op in @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue)) {
|
||||
if (-not $script:Spawned.Contains($op.Id)) { Register-ScratchProcessId -ProcessId $op.Id; [void]$script:Spawned.Add($op.Id) }
|
||||
}
|
||||
Start-Sleep -Seconds 14
|
||||
$chromiumTop = [IntPtr]::Zero
|
||||
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
|
||||
$c = $walker.GetFirstChild($AE::RootElement)
|
||||
while ($null -ne $c) {
|
||||
try {
|
||||
if ($c.Current.ClassName -eq 'Chrome_WidgetWin_1' -and $script:Spawned.Contains([int]$c.Current.ProcessId)) {
|
||||
$chromiumTop = [IntPtr][int]$c.Current.NativeWindowHandle
|
||||
break
|
||||
}
|
||||
} catch { }
|
||||
$c = $walker.GetNextSibling($c)
|
||||
}
|
||||
if ($chromiumTop -eq [IntPtr]::Zero) {
|
||||
[void]$postRows.Add([ordered]@{ target = 'chromium/electron'; verdict = 'SKIPPED - no probe-owned Chrome_WidgetWin_1 top-level window appeared' })
|
||||
} else {
|
||||
$childClasses = [AgentDesktopProbe.Injector]::ChildClassCensus($chromiumTop)
|
||||
$renderWidget = [AgentDesktopProbe.Injector]::FindDescendantByClass($chromiumTop, 'Chrome_RenderWidgetHostHWND')
|
||||
$chromiumRoot = $AE::FromHandle($chromiumTop)
|
||||
$fpSettled = Get-SubtreeFingerprint -Root $chromiumRoot
|
||||
$stabilizationPasses = 0
|
||||
for ($i = 1; $i -le 20; $i++) {
|
||||
Start-Sleep -Seconds 2
|
||||
$fpNow = Get-SubtreeFingerprint -Root $chromiumRoot
|
||||
$stabilizationPasses = $i
|
||||
if ($fpNow.sha256Utf16 -eq $fpSettled.sha256Utf16) { break }
|
||||
$fpSettled = $fpNow
|
||||
}
|
||||
Start-Sleep -Seconds 5
|
||||
$fpIdle = Get-SubtreeFingerprint -Root $chromiumRoot
|
||||
$topDown = [AgentDesktopProbe.Injector]::PostKey($chromiumTop, 0x0100, 0x5A)
|
||||
Start-Sleep -Milliseconds 200
|
||||
$topUp = [AgentDesktopProbe.Injector]::PostKey($chromiumTop, 0x0101, 0x5A)
|
||||
$childDown = '<no-render-widget-child>'
|
||||
$childUp = '<no-render-widget-child>'
|
||||
$childChar = '<no-render-widget-child>'
|
||||
if ($renderWidget -ne [IntPtr]::Zero) {
|
||||
Start-Sleep -Milliseconds 200
|
||||
$childDown = [AgentDesktopProbe.Injector]::PostKey($renderWidget, 0x0100, 0x5A)
|
||||
Start-Sleep -Milliseconds 200
|
||||
$childUp = [AgentDesktopProbe.Injector]::PostKey($renderWidget, 0x0101, 0x5A)
|
||||
Start-Sleep -Milliseconds 200
|
||||
$childChar = [AgentDesktopProbe.Injector]::PostChar($renderWidget, 0x5A)
|
||||
}
|
||||
Start-Sleep -Seconds 4
|
||||
$fpAfter = Get-SubtreeFingerprint -Root $chromiumRoot
|
||||
$churnWithoutInput = ($fpSettled.sha256Utf16 -ne $fpIdle.sha256Utf16)
|
||||
$changedAfterPost = ($fpIdle.sha256Utf16 -ne $fpAfter.sha256Utf16)
|
||||
[void]$postRows.Add([ordered]@{
|
||||
target = 'chromium/electron (Obsidian, launched by this probe)'
|
||||
stack = 'win32-PostMessage'
|
||||
obsidianVersion = (Get-Item -LiteralPath $obsidianExe).VersionInfo.FileVersion
|
||||
topLevelClassName = 'Chrome_WidgetWin_1'
|
||||
childWindowClasses = $childClasses
|
||||
renderWidgetChildFound = ($renderWidget -ne [IntPtr]::Zero)
|
||||
topLevelKeyDownPostResult = $topDown
|
||||
topLevelKeyUpPostResult = $topUp
|
||||
renderWidgetKeyDownPostResult = $childDown
|
||||
renderWidgetKeyUpPostResult = $childUp
|
||||
renderWidgetCharPostResult = $childChar
|
||||
observable = 'a bounded RawView UIA walk of the Chromium top-level window (250 nodes, depth 10) reduced to a SHA-256 of the ControlType|Name stream. Only the derived booleans and node counts are recorded - neither the hash nor any Chromium Name value reaches the capture (R11), and the hash would in any case be run-varying and break the KTD9 twin.'
|
||||
controlPass = 'the fingerprint is first polled every 2 s until two consecutive reads agree, so the measurement starts from a quiet tree rather than a loading one. A further idle fingerprint is then taken five seconds later with NO input at all, and only then are the messages posted. Without that control an Electron tree still growing on its own reads as a registered keystroke - which is exactly what the first run of this probe recorded before the control was added: 13 nodes to 15 with nothing posted.'
|
||||
timingStabilizationPasses = $stabilizationPasses
|
||||
nodesAtSettle = $fpSettled.nodes
|
||||
nodesAfterIdleWait = $fpIdle.nodes
|
||||
nodesAfterPostedKeys = $fpAfter.nodes
|
||||
treeChurnWithoutInput = $churnWithoutInput
|
||||
treeChangedAfterPostedKeys = $changedAfterPost
|
||||
keystrokeRegistered = $changedAfterPost
|
||||
verdict = $(if (-not $changedAfterPost) { 'not registered - every PostMessage call returned success and the Chromium tree was byte-identical before and after, over the same window and the same bounded walk' } elseif ($churnWithoutInput) { 'INCONCLUSIVE - the tree was still changing on its own during the idle control pass, so the post-injection change cannot be attributed to the keystroke' } else { 'registered - the posted keystroke changed a Chromium tree that the idle control pass had just shown to be quiet' })
|
||||
caveat = 'an unchanged fingerprint is evidence of no observable effect within the bounded walk, not a proof that no byte anywhere changed. It is recorded as such.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
[void](Write-ProbeJson -Probe $Probe -Name 'postmessage.json' -InputObject ([ordered]@{
|
||||
probe = $Probe
|
||||
question = 'does posting WM_KEYDOWN/WM_KEYUP to a control actually register the keystroke - on a classic Win32 control and on Chromium'
|
||||
stack = 'win32-PostMessage'
|
||||
scope = 'system'
|
||||
why = 'Engineering Invariant #5 asserts the message-posting path is dead for Chromium and UWP. Nothing else in this corpus tests it, so the invariant has been carried on assumption. These rows file it from observation.'
|
||||
foregroundIrrelevantNote = 'PostMessage is targeted at a window, not at the desktop input queue, so these rows are deliberately NOT foreground-gated. That is the whole appeal of the path and the reason it is worth measuring rather than assuming.'
|
||||
rows = @($postRows)
|
||||
}))
|
||||
|
||||
$resultData['injections'] = $script:Injections
|
||||
$resultData['typingRows'] = @($typedRows).Count
|
||||
$resultData['astralRoundTrip'] = @($typedRows | Where-Object { $_.payloadKind -eq 'astral-plane' } | ForEach-Object { $_.exactRoundTrip })
|
||||
$resultData['chordVerified'] = ($chordSourceShape.sha256Utf16 -eq $clipObservedShape.sha256Utf16)
|
||||
$resultData['wheelRegistered'] = ($scrollPixelsAfter -gt $scrollPixelsBefore)
|
||||
$resultData['dragMonotonic'] = $monotonic
|
||||
$resultData['interference'] = ($null -ne $script:Interference)
|
||||
$message = 'sendinput ' + $script:Injections + ' gated injections; postmessage rows ' + @($postRows).Count
|
||||
} catch {
|
||||
$status = 'fail'
|
||||
$message = ($_.Exception.Message -replace '[\r\n]+', ' ')
|
||||
Write-ProbeLog -Message ('probe failed: ' + $message) -Level 'error'
|
||||
} finally {
|
||||
$modifierBeforeSweep = @()
|
||||
$modifierAfterSweep = @()
|
||||
$sweepInjected = @()
|
||||
try {
|
||||
$modifierBeforeSweep = Get-ModifierState
|
||||
foreach ($m in @($modifierBeforeSweep | Where-Object { $_.asyncKeyIsDown -or $_.keyStateIsDown })) {
|
||||
$vk = ($Modifiers | Where-Object { $_.Name -eq $m.key }).Vk
|
||||
[void][AgentDesktopProbe.Injector]::SendVirtualKey([uint16]$vk, $true)
|
||||
$sweepInjected += $m.key
|
||||
Start-Sleep -Milliseconds 60
|
||||
}
|
||||
$modifierAfterSweep = Get-ModifierState
|
||||
} catch { Write-ProbeLog -Message ('modifier sweep failed: ' + $_.Exception.Message) -Level 'warn' }
|
||||
|
||||
$clipRestored = $false
|
||||
$clipAfterRestoreShape = $null
|
||||
if ($clipSnapshotTaken) {
|
||||
try {
|
||||
if ($clipHadText) { [System.Windows.Forms.Clipboard]::SetText($clipOriginal) } else { [System.Windows.Forms.Clipboard]::Clear() }
|
||||
Start-Sleep -Milliseconds 200
|
||||
$back = ''
|
||||
if ([System.Windows.Forms.Clipboard]::ContainsText()) { $back = [System.Windows.Forms.Clipboard]::GetText() }
|
||||
$clipAfterRestoreShape = Get-TextShape -Text $back
|
||||
$clipRestored = ($clipAfterRestoreShape.sha256Utf16 -eq (Get-TextShape -Text $clipOriginal).sha256Utf16)
|
||||
} catch { Write-ProbeLog -Message ('clipboard restore failed: ' + $_.Exception.Message) -Level 'warn' }
|
||||
}
|
||||
|
||||
$cursorRestored = $false
|
||||
$cursorAfter = $null
|
||||
if ($null -ne $cursorOrigin) {
|
||||
try {
|
||||
[void][AgentDesktopProbe.Injector]::SetCursorPos($cursorOrigin.X, $cursorOrigin.Y)
|
||||
Start-Sleep -Milliseconds 150
|
||||
$cursorAfter = New-Object AgentDesktopProbe.ProbePoint
|
||||
[void][AgentDesktopProbe.Injector]::GetCursorPos([ref]$cursorAfter)
|
||||
$cursorRestored = ($cursorAfter.X -eq $cursorOrigin.X -and $cursorAfter.Y -eq $cursorOrigin.Y)
|
||||
} catch { Write-ProbeLog -Message ('cursor restore failed: ' + $_.Exception.Message) -Level 'warn' }
|
||||
}
|
||||
|
||||
foreach ($id in @($script:Spawned)) {
|
||||
try { Stop-ScratchProcess -ProcessId $id } catch { Write-ProbeLog -Message ('teardown: ' + $_.Exception.Message) -Level 'warn' }
|
||||
}
|
||||
$survivors = @(@($script:Spawned) | Where-Object { $null -ne (Get-Process -Id $_ -ErrorAction SilentlyContinue) })
|
||||
|
||||
$teardown = [ordered]@{
|
||||
probe = $Probe
|
||||
question = 'does the probe leave the desktop exactly as it found it - no stuck modifier, the original clipboard, the original cursor position, no surviving process'
|
||||
why = 'KTD5. A probe that types into a shared desktop and leaves a modifier down or a clipboard overwritten has corrupted the box for everything that runs after it, including the rest of this corpus.'
|
||||
modifierSweep = [ordered]@{
|
||||
policy = 'conditional by design: modifier state is read first and a key-up is injected only for a modifier actually found down. On the normal path no key is injected at all, so the sweep does not need the foreground gate it would otherwise require.'
|
||||
stateBeforeSweep = @($modifierBeforeSweep)
|
||||
keysSwept = @($sweepInjected)
|
||||
stateAfterSweep = @($modifierAfterSweep)
|
||||
allClear = (@($modifierAfterSweep | Where-Object { $_.asyncKeyIsDown -or $_.keyStateIsDown }).Count -eq 0)
|
||||
}
|
||||
clipboard = [ordered]@{
|
||||
snapshotTaken = $clipSnapshotTaken
|
||||
originalFormats = @($clipKinds)
|
||||
originalTextPresent = $clipHadText
|
||||
restoredExactly = $clipRestored
|
||||
verificationMethod = 'the restored clipboard is read back and compared to the snapshot by SHA-256 of its UTF-16 bytes, in memory. Neither the value nor its hash reaches the capture: the operator clipboard can hold a secret, and a hash of a secret is still a fingerprint of it.'
|
||||
formatCaveat = 'only text is snapshotted and restored. originalFormats records what was actually on the clipboard so a non-text loss would be visible in the capture rather than silent.'
|
||||
}
|
||||
cursor = [ordered]@{
|
||||
snapshotTaken = ($null -ne $cursorOrigin)
|
||||
offsetAfterRestore = $(if ($null -ne $cursorAfter -and $null -ne $cursorOrigin) { [string]($cursorAfter.X - $cursorOrigin.X) + ',' + ($cursorAfter.Y - $cursorOrigin.Y) } else { '<not-captured>' })
|
||||
restoredExactly = $cursorRestored
|
||||
recordingChoice = 'the offset from the snapshot is recorded rather than the absolute positions: an absolute origin is a run-varying value that would break the KTD9 twin, while "0,0" proves exact restoration. It is a comma-joined string rather than numeric x/y keys because the normalizer buckets any numeric key named x or y to 8 px, which would have hidden an 8 px restoration error.'
|
||||
}
|
||||
processes = [ordered]@{
|
||||
spawnedCount = @($script:Spawned).Count
|
||||
survivorCount = @($survivors).Count
|
||||
confirmedGone = (@($survivors).Count -eq 0)
|
||||
confirmationMethod = 'Stop-ScratchProcess terminates and then re-reads the process list until the pid is gone or a 10 s deadline expires; the survivor count above is a second, independent re-read after all teardown.'
|
||||
}
|
||||
}
|
||||
try { [void](Write-ProbeJson -Probe $Probe -Name 'teardown.json' -InputObject $teardown) } catch { }
|
||||
|
||||
foreach ($f in @(Get-ChildItem -LiteralPath (Get-CaptureDir -Probe $Probe) -File -ErrorAction SilentlyContinue)) {
|
||||
if ($f.Name -like '*.normalized') { continue }
|
||||
if (-not (Test-CaptureRedaction -Path $f.FullName)) { $status = 'fail'; $message = ('redaction residue in ' + $f.Name) }
|
||||
}
|
||||
}
|
||||
|
||||
Write-ProbeResult -Probe $Probe -Status $status -Message $message -Data $resultData
|
||||
if ($status -eq 'fail') { exit 1 }
|
||||
exit 0
|
||||
439
probes/windows/07-hittest.ps1
Normal file
439
probes/windows/07-hittest.ps1
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Probe 07 (sub-phase 2.0, unit U6): ElementFromPoint against occluded, zero-size and
|
||||
minimized targets - the primitives 2.6's occlusion gate is built on.
|
||||
|
||||
.DESCRIPTION
|
||||
Stack: managed System.Windows.Automation for ElementFromPoint, Win32 for the
|
||||
independent cross-checks (WindowFromPoint, GetWindowRect, GetDlgItem). Scope: system.
|
||||
|
||||
Three questions, all measured against probe-owned scratch windows:
|
||||
|
||||
1. Occlusion. Two scratch windows are launched at deterministic origins so their
|
||||
rects overlap by construction, the second is raised with ShowWindow(SW_MINIMIZE)
|
||||
then ShowWindow(SW_RESTORE) on its own window - SetForegroundWindow is never
|
||||
called - and ElementFromPoint is asked about a point inside the overlap. The
|
||||
occlusion gate is only sound if the answer is the occluder.
|
||||
|
||||
2. Zero size. btnZeroSize is a real 0x0 Win32 control with control id 1007. U7's COM
|
||||
census recorded it as absent from both RawView and ControlView; this probe
|
||||
confirms that from the managed side, shows it is still reachable by GetDlgItem,
|
||||
and asks what ElementFromPoint returns at its origin.
|
||||
|
||||
3. Minimize. U3 measured that minimizing degenerates geometry in two different
|
||||
shapes: the top-level window reports an empty rect while its descendants report
|
||||
REAL dimensions anchored at -32000, and every node still reports IsOffscreen
|
||||
false. An occlusion gate that tests emptiness or IsOffscreen alone accepts both
|
||||
as visible. This probe re-confirms both shapes and adds the consequence the
|
||||
earlier unit could not measure: what ElementFromPoint returns at the coordinates
|
||||
the window used to occupy.
|
||||
|
||||
Rectangles are recorded as comma-joined strings, not as numeric x/y/width/height
|
||||
keys, because the KTD9 normalizer buckets those to 8 px - which would round -32000
|
||||
into a neighbouring bucket and quietly destroy the evidence.
|
||||
|
||||
Captures under captures/07-hittest/:
|
||||
hittest.json occlusion, zero-size and minimized rows
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. "$PSScriptRoot\common.ps1"
|
||||
|
||||
Add-Type -AssemblyName UIAutomationClient | Out-Null
|
||||
Add-Type -AssemblyName UIAutomationTypes | Out-Null
|
||||
|
||||
$Probe = '07-hittest'
|
||||
$AE = [System.Windows.Automation.AutomationElement]
|
||||
$Walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
|
||||
$RawWalker = [System.Windows.Automation.TreeWalker]::RawViewWalker
|
||||
$Descendants = [System.Windows.Automation.TreeScope]::Descendants
|
||||
$UnderOrigin = @{ X = 100; Y = 100 }
|
||||
$OverOrigin = @{ X = 260; Y = 180 }
|
||||
$ZeroSizeControlId = 1007
|
||||
$script:Spawned = New-Object System.Collections.ArrayList
|
||||
|
||||
function Initialize-HitTestNative {
|
||||
if ('AgentDesktopProbe.HitTest' -as [type]) { return }
|
||||
$src = @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace AgentDesktopProbe {
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct HitRect { public int Left; public int Top; public int Right; public int Bottom; }
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct HitPoint { public int X; public int Y; }
|
||||
|
||||
public static class HitTest {
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool GetWindowRect(IntPtr hWnd, out HitRect lpRect);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr WindowFromPoint(HitPoint p);
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern IntPtr GetDlgItem(IntPtr hDlg, int nIDDlgItem);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetAncestor(IntPtr hWnd, uint flags);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool IsIconic(IntPtr hWnd);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetWindowTextW(IntPtr hWnd, StringBuilder buffer, int maxCount);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetClassNameW(IntPtr hWnd, StringBuilder buffer, int maxCount);
|
||||
|
||||
public static string TitleOf(IntPtr h) {
|
||||
if (h == IntPtr.Zero) { return "<null-hwnd>"; }
|
||||
StringBuilder sb = new StringBuilder(512);
|
||||
GetWindowTextW(h, sb, sb.Capacity);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string ClassOf(IntPtr h) {
|
||||
if (h == IntPtr.Zero) { return "<null-hwnd>"; }
|
||||
StringBuilder sb = new StringBuilder(256);
|
||||
GetClassNameW(h, sb, sb.Capacity);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string RootTitleAt(int x, int y) {
|
||||
HitPoint p = new HitPoint();
|
||||
p.X = x;
|
||||
p.Y = y;
|
||||
IntPtr h = WindowFromPoint(p);
|
||||
if (h == IntPtr.Zero) { return "<no-window>"; }
|
||||
IntPtr root = GetAncestor(h, 2);
|
||||
return TitleOf(root);
|
||||
}
|
||||
|
||||
public static string RectOf(IntPtr h) {
|
||||
HitRect r;
|
||||
if (!GetWindowRect(h, out r)) { return "<getwindowrect-failed>"; }
|
||||
return r.Left.ToString() + "," + r.Top.ToString() + "," +
|
||||
(r.Right - r.Left).ToString() + "," + (r.Bottom - r.Top).ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
'@
|
||||
Add-Type -TypeDefinition $src -Language CSharp | Out-Null
|
||||
}
|
||||
|
||||
function Start-Tracked {
|
||||
param([string]$FilePath, [string[]]$ArgumentList = @(), [int]$TimeoutSec = 25)
|
||||
$p = Start-ScratchProcess -FilePath $FilePath -ArgumentList $ArgumentList -NoActivate -TimeoutSec $TimeoutSec
|
||||
[void]$script:Spawned.Add($p.ProcessId)
|
||||
return $p
|
||||
}
|
||||
|
||||
function Get-WindowRectString {
|
||||
param([IntPtr]$Handle)
|
||||
return [AgentDesktopProbe.HitTest]::RectOf($Handle)
|
||||
}
|
||||
|
||||
function Get-ElementRectString {
|
||||
param($El)
|
||||
try {
|
||||
$r = $El.Current.BoundingRectangle
|
||||
if ($r.IsEmpty) { return '<empty>' }
|
||||
return ([string][int]$r.Left + ',' + [int]$r.Top + ',' + [int]$r.Width + ',' + [int]$r.Height)
|
||||
} catch { return '<read-failed>' }
|
||||
}
|
||||
|
||||
function Get-SafeTopLevelName {
|
||||
param([AllowEmptyString()][string]$Name)
|
||||
if ([string]::IsNullOrEmpty($Name)) { return '' }
|
||||
if ($Name -like 'AgentDesktop Scratch*') { return $Name }
|
||||
if (@('Program Manager', 'Desktop', 'Windows Input Experience') -contains $Name) { return $Name }
|
||||
return (Protect-ProbeName -Name $Name)
|
||||
}
|
||||
|
||||
function Get-TopLevelName {
|
||||
param($El)
|
||||
$cur = $El
|
||||
for ($i = 0; $i -lt 40; $i++) {
|
||||
if ($null -eq $cur) { break }
|
||||
$parent = $null
|
||||
try { $parent = $Walker.GetParent($cur) } catch { }
|
||||
if ($null -eq $parent) { break }
|
||||
try { if ([System.Windows.Automation.Automation]::Compare($parent, $AE::RootElement)) { return (Get-SafeTopLevelName -Name ([string]$cur.Current.Name)) } } catch { }
|
||||
$cur = $parent
|
||||
}
|
||||
return '<no-top-level-reached>'
|
||||
}
|
||||
|
||||
function Get-ElementAtPoint {
|
||||
param([int]$X, [int]$Y)
|
||||
$point = New-Object System.Windows.Point($X, $Y)
|
||||
$el = $null
|
||||
try { $el = $AE::FromPoint($point) } catch { }
|
||||
if ($null -eq $el) {
|
||||
return [ordered]@{ resolved = $false }
|
||||
}
|
||||
$controlType = ''
|
||||
$automationId = ''
|
||||
$className = ''
|
||||
try { $controlType = $el.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '' } catch { }
|
||||
try { $automationId = [string]$el.Current.AutomationId } catch { }
|
||||
try { $className = [string]$el.Current.ClassName } catch { }
|
||||
return [ordered]@{
|
||||
resolved = $true
|
||||
controlType = $controlType
|
||||
automationId = $automationId
|
||||
className = $className
|
||||
topLevelWindow = (Get-TopLevelName -El $el)
|
||||
rect = (Get-ElementRectString -El $el)
|
||||
}
|
||||
}
|
||||
|
||||
function Test-PointInRect {
|
||||
param([string]$Rect, [int]$X, [int]$Y)
|
||||
$parts = $Rect -split ','
|
||||
if ($parts.Count -ne 4) { return $false }
|
||||
$l = [int]$parts[0]
|
||||
$t = [int]$parts[1]
|
||||
$r = $l + [int]$parts[2]
|
||||
$b = $t + [int]$parts[3]
|
||||
return ($X -ge $l -and $X -lt $r -and $Y -ge $t -and $Y -lt $b)
|
||||
}
|
||||
|
||||
function Find-ByAutomationId {
|
||||
param($Root, [string]$AutomationId, $Scope)
|
||||
try {
|
||||
$c = New-Object System.Windows.Automation.PropertyCondition($AE::AutomationIdProperty, $AutomationId)
|
||||
return $Root.FindFirst($Scope, $c)
|
||||
} catch { return $null }
|
||||
}
|
||||
|
||||
function Measure-Descendants {
|
||||
param($Root, $TreeWalkerToUse, [int]$Budget = 400)
|
||||
$n = 0
|
||||
$stack = New-Object System.Collections.Stack
|
||||
$stack.Push($Root)
|
||||
while ($stack.Count -gt 0 -and $n -lt $Budget) {
|
||||
$el = $stack.Pop()
|
||||
$n++
|
||||
try {
|
||||
$k = $TreeWalkerToUse.GetFirstChild($el)
|
||||
while ($null -ne $k) { $stack.Push($k); $k = $TreeWalkerToUse.GetNextSibling($k) }
|
||||
} catch { }
|
||||
}
|
||||
return $n
|
||||
}
|
||||
|
||||
$status = 'ok'
|
||||
$message = ''
|
||||
$resultData = [ordered]@{}
|
||||
|
||||
try {
|
||||
Initialize-ProbeNative
|
||||
Initialize-HitTestNative
|
||||
$scratchExe = Join-Path (Get-ProbeRoot) 'scratch\bin\ScratchForms.exe'
|
||||
if (-not (Test-Path -LiteralPath $scratchExe)) {
|
||||
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path (Get-ProbeRoot) 'scratch\build-scratch.ps1') | Out-Null
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $scratchExe)) { throw ('scratch fixture missing at ' + $scratchExe) }
|
||||
|
||||
$under = Start-Tracked -FilePath $scratchExe -ArgumentList @('--tag', 'u6-under', '--pos', ($UnderOrigin.X.ToString() + ',' + $UnderOrigin.Y))
|
||||
if ($under.MainWindowHandle -eq [IntPtr]::Zero) { throw 'the under window never appeared' }
|
||||
$over = Start-Tracked -FilePath $scratchExe -ArgumentList @('--tag', 'u6-over', '--pos', ($OverOrigin.X.ToString() + ',' + $OverOrigin.Y))
|
||||
if ($over.MainWindowHandle -eq [IntPtr]::Zero) { throw 'the over window never appeared' }
|
||||
Start-Sleep -Milliseconds 800
|
||||
|
||||
$underTitle = [AgentDesktopProbe.HitTest]::TitleOf($under.MainWindowHandle)
|
||||
$overTitle = [AgentDesktopProbe.HitTest]::TitleOf($over.MainWindowHandle)
|
||||
|
||||
[void][AgentDesktopProbe.Native]::ShowWindow($over.MainWindowHandle, 6)
|
||||
Start-Sleep -Milliseconds 500
|
||||
[void][AgentDesktopProbe.Native]::ShowWindow($over.MainWindowHandle, 9)
|
||||
Start-Sleep -Milliseconds 900
|
||||
|
||||
$underRect = Get-WindowRectString -Handle $under.MainWindowHandle
|
||||
$overRect = Get-WindowRectString -Handle $over.MainWindowHandle
|
||||
$u = $underRect -split ','
|
||||
$o = $overRect -split ','
|
||||
$overlapLeft = [Math]::Max([int]$u[0], [int]$o[0])
|
||||
$overlapTop = [Math]::Max([int]$u[1], [int]$o[1])
|
||||
$overlapRight = [Math]::Min(([int]$u[0] + [int]$u[2]), ([int]$o[0] + [int]$o[2]))
|
||||
$overlapBottom = [Math]::Min(([int]$u[1] + [int]$u[3]), ([int]$o[1] + [int]$o[3]))
|
||||
$probeX = [int](($overlapLeft + $overlapRight) / 2)
|
||||
$probeY = [int](($overlapTop + $overlapBottom) / 2)
|
||||
|
||||
$occludedElement = Get-ElementAtPoint -X $probeX -Y $probeY
|
||||
$win32RootTitle = Get-SafeTopLevelName -Name ([AgentDesktopProbe.HitTest]::RootTitleAt($probeX, $probeY))
|
||||
|
||||
$occlusion = [ordered]@{
|
||||
question = 'at a point covered by a second window, does ElementFromPoint return the occluder or the covered target'
|
||||
why = "2.6's occlusion gate decides whether an element is actually clickable. If ElementFromPoint answered with the covered element the gate would have no primitive to build on and every actionability check would be a guess."
|
||||
underWindowTitle = $underTitle
|
||||
overWindowTitle = $overTitle
|
||||
underWindowRect = $underRect
|
||||
overWindowRect = $overRect
|
||||
raiseMethod = 'ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) on the probe-owned over window. SetForegroundWindow is never called (KTD5).'
|
||||
overlapRect = ([string]$overlapLeft + ',' + $overlapTop + ',' + ($overlapRight - $overlapLeft) + ',' + ($overlapBottom - $overlapTop))
|
||||
probePoint = ([string]$probeX + ',' + $probeY)
|
||||
probePointInsideUnder = (Test-PointInRect -Rect $underRect -X $probeX -Y $probeY)
|
||||
probePointInsideOver = (Test-PointInRect -Rect $overRect -X $probeX -Y $probeY)
|
||||
elementFromPoint = $occludedElement
|
||||
win32WindowFromPointRootTitle = $win32RootTitle
|
||||
resolvedToOccluder = ($occludedElement.topLevelWindow -eq $overTitle)
|
||||
resolvedToOccluded = ($occludedElement.topLevelWindow -eq $underTitle)
|
||||
win32AgreesWithUia = ($win32RootTitle -eq $occludedElement.topLevelWindow)
|
||||
titleEvidenceNote = 'the two windows are distinguished by the --tag switch, so the identity that proves which window answered survives KTD9 normalization - process ids and window handles do not.'
|
||||
verdict = $(if ($occludedElement.topLevelWindow -eq $overTitle) { 'ok - ElementFromPoint returned the occluder' } elseif ($occludedElement.topLevelWindow -eq $underTitle) { 'FAIL - ElementFromPoint returned the covered target' } else { 'unexpected - ElementFromPoint returned neither scratch window' })
|
||||
}
|
||||
|
||||
# --- zero-size element --------------------------------------------------
|
||||
# the remaining two blocks ask what is at a coordinate INSIDE the under window, so
|
||||
# the under window is raised first with the same probe-owned minimize/restore. On
|
||||
# the first run of this probe it was not, and a hit test at btnZeroSize's origin
|
||||
# resolved to an unrelated console window that happened to sit above it - a foreign
|
||||
# element in the capture and a run-varying one.
|
||||
[void][AgentDesktopProbe.Native]::ShowWindow($under.MainWindowHandle, 6)
|
||||
Start-Sleep -Milliseconds 500
|
||||
[void][AgentDesktopProbe.Native]::ShowWindow($under.MainWindowHandle, 9)
|
||||
Start-Sleep -Milliseconds 900
|
||||
|
||||
$underRoot = $AE::FromHandle($under.MainWindowHandle)
|
||||
$zeroHandle = [AgentDesktopProbe.HitTest]::GetDlgItem($under.MainWindowHandle, $ZeroSizeControlId)
|
||||
$zeroRect = '<no-handle>'
|
||||
$zeroClass = '<no-handle>'
|
||||
$zeroVisible = $false
|
||||
if ($zeroHandle -ne [IntPtr]::Zero) {
|
||||
$zeroRect = Get-WindowRectString -Handle $zeroHandle
|
||||
$zeroClass = [AgentDesktopProbe.HitTest]::ClassOf($zeroHandle)
|
||||
$zeroVisible = [AgentDesktopProbe.HitTest]::IsWindowVisible($zeroHandle)
|
||||
}
|
||||
$zeroInControlView = Find-ByAutomationId -Root $underRoot -AutomationId 'btnZeroSize' -Scope $Descendants
|
||||
$zeroFromHandle = $null
|
||||
if ($zeroHandle -ne [IntPtr]::Zero) {
|
||||
try { $zeroFromHandle = $AE::FromHandle($zeroHandle) } catch { }
|
||||
}
|
||||
$zeroOrigin = $zeroRect -split ','
|
||||
$zeroPointElement = [ordered]@{ resolved = $false }
|
||||
if ($zeroOrigin.Count -eq 4) {
|
||||
$zeroPointElement = Get-ElementAtPoint -X ([int]$zeroOrigin[0]) -Y ([int]$zeroOrigin[1])
|
||||
}
|
||||
|
||||
$zeroSize = [ordered]@{
|
||||
question = 'what does a 0x0 control look like to a hit test, and is it reachable at all'
|
||||
controlId = $ZeroSizeControlId
|
||||
automationId = 'btnZeroSize'
|
||||
reachableByGetDlgItem = ($zeroHandle -ne [IntPtr]::Zero)
|
||||
win32Rect = $zeroRect
|
||||
win32ClassName = $zeroClass
|
||||
win32IsWindowVisible = $zeroVisible
|
||||
foundByFindFirstAutomationId = ($null -ne $zeroInControlView)
|
||||
controlViewNodeCount = (Measure-Descendants -Root $underRoot -TreeWalkerToUse $Walker)
|
||||
rawViewNodeCount = (Measure-Descendants -Root $underRoot -TreeWalkerToUse $RawWalker)
|
||||
fromHandleResolved = ($null -ne $zeroFromHandle)
|
||||
fromHandleControlType = $(if ($null -ne $zeroFromHandle) { ($zeroFromHandle.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '') } else { '<not-resolved>' })
|
||||
fromHandleRect = $(if ($null -ne $zeroFromHandle) { (Get-ElementRectString -El $zeroFromHandle) } else { '<not-resolved>' })
|
||||
fromHandleIsOffscreen = $(if ($null -ne $zeroFromHandle) { [bool]$zeroFromHandle.Current.IsOffscreen } else { $false })
|
||||
elementFromPointAtItsOrigin = $zeroPointElement
|
||||
priorEvidence = 'U7 recorded this control as absent from BOTH the COM RawView and ControlView walks of the same fixture. The rows above are the managed-side confirmation plus the two things the COM census could not answer: whether the control is still reachable by handle, and what a hit test at its coordinates returns.'
|
||||
consequence = 'a zero-size control is addressable through Win32 and through AutomationElement.FromHandle, but it can be enumerated by no tree walk and hit by no point. Any Windows actionability check that treats "not hit-testable" as "does not exist" will be right about the click and wrong about the element.'
|
||||
}
|
||||
|
||||
# --- minimized window ---------------------------------------------------
|
||||
$underOnlyX = [int]$u[0] + 30
|
||||
$underOnlyY = [int]$u[1] + [int]([int]$u[3] / 2)
|
||||
$visiblePointBefore = Get-ElementAtPoint -X $underOnlyX -Y $underOnlyY
|
||||
$descendantBefore = Find-ByAutomationId -Root $underRoot -AutomationId 'btnAction' -Scope $Descendants
|
||||
$topRectBefore = Get-ElementRectString -El $underRoot
|
||||
$topOffscreenBefore = [bool]$underRoot.Current.IsOffscreen
|
||||
$descRectBefore = '<not-found>'
|
||||
$descOffscreenBefore = $false
|
||||
if ($null -ne $descendantBefore) {
|
||||
$descRectBefore = Get-ElementRectString -El $descendantBefore
|
||||
$descOffscreenBefore = [bool]$descendantBefore.Current.IsOffscreen
|
||||
}
|
||||
|
||||
[void][AgentDesktopProbe.Native]::ShowWindow($under.MainWindowHandle, 6)
|
||||
Start-Sleep -Milliseconds 1200
|
||||
|
||||
$underRootAfter = $AE::FromHandle($under.MainWindowHandle)
|
||||
$descendantAfter = Find-ByAutomationId -Root $underRootAfter -AutomationId 'btnAction' -Scope $Descendants
|
||||
$topRectAfter = Get-ElementRectString -El $underRootAfter
|
||||
$topOffscreenAfter = [bool]$underRootAfter.Current.IsOffscreen
|
||||
$descRectAfter = '<not-found>'
|
||||
$descOffscreenAfter = $false
|
||||
if ($null -ne $descendantAfter) {
|
||||
$descRectAfter = Get-ElementRectString -El $descendantAfter
|
||||
$descOffscreenAfter = [bool]$descendantAfter.Current.IsOffscreen
|
||||
}
|
||||
$visiblePointAfter = Get-ElementAtPoint -X $underOnlyX -Y $underOnlyY
|
||||
$minimizedDescendantPoint = [ordered]@{ resolved = $false }
|
||||
$descParts = $descRectAfter -split ','
|
||||
if ($descParts.Count -eq 4 -and [int]$descParts[0] -gt -32768) {
|
||||
$minimizedDescendantPoint = Get-ElementAtPoint -X ([int]$descParts[0] + 2) -Y ([int]$descParts[1] + 2)
|
||||
}
|
||||
$win32IsIconic = [AgentDesktopProbe.HitTest]::IsIconic($under.MainWindowHandle)
|
||||
|
||||
[void][AgentDesktopProbe.Native]::ShowWindow($under.MainWindowHandle, 9)
|
||||
Start-Sleep -Milliseconds 900
|
||||
$topRectRestored = Get-ElementRectString -El ($AE::FromHandle($under.MainWindowHandle))
|
||||
|
||||
$minimized = [ordered]@{
|
||||
question = 'what does a minimized window report, and what does a hit test at its former coordinates return'
|
||||
priorEvidence = 'U3 measured that minimizing degenerates geometry in two different shapes: only the TOP-LEVEL window reports an empty rect, while descendants report REAL dimensions anchored at -32000, and every node still reports IsOffscreen false. An occlusion gate testing emptiness or IsOffscreen alone accepts both as visible.'
|
||||
pointProbed = ([string]$underOnlyX + ',' + $underOnlyY)
|
||||
pointChoice = 'a point inside the under window but outside the over window, so the only thing that can change the answer is the minimize itself'
|
||||
topLevelRectBefore = $topRectBefore
|
||||
topLevelRectAfterMinimize = $topRectAfter
|
||||
topLevelRectAfterRestore = $topRectRestored
|
||||
topLevelIsOffscreenBefore = $topOffscreenBefore
|
||||
topLevelIsOffscreenAfterMinimize = $topOffscreenAfter
|
||||
descendantAutomationId = 'btnAction'
|
||||
descendantFoundAfterMinimize = ($null -ne $descendantAfter)
|
||||
descendantRectBefore = $descRectBefore
|
||||
descendantRectAfterMinimize = $descRectAfter
|
||||
descendantIsOffscreenBefore = $descOffscreenBefore
|
||||
descendantIsOffscreenAfterMinimize = $descOffscreenAfter
|
||||
win32IsIconic = $win32IsIconic
|
||||
elementFromPointBefore = $visiblePointBefore
|
||||
elementFromPointAfterMinimize = $visiblePointAfter
|
||||
elementFromPointAtDescendantAnchor = $minimizedDescendantPoint
|
||||
hitTestFreedTheCoordinates = ($visiblePointBefore.topLevelWindow -ne $visiblePointAfter.topLevelWindow)
|
||||
gateConsequence = 'ElementFromPoint is the one primitive here that degrades cleanly: after the minimize the coordinates resolve to whatever is genuinely on top instead of to the minimized window. Emptiness and IsOffscreen do not - the top-level rect empties while its descendants keep real dimensions at -32000, and IsOffscreen stays false on both. 2.6 should gate on a hit test, and where it must reason about geometry it has to test the -32000 anchor explicitly, because a -32000 rect is neither empty nor offscreen by either property.'
|
||||
rectRecordingNote = 'every rect here is a comma-joined string. As numeric x/y/width/height keys the KTD9 normalizer would bucket them to 8 px, which rounds -32000 into a neighbouring bucket and erases exactly the anchor this row exists to record.'
|
||||
}
|
||||
|
||||
[void](Write-ProbeJson -Probe $Probe -Name 'hittest.json' -InputObject ([ordered]@{
|
||||
probe = $Probe
|
||||
question = 'do the hit-test primitives 2.6 needs behave correctly against occluded, zero-size and minimized targets'
|
||||
stack = 'managed-System.Windows.Automation AutomationElement.FromPoint, cross-checked with Win32 WindowFromPoint'
|
||||
scope = 'system'
|
||||
safety = 'both windows are launched by this probe at deterministic --pos origins; nothing else on the desktop is touched, moved or raised.'
|
||||
occlusion = $occlusion
|
||||
zeroSize = $zeroSize
|
||||
minimized = $minimized
|
||||
}))
|
||||
|
||||
$resultData['occlusionVerdict'] = $occlusion.verdict
|
||||
$resultData['zeroSizeInTree'] = $zeroSize.foundByFindFirstAutomationId
|
||||
$resultData['zeroSizeReachableByHandle'] = $zeroSize.reachableByGetDlgItem
|
||||
$resultData['minimizedTopRect'] = $minimized.topLevelRectAfterMinimize
|
||||
$resultData['minimizedDescendantRect'] = $minimized.descendantRectAfterMinimize
|
||||
$resultData['minimizedDescendantIsOffscreen'] = $minimized.descendantIsOffscreenAfterMinimize
|
||||
$message = 'hittest: ' + $occlusion.verdict + '; zero-size in tree=' + $zeroSize.foundByFindFirstAutomationId + '; minimized descendant rect=' + $minimized.descendantRectAfterMinimize
|
||||
} catch {
|
||||
$status = 'fail'
|
||||
$message = ($_.Exception.Message -replace '[\r\n]+', ' ')
|
||||
Write-ProbeLog -Message ('probe failed: ' + $message) -Level 'error'
|
||||
} finally {
|
||||
foreach ($id in @($script:Spawned)) {
|
||||
try { Stop-ScratchProcess -ProcessId $id } catch { Write-ProbeLog -Message ('teardown: ' + $_.Exception.Message) -Level 'warn' }
|
||||
}
|
||||
foreach ($f in @(Get-ChildItem -LiteralPath (Get-CaptureDir -Probe $Probe) -File -ErrorAction SilentlyContinue)) {
|
||||
if ($f.Name -like '*.normalized') { continue }
|
||||
if (-not (Test-CaptureRedaction -Path $f.FullName)) { $status = 'fail'; $message = ('redaction residue in ' + $f.Name) }
|
||||
}
|
||||
}
|
||||
|
||||
Write-ProbeResult -Probe $Probe -Status $status -Message $message -Data $resultData
|
||||
if ($status -eq 'fail') { exit 1 }
|
||||
exit 0
|
||||
23
probes/windows/captures/05-interactions/focus.json
Normal file
23
probes/windows/captures/05-interactions/focus.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"probe": "05-interactions",
|
||||
"question": "does AutomationElement.SetFocus on a background window move the desktop foreground, or only keyboard focus inside that window",
|
||||
"why": "2.7 wants headless interaction. If SetFocus steals foreground, no interaction that needs focus can be headless; if it does not, focus-dependent actions are headless-safe and the design can rely on it.",
|
||||
"method": "a second probe-owned scratch window is brought forward with ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) - the sanctioned pattern, SetForegroundWindow is never called - then SetFocus is issued on an element of the OTHER probe-owned window and the foreground is re-read.",
|
||||
"foregroundBefore": {
|
||||
"windowTitle": "AgentDesktop Scratch WinForms [u5-default]",
|
||||
"processId": 4052
|
||||
},
|
||||
"foregroundAfter": {
|
||||
"windowTitle": "AgentDesktop Scratch WPF [u5]",
|
||||
"processId": 9180
|
||||
},
|
||||
"foregroundChanged": true,
|
||||
"foregroundWasTheOtherScratchWindow": true,
|
||||
"setFocusTarget": "wpf txtValue",
|
||||
"setFocusError": "",
|
||||
"hadKeyboardFocusBefore": false,
|
||||
"hasKeyboardFocusAfter": true,
|
||||
"focusedElementAutomationIdAfter": "txtValue",
|
||||
"titleEvidenceNote": "the two foreground observations are nested objects with a key named exactly processId, so the KTD9 normalizer canonicalizes the run-varying pid to \u003cpid\u003e. A flat foregroundPidBefore/foregroundPidAfter pair was tried first and does NOT match the normalizer\u0027s word-boundary rule: the raw pids survived into the twin and the capture failed to reproduce across runs. The probe-owned window titles are what carry the fact into the normalized twin.",
|
||||
"verdict": "SetFocus moved the desktop foreground"
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"probe": "05-interactions",
|
||||
"question": "does AutomationElement.SetFocus on a background window move the desktop foreground, or only keyboard focus inside that window",
|
||||
"why": "2.7 wants headless interaction. If SetFocus steals foreground, no interaction that needs focus can be headless; if it does not, focus-dependent actions are headless-safe and the design can rely on it.",
|
||||
"method": "a second probe-owned scratch window is brought forward with ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) - the sanctioned pattern, SetForegroundWindow is never called - then SetFocus is issued on an element of the OTHER probe-owned window and the foreground is re-read.",
|
||||
"foregroundBefore": {
|
||||
"windowTitle": "AgentDesktop Scratch WinForms [u5-default]",
|
||||
"processId": <pid>
|
||||
},
|
||||
"foregroundAfter": {
|
||||
"windowTitle": "AgentDesktop Scratch WPF [u5]",
|
||||
"processId": <pid>
|
||||
},
|
||||
"foregroundChanged": true,
|
||||
"foregroundWasTheOtherScratchWindow": true,
|
||||
"setFocusTarget": "wpf txtValue",
|
||||
"setFocusError": "",
|
||||
"hadKeyboardFocusBefore": false,
|
||||
"hasKeyboardFocusAfter": true,
|
||||
"focusedElementAutomationIdAfter": "txtValue",
|
||||
"titleEvidenceNote": "the two foreground observations are nested objects with a key named exactly processId, so the KTD9 normalizer canonicalizes the run-varying pid to \u003cpid\u003e. A flat foregroundPidBefore/foregroundPidAfter pair was tried first and does NOT match the normalizer\u0027s word-boundary rule: the raw pids survived into the twin and the capture failed to reproduce across runs. The probe-owned window titles are what carry the fact into the normalized twin.",
|
||||
"verdict": "SetFocus moved the desktop foreground"
|
||||
}
|
||||
394
probes/windows/captures/05-interactions/interactions.json
Normal file
394
probes/windows/captures/05-interactions/interactions.json
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
{
|
||||
"probe": "05-interactions",
|
||||
"question": "does every 2.0(3) interaction actually take effect through a UIA pattern, verified by independent re-read rather than by the call return",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"scope": "app/provider",
|
||||
"verificationDiscipline": "no row trusts the pattern call return. Element state is re-read after re-finding the element by AutomationId, and where the fixture exposes an independent sink control (lblStatus, lblScrollPos) that separate element is read as well.",
|
||||
"fixtureArtifactNote": "winforms-default rows report pattern-unavailable because the fixture installs a server-side IRawElementProviderSimple that suppresses the client-side proxies and WinForms\u0027 own providers. That is a property of this fixture, not of WinForms or of Windows. winforms-host-providers rows show what WinForms itself exposes to a managed client.",
|
||||
"managedVsComNote": "several winforms-host-providers rows report pattern-unavailable for patterns the UIA3 COM census in captures/08-uia3-com/census.json records on the same providers (Invoke and Toggle on chkToggle, Value on txtValue, SelectionItem on the ListItems, Scroll on pnlScroll). Under KTD1 the COM row is the product-relevant one because the Rust adapter wraps a UIA3 COM client; these rows are filed as managed-client divergence, not as absent affordances. ExpandCollapse on cboChoice is the one WinForms pattern both stacks agree on, which is why it is the only non-WPF ok row here.",
|
||||
"wheelCounterpartNote": "the SendInput wheel arm is deliberately not in this file. It runs in 06-input-synthesis against the WinForms pnlScroll and its lblScrollPos sink, because that is the control this probe could NOT drive by pattern - the pair of the two captures is the pattern-vs-physical comparison 2.6 needs.",
|
||||
"rows": [
|
||||
{
|
||||
"interaction": "invoke",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "btnAction",
|
||||
"pattern": "Invoke",
|
||||
"controlType": "Button",
|
||||
"patternAcquired": true,
|
||||
"supportedPatterns": [
|
||||
"Invoke",
|
||||
"SynchronizedInput"
|
||||
],
|
||||
"sinkElement": "lblStatus (a different element than the one invoked)",
|
||||
"sinkBefore": "status:ready",
|
||||
"sinkAfter": "action:1",
|
||||
"changed": true,
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "invoke",
|
||||
"target": "winforms-host-providers",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "btnAction",
|
||||
"pattern": "Invoke",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no InvokePattern on this element in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "invoke",
|
||||
"target": "winforms-default",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "btnAction",
|
||||
"pattern": "Invoke",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no InvokePattern on this element in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "toggle",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "chkToggle",
|
||||
"pattern": "Toggle",
|
||||
"controlType": "CheckBox",
|
||||
"patternAcquired": true,
|
||||
"preState": "Off",
|
||||
"postState": "On",
|
||||
"reReadMethod": "the element is re-found by AutomationId and its TogglePattern re-acquired, so the post-state is not read off the same pattern object the call returned",
|
||||
"sinkBefore": "action:1",
|
||||
"sinkAfter": "action:1",
|
||||
"sinkChanged": false,
|
||||
"sinkNote": "the WPF fixture updates lblStatus from a Click handler. TogglePattern.Toggle flips ToggleState without raising Click, so the sink is expected to stay silent here while the element state changes - which is exactly why the element is re-read instead of the sink being trusted as the only observable.",
|
||||
"changed": true,
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "toggle",
|
||||
"target": "winforms-host-providers",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "chkToggle",
|
||||
"pattern": "Toggle",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no TogglePattern on this element in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "toggle",
|
||||
"target": "winforms-default",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "chkToggle",
|
||||
"pattern": "Toggle",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no TogglePattern on this element in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "set-value/ascii",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "txtValue",
|
||||
"pattern": "Value",
|
||||
"controlType": "Edit",
|
||||
"patternAcquired": true,
|
||||
"payloadKind": "ascii",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
|
||||
"preValueShape": {
|
||||
"utf16Units": 10,
|
||||
"codepoints": 10,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "730365c1825e69a9171fd18ccdefab7962df67a8fcb7b64e4e3c8dd8bdf8ec6b"
|
||||
},
|
||||
"expectedShape": {
|
||||
"utf16Units": 20,
|
||||
"codepoints": 20,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "030def08bed320dbbc368c2ea990e349c43af499a9e25a5c6727d280e9e816e0"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 20,
|
||||
"codepoints": 20,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "030def08bed320dbbc368c2ea990e349c43af499a9e25a5c6727d280e9e816e0"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
|
||||
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "set-value/cjk",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "txtValue",
|
||||
"pattern": "Value",
|
||||
"controlType": "Edit",
|
||||
"patternAcquired": true,
|
||||
"payloadKind": "cjk",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
|
||||
"preValueShape": {
|
||||
"utf16Units": 20,
|
||||
"codepoints": 20,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "030def08bed320dbbc368c2ea990e349c43af499a9e25a5c6727d280e9e816e0"
|
||||
},
|
||||
"expectedShape": {
|
||||
"utf16Units": 3,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 3,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
|
||||
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "set-value/astral-plane",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "txtValue",
|
||||
"pattern": "Value",
|
||||
"controlType": "Edit",
|
||||
"patternAcquired": true,
|
||||
"payloadKind": "astral-plane",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
|
||||
"preValueShape": {
|
||||
"utf16Units": 3,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
|
||||
},
|
||||
"expectedShape": {
|
||||
"utf16Units": 4,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 4,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
|
||||
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "set-value/mixed",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "txtValue",
|
||||
"pattern": "Value",
|
||||
"controlType": "Edit",
|
||||
"patternAcquired": true,
|
||||
"payloadKind": "mixed",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
|
||||
"preValueShape": {
|
||||
"utf16Units": 4,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
|
||||
},
|
||||
"expectedShape": {
|
||||
"utf16Units": 29,
|
||||
"codepoints": 28,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 29,
|
||||
"codepoints": 28,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
|
||||
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "set-value",
|
||||
"target": "winforms-host-providers",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "txtValue",
|
||||
"pattern": "Value",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no ValuePattern on this element in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "expand-collapse",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "cboChoice",
|
||||
"pattern": "ExpandCollapse",
|
||||
"controlType": "ComboBox",
|
||||
"patternAcquired": true,
|
||||
"preState": "Collapsed",
|
||||
"stateAfterExpand": "Expanded",
|
||||
"stateAfterCollapse": "Collapsed",
|
||||
"reReadMethod": "state re-read after each half of the round trip from a freshly found element",
|
||||
"roundTripped": true,
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "expand-collapse",
|
||||
"target": "winforms-host-providers",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "cboChoice",
|
||||
"pattern": "ExpandCollapse",
|
||||
"controlType": "ComboBox",
|
||||
"patternAcquired": true,
|
||||
"preState": "Collapsed",
|
||||
"stateAfterExpand": "Expanded",
|
||||
"stateAfterCollapse": "Collapsed",
|
||||
"reReadMethod": "state re-read after each half of the round trip from a freshly found element",
|
||||
"roundTripped": true,
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "expand-collapse",
|
||||
"target": "winforms-default",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "cboChoice",
|
||||
"pattern": "ExpandCollapse",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no ExpandCollapsePattern on this element in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "select",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "lstItems/item[2]",
|
||||
"pattern": "SelectionItem",
|
||||
"controlType": "ListItem",
|
||||
"patternAcquired": true,
|
||||
"selectionCountBefore": 0,
|
||||
"selectionCountAfter": 1,
|
||||
"isSelectedAfter": true,
|
||||
"sinkBefore": "action:1",
|
||||
"sinkAfter": "list-sel:Item-Charlie",
|
||||
"reReadMethod": "container SelectionPattern re-acquired from a freshly found list, plus the item IsSelected re-read, plus the independent lblStatus sink",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "select",
|
||||
"target": "winforms-host-providers",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "lstItems/item[2]",
|
||||
"pattern": "SelectionItem",
|
||||
"controlType": "\u003celement-not-found\u003e",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no ListItem with SelectionItemPattern reachable under lstItems in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "select",
|
||||
"target": "winforms-default",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "lstItems/item[2]",
|
||||
"pattern": "SelectionItem",
|
||||
"controlType": "\u003celement-not-found\u003e",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no ListItem with SelectionItemPattern reachable under lstItems in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "scroll-via-pattern",
|
||||
"target": "winforms-host-providers",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "pnlScroll",
|
||||
"pattern": "Scroll",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "the UIA3 COM census (captures/08-uia3-com/census.json) records Scroll on this exact provider in BOTH fixture modes; the managed client cannot acquire it. Divergence row, not a platform verdict."
|
||||
},
|
||||
{
|
||||
"interaction": "scroll-via-pattern",
|
||||
"target": "winforms-default",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "pnlScroll",
|
||||
"pattern": "Scroll",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "the UIA3 COM census (captures/08-uia3-com/census.json) records Scroll on this exact provider in BOTH fixture modes; the managed client cannot acquire it. Divergence row, not a platform verdict."
|
||||
},
|
||||
{
|
||||
"interaction": "scroll-via-pattern",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "lstItems",
|
||||
"pattern": "Scroll",
|
||||
"controlType": "List",
|
||||
"patternAcquired": true,
|
||||
"makeScrollableMethod": "the window is shrunk with SetWindowPos(SWP_NOACTIVATE) until the ListBox viewport is smaller than its five items; the client-height step that achieved it is recorded",
|
||||
"shrinkStepUsed": 240,
|
||||
"verticallyScrollable": true,
|
||||
"verticalPercentBefore": 0,
|
||||
"verticalPercentAfter": 80,
|
||||
"reReadMethod": "percent re-read from a freshly acquired ScrollPattern on a freshly found element",
|
||||
"verdict": "ok"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,394 @@
|
|||
{
|
||||
"probe": "05-interactions",
|
||||
"question": "does every 2.0(3) interaction actually take effect through a UIA pattern, verified by independent re-read rather than by the call return",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"scope": "app/provider",
|
||||
"verificationDiscipline": "no row trusts the pattern call return. Element state is re-read after re-finding the element by AutomationId, and where the fixture exposes an independent sink control (lblStatus, lblScrollPos) that separate element is read as well.",
|
||||
"fixtureArtifactNote": "winforms-default rows report pattern-unavailable because the fixture installs a server-side IRawElementProviderSimple that suppresses the client-side proxies and WinForms\u0027 own providers. That is a property of this fixture, not of WinForms or of Windows. winforms-host-providers rows show what WinForms itself exposes to a managed client.",
|
||||
"managedVsComNote": "several winforms-host-providers rows report pattern-unavailable for patterns the UIA3 COM census in captures/08-uia3-com/census.json records on the same providers (Invoke and Toggle on chkToggle, Value on txtValue, SelectionItem on the ListItems, Scroll on pnlScroll). Under KTD1 the COM row is the product-relevant one because the Rust adapter wraps a UIA3 COM client; these rows are filed as managed-client divergence, not as absent affordances. ExpandCollapse on cboChoice is the one WinForms pattern both stacks agree on, which is why it is the only non-WPF ok row here.",
|
||||
"wheelCounterpartNote": "the SendInput wheel arm is deliberately not in this file. It runs in 06-input-synthesis against the WinForms pnlScroll and its lblScrollPos sink, because that is the control this probe could NOT drive by pattern - the pair of the two captures is the pattern-vs-physical comparison 2.6 needs.",
|
||||
"rows": [
|
||||
{
|
||||
"interaction": "invoke",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "btnAction",
|
||||
"pattern": "Invoke",
|
||||
"controlType": "Button",
|
||||
"patternAcquired": true,
|
||||
"supportedPatterns": [
|
||||
"Invoke",
|
||||
"SynchronizedInput"
|
||||
],
|
||||
"sinkElement": "lblStatus (a different element than the one invoked)",
|
||||
"sinkBefore": "status:ready",
|
||||
"sinkAfter": "action:1",
|
||||
"changed": true,
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "invoke",
|
||||
"target": "winforms-host-providers",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "btnAction",
|
||||
"pattern": "Invoke",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no InvokePattern on this element in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "invoke",
|
||||
"target": "winforms-default",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "btnAction",
|
||||
"pattern": "Invoke",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no InvokePattern on this element in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "toggle",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "chkToggle",
|
||||
"pattern": "Toggle",
|
||||
"controlType": "CheckBox",
|
||||
"patternAcquired": true,
|
||||
"preState": "Off",
|
||||
"postState": "On",
|
||||
"reReadMethod": "the element is re-found by AutomationId and its TogglePattern re-acquired, so the post-state is not read off the same pattern object the call returned",
|
||||
"sinkBefore": "action:1",
|
||||
"sinkAfter": "action:1",
|
||||
"sinkChanged": false,
|
||||
"sinkNote": "the WPF fixture updates lblStatus from a Click handler. TogglePattern.Toggle flips ToggleState without raising Click, so the sink is expected to stay silent here while the element state changes - which is exactly why the element is re-read instead of the sink being trusted as the only observable.",
|
||||
"changed": true,
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "toggle",
|
||||
"target": "winforms-host-providers",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "chkToggle",
|
||||
"pattern": "Toggle",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no TogglePattern on this element in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "toggle",
|
||||
"target": "winforms-default",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "chkToggle",
|
||||
"pattern": "Toggle",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no TogglePattern on this element in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "set-value/ascii",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "txtValue",
|
||||
"pattern": "Value",
|
||||
"controlType": "Edit",
|
||||
"patternAcquired": true,
|
||||
"payloadKind": "ascii",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
|
||||
"preValueShape": {
|
||||
"utf16Units": 10,
|
||||
"codepoints": 10,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "730365c1825e69a9171fd18ccdefab7962df67a8fcb7b64e4e3c8dd8bdf8ec6b"
|
||||
},
|
||||
"expectedShape": {
|
||||
"utf16Units": 20,
|
||||
"codepoints": 20,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "030def08bed320dbbc368c2ea990e349c43af499a9e25a5c6727d280e9e816e0"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 20,
|
||||
"codepoints": 20,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "030def08bed320dbbc368c2ea990e349c43af499a9e25a5c6727d280e9e816e0"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
|
||||
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "set-value/cjk",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "txtValue",
|
||||
"pattern": "Value",
|
||||
"controlType": "Edit",
|
||||
"patternAcquired": true,
|
||||
"payloadKind": "cjk",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
|
||||
"preValueShape": {
|
||||
"utf16Units": 20,
|
||||
"codepoints": 20,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "030def08bed320dbbc368c2ea990e349c43af499a9e25a5c6727d280e9e816e0"
|
||||
},
|
||||
"expectedShape": {
|
||||
"utf16Units": 3,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 3,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
|
||||
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "set-value/astral-plane",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "txtValue",
|
||||
"pattern": "Value",
|
||||
"controlType": "Edit",
|
||||
"patternAcquired": true,
|
||||
"payloadKind": "astral-plane",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
|
||||
"preValueShape": {
|
||||
"utf16Units": 3,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
|
||||
},
|
||||
"expectedShape": {
|
||||
"utf16Units": 4,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 4,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
|
||||
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "set-value/mixed",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "txtValue",
|
||||
"pattern": "Value",
|
||||
"controlType": "Edit",
|
||||
"patternAcquired": true,
|
||||
"payloadKind": "mixed",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 so payload integrity does not depend on this file\u0027s encoding (R12)",
|
||||
"preValueShape": {
|
||||
"utf16Units": 4,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
|
||||
},
|
||||
"expectedShape": {
|
||||
"utf16Units": 29,
|
||||
"codepoints": 28,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 29,
|
||||
"codepoints": 28,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"reReadMethod": "element re-found by AutomationId and ValuePattern re-acquired before reading",
|
||||
"payloadHandling": "only length, codepoint counts and a SHA-256 of the UTF-16 bytes are recorded; no payload text reaches the capture",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "set-value",
|
||||
"target": "winforms-host-providers",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "txtValue",
|
||||
"pattern": "Value",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no ValuePattern on this element in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "expand-collapse",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "cboChoice",
|
||||
"pattern": "ExpandCollapse",
|
||||
"controlType": "ComboBox",
|
||||
"patternAcquired": true,
|
||||
"preState": "Collapsed",
|
||||
"stateAfterExpand": "Expanded",
|
||||
"stateAfterCollapse": "Collapsed",
|
||||
"reReadMethod": "state re-read after each half of the round trip from a freshly found element",
|
||||
"roundTripped": true,
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "expand-collapse",
|
||||
"target": "winforms-host-providers",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "cboChoice",
|
||||
"pattern": "ExpandCollapse",
|
||||
"controlType": "ComboBox",
|
||||
"patternAcquired": true,
|
||||
"preState": "Collapsed",
|
||||
"stateAfterExpand": "Expanded",
|
||||
"stateAfterCollapse": "Collapsed",
|
||||
"reReadMethod": "state re-read after each half of the round trip from a freshly found element",
|
||||
"roundTripped": true,
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "expand-collapse",
|
||||
"target": "winforms-default",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "cboChoice",
|
||||
"pattern": "ExpandCollapse",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no ExpandCollapsePattern on this element in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "select",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "lstItems/item[2]",
|
||||
"pattern": "SelectionItem",
|
||||
"controlType": "ListItem",
|
||||
"patternAcquired": true,
|
||||
"selectionCountBefore": 0,
|
||||
"selectionCountAfter": 1,
|
||||
"isSelectedAfter": true,
|
||||
"sinkBefore": "action:1",
|
||||
"sinkAfter": "list-sel:Item-Charlie",
|
||||
"reReadMethod": "container SelectionPattern re-acquired from a freshly found list, plus the item IsSelected re-read, plus the independent lblStatus sink",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"interaction": "select",
|
||||
"target": "winforms-host-providers",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "lstItems/item[2]",
|
||||
"pattern": "SelectionItem",
|
||||
"controlType": "\u003celement-not-found\u003e",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no ListItem with SelectionItemPattern reachable under lstItems in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "select",
|
||||
"target": "winforms-default",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "lstItems/item[2]",
|
||||
"pattern": "SelectionItem",
|
||||
"controlType": "\u003celement-not-found\u003e",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "no ListItem with SelectionItemPattern reachable under lstItems in this fixture mode; recorded as a verdict row"
|
||||
},
|
||||
{
|
||||
"interaction": "scroll-via-pattern",
|
||||
"target": "winforms-host-providers",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "pnlScroll",
|
||||
"pattern": "Scroll",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "the UIA3 COM census (captures/08-uia3-com/census.json) records Scroll on this exact provider in BOTH fixture modes; the managed client cannot acquire it. Divergence row, not a platform verdict."
|
||||
},
|
||||
{
|
||||
"interaction": "scroll-via-pattern",
|
||||
"target": "winforms-default",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "pnlScroll",
|
||||
"pattern": "Scroll",
|
||||
"controlType": "Pane",
|
||||
"patternAcquired": false,
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"verdict": "pattern-unavailable",
|
||||
"detail": "the UIA3 COM census (captures/08-uia3-com/census.json) records Scroll on this exact provider in BOTH fixture modes; the managed client cannot acquire it. Divergence row, not a platform verdict."
|
||||
},
|
||||
{
|
||||
"interaction": "scroll-via-pattern",
|
||||
"target": "wpf",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"automationId": "lstItems",
|
||||
"pattern": "Scroll",
|
||||
"controlType": "List",
|
||||
"patternAcquired": true,
|
||||
"makeScrollableMethod": "the window is shrunk with SetWindowPos(SWP_NOACTIVATE) until the ListBox viewport is smaller than its five items; the client-height step that achieved it is recorded",
|
||||
"shrinkStepUsed": 240,
|
||||
"verticallyScrollable": true,
|
||||
"verticalPercentBefore": 0,
|
||||
"verticalPercentAfter": 80,
|
||||
"reReadMethod": "percent re-read from a freshly acquired ScrollPattern on a freshly found element",
|
||||
"verdict": "ok"
|
||||
}
|
||||
]
|
||||
}
|
||||
84
probes/windows/captures/05-interactions/text-pattern.json
Normal file
84
probes/windows/captures/05-interactions/text-pattern.json
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"probe": "05-interactions",
|
||||
"question": "is TextPattern exposed on a classic Win32 Edit on Server 2019 at all, and does the managed stack agree with the COM stack about it",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"scope": "app/provider",
|
||||
"whyTwoLookups": "GetSupportedPatterns is not a reliable negative: it is answered by the client-side proxy that happens to be bound. Every row therefore also records TryGetCurrentPattern per pattern, which is the call the adapter would actually make. Both lookups are recorded because they disagree.",
|
||||
"comCounterpart": "captures/08-uia3-com/census.json records the SAME notepad window through hand-declared UIA3 COM as one Document element carrying LegacyIAccessible, Scroll, Text, Text2 and Value. Any managed row below that reports Text absent is a client-stack divergence, not an absence of the provider.",
|
||||
"p2o12Relevance": "2.0(3) text get/selection/caret/insert and P2-O12 both assume a Text-capable Edit. The WPF row shows what a full managed TextPattern surface supports; the notepad rows show what the real Win32 Edit gives the two client stacks.",
|
||||
"rows": [
|
||||
{
|
||||
"target": "wpf-txtValue",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"controlType": "Edit",
|
||||
"className": "TextBox",
|
||||
"supportedPatterns": [
|
||||
"Scroll",
|
||||
"SynchronizedInput",
|
||||
"Text",
|
||||
"Value"
|
||||
],
|
||||
"textPatternExposed": true,
|
||||
"getText": {
|
||||
"expectedShape": {
|
||||
"utf16Units": 29,
|
||||
"codepoints": 28,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 29,
|
||||
"codepoints": 28,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
|
||||
},
|
||||
"exactMatch": true,
|
||||
"note": "the value was set through ValuePattern and read back through TextPattern.DocumentRange.GetText - a cross-pattern read, not the same call returning its own argument"
|
||||
},
|
||||
"selection": {
|
||||
"rangesBefore": 1,
|
||||
"rangesAfter": 1,
|
||||
"requestedUnits": 5,
|
||||
"selectedShape": {
|
||||
"utf16Units": 5,
|
||||
"codepoints": 5,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "9d17cf8d7d1b14ceec751b8f91702aaea1d386a4ed188d5e279cbe02394b18ca"
|
||||
},
|
||||
"matchesSubstring": true
|
||||
},
|
||||
"caret": {
|
||||
"method": "a degenerate range (End collapsed onto Start) is Selected at character offset 4 and the selection is re-read",
|
||||
"rangesAfter": 1,
|
||||
"degenerate": true
|
||||
},
|
||||
"insert": {
|
||||
"method": "TextPattern is read-only by contract; insertion is done through ValuePattern.SetValue and verified through the TextPattern read above",
|
||||
"verified": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": "notepad-edit",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"lookup": "FindFirst(Descendants, ControlType=Document)",
|
||||
"resolved": false
|
||||
},
|
||||
{
|
||||
"target": "notepad-edit",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"lookup": "AutomationElement.FromHandle(the Edit child HWND found with FindWindowEx)",
|
||||
"resolved": true,
|
||||
"controlType": "Pane",
|
||||
"className": "Edit",
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"tryGetCurrentPattern_Text": false,
|
||||
"tryGetCurrentPattern_Value": false,
|
||||
"tryGetCurrentPattern_Scroll": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"probe": "05-interactions",
|
||||
"question": "is TextPattern exposed on a classic Win32 Edit on Server 2019 at all, and does the managed stack agree with the COM stack about it",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"scope": "app/provider",
|
||||
"whyTwoLookups": "GetSupportedPatterns is not a reliable negative: it is answered by the client-side proxy that happens to be bound. Every row therefore also records TryGetCurrentPattern per pattern, which is the call the adapter would actually make. Both lookups are recorded because they disagree.",
|
||||
"comCounterpart": "captures/08-uia3-com/census.json records the SAME notepad window through hand-declared UIA3 COM as one Document element carrying LegacyIAccessible, Scroll, Text, Text2 and Value. Any managed row below that reports Text absent is a client-stack divergence, not an absence of the provider.",
|
||||
"p2o12Relevance": "2.0(3) text get/selection/caret/insert and P2-O12 both assume a Text-capable Edit. The WPF row shows what a full managed TextPattern surface supports; the notepad rows show what the real Win32 Edit gives the two client stacks.",
|
||||
"rows": [
|
||||
{
|
||||
"target": "wpf-txtValue",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"controlType": "Edit",
|
||||
"className": "TextBox",
|
||||
"supportedPatterns": [
|
||||
"Scroll",
|
||||
"SynchronizedInput",
|
||||
"Text",
|
||||
"Value"
|
||||
],
|
||||
"textPatternExposed": true,
|
||||
"getText": {
|
||||
"expectedShape": {
|
||||
"utf16Units": 29,
|
||||
"codepoints": 28,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 29,
|
||||
"codepoints": 28,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "5783f6f2168ecd35d4775d850bf880d7dca2b3d8a3a811f6893d4c82a089f441"
|
||||
},
|
||||
"exactMatch": true,
|
||||
"note": "the value was set through ValuePattern and read back through TextPattern.DocumentRange.GetText - a cross-pattern read, not the same call returning its own argument"
|
||||
},
|
||||
"selection": {
|
||||
"rangesBefore": 1,
|
||||
"rangesAfter": 1,
|
||||
"requestedUnits": 5,
|
||||
"selectedShape": {
|
||||
"utf16Units": 5,
|
||||
"codepoints": 5,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "9d17cf8d7d1b14ceec751b8f91702aaea1d386a4ed188d5e279cbe02394b18ca"
|
||||
},
|
||||
"matchesSubstring": true
|
||||
},
|
||||
"caret": {
|
||||
"method": "a degenerate range (End collapsed onto Start) is Selected at character offset 4 and the selection is re-read",
|
||||
"rangesAfter": 1,
|
||||
"degenerate": true
|
||||
},
|
||||
"insert": {
|
||||
"method": "TextPattern is read-only by contract; insertion is done through ValuePattern.SetValue and verified through the TextPattern read above",
|
||||
"verified": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": "notepad-edit",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"lookup": "FindFirst(Descendants, ControlType=Document)",
|
||||
"resolved": false
|
||||
},
|
||||
{
|
||||
"target": "notepad-edit",
|
||||
"stack": "managed-System.Windows.Automation",
|
||||
"lookup": "AutomationElement.FromHandle(the Edit child HWND found with FindWindowEx)",
|
||||
"resolved": true,
|
||||
"controlType": "Pane",
|
||||
"className": "Edit",
|
||||
"supportedPatterns": [
|
||||
|
||||
],
|
||||
"tryGetCurrentPattern_Text": false,
|
||||
"tryGetCurrentPattern_Value": false,
|
||||
"tryGetCurrentPattern_Scroll": false
|
||||
}
|
||||
]
|
||||
}
|
||||
197
probes/windows/captures/06-input-synthesis/keyboard.json
Normal file
197
probes/windows/captures/06-input-synthesis/keyboard.json
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
{
|
||||
"probe": "06-input-synthesis",
|
||||
"question": "does SendInput deliver non-BMP text intact through the UTF-16 chunking the API forces, and does a modifier chord register",
|
||||
"stack": "win32-SendInput",
|
||||
"scope": "system",
|
||||
"why": "2.8\u0027s type_text has to decide how to chunk a string into KEYEVENTF_UNICODE events. A surrogate pair cannot be sent as one event, so the chunk boundary is forced by the API; whether the target reassembles it is the measurement.",
|
||||
"foregroundAcquisitionMethod": "ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) on the probe-owned scratch window. SetForegroundWindow is never called.",
|
||||
"foregroundAcquired": true,
|
||||
"injectionGate": "every SendInput call below is bracketed by Assert-Foreground before and after; the first mismatch stops all further injection and is recorded in interference",
|
||||
"interference": null,
|
||||
"typingRows": [
|
||||
{
|
||||
"payloadKind": "ascii",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
|
||||
"clearedBeforeTyping": true,
|
||||
"utf16UnitsSent": 14,
|
||||
"expectedShape": {
|
||||
"utf16Units": 14,
|
||||
"codepoints": 14,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "8979f954f551343fafd76c88772a84e9b12a12b8f56aca0796fb98f0331bf603"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 14,
|
||||
"codepoints": 14,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "8979f954f551343fafd76c88772a84e9b12a12b8f56aca0796fb98f0331bf603"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"surrogatePairSurvived": true,
|
||||
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"payloadKind": "cjk",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
|
||||
"clearedBeforeTyping": true,
|
||||
"utf16UnitsSent": 3,
|
||||
"expectedShape": {
|
||||
"utf16Units": 3,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 3,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"surrogatePairSurvived": true,
|
||||
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"payloadKind": "astral-plane",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
|
||||
"clearedBeforeTyping": true,
|
||||
"utf16UnitsSent": 4,
|
||||
"expectedShape": {
|
||||
"utf16Units": 4,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 4,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"surrogatePairSurvived": true,
|
||||
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"payloadKind": "mixed",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
|
||||
"clearedBeforeTyping": true,
|
||||
"utf16UnitsSent": 5,
|
||||
"expectedShape": {
|
||||
"utf16Units": 5,
|
||||
"codepoints": 4,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "466b637a36902077d734c147874d2a78a8449ad23e25e0fcc9322573276f30ec"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 5,
|
||||
"codepoints": 4,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "466b637a36902077d734c147874d2a78a8449ad23e25e0fcc9322573276f30ec"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"surrogatePairSurvived": true,
|
||||
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
|
||||
"verdict": "ok"
|
||||
}
|
||||
],
|
||||
"chord": {
|
||||
"chordSent": true,
|
||||
"keys": "Ctrl down, A down, A up, C down, C up, Ctrl up - six discrete SendInput calls, each individually foreground-gated",
|
||||
"sourceTypedShape": {
|
||||
"utf16Units": 16,
|
||||
"codepoints": 16,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "ac4b5a78fbe066f9b012bb77a4cdc6699889391dbccff4c5b58bdb5d3ba6ad55"
|
||||
},
|
||||
"sourceObservedShape": {
|
||||
"utf16Units": 16,
|
||||
"codepoints": 16,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "ac4b5a78fbe066f9b012bb77a4cdc6699889391dbccff4c5b58bdb5d3ba6ad55"
|
||||
},
|
||||
"sourceRoundTrip": true,
|
||||
"clipboardReadOk": true,
|
||||
"clipboardObservedShape": {
|
||||
"utf16Units": 16,
|
||||
"codepoints": 16,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "ac4b5a78fbe066f9b012bb77a4cdc6699889391dbccff4c5b58bdb5d3ba6ad55"
|
||||
},
|
||||
"verifiedByShape": true,
|
||||
"verificationRule": "the clipboard is verified by SHAPE against the hash of the string the probe itself typed into the control, never against the observed clipboard value. The operator clipboard could hold a secret; comparing hashes means a non-probe value simply fails to match and nothing about it is recorded beyond its length and hash.",
|
||||
"verdict": "ok - Ctrl+A then Ctrl+C put exactly the typed string on the clipboard"
|
||||
},
|
||||
"modifierStateAfterChord": [
|
||||
{
|
||||
"key": "VK_SHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_CONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_MENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LWIN",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RWIN",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LSHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RSHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LCONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RCONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LMENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RMENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
}
|
||||
],
|
||||
"modifierStateAfterChordVerdict": "no modifier left down by the chord"
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
{
|
||||
"probe": "06-input-synthesis",
|
||||
"question": "does SendInput deliver non-BMP text intact through the UTF-16 chunking the API forces, and does a modifier chord register",
|
||||
"stack": "win32-SendInput",
|
||||
"scope": "system",
|
||||
"why": "2.8\u0027s type_text has to decide how to chunk a string into KEYEVENTF_UNICODE events. A surrogate pair cannot be sent as one event, so the chunk boundary is forced by the API; whether the target reassembles it is the measurement.",
|
||||
"foregroundAcquisitionMethod": "ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) on the probe-owned scratch window. SetForegroundWindow is never called.",
|
||||
"foregroundAcquired": true,
|
||||
"injectionGate": "every SendInput call below is bracketed by Assert-Foreground before and after; the first mismatch stops all further injection and is recorded in interference",
|
||||
"interference": null,
|
||||
"typingRows": [
|
||||
{
|
||||
"payloadKind": "ascii",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
|
||||
"clearedBeforeTyping": true,
|
||||
"utf16UnitsSent": 14,
|
||||
"expectedShape": {
|
||||
"utf16Units": 14,
|
||||
"codepoints": 14,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "8979f954f551343fafd76c88772a84e9b12a12b8f56aca0796fb98f0331bf603"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 14,
|
||||
"codepoints": 14,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "8979f954f551343fafd76c88772a84e9b12a12b8f56aca0796fb98f0331bf603"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"surrogatePairSurvived": true,
|
||||
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"payloadKind": "cjk",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
|
||||
"clearedBeforeTyping": true,
|
||||
"utf16UnitsSent": 3,
|
||||
"expectedShape": {
|
||||
"utf16Units": 3,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 3,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "6181046d83c65ba9b401f6f67eec1316201493cb8c6c7faad5fb68009b3c7c33"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"surrogatePairSurvived": true,
|
||||
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"payloadKind": "astral-plane",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
|
||||
"clearedBeforeTyping": true,
|
||||
"utf16UnitsSent": 4,
|
||||
"expectedShape": {
|
||||
"utf16Units": 4,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 4,
|
||||
"codepoints": 3,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "67c86e7a35b2aa45d8169108221af0cce098b6f81ea3da0c3df836caca0c743b"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"surrogatePairSurvived": true,
|
||||
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
|
||||
"verdict": "ok"
|
||||
},
|
||||
{
|
||||
"payloadKind": "mixed",
|
||||
"payloadOrigin": "built with [char]::ConvertFromUtf32 (R12), then sent one UTF-16 code unit per KEYEVENTF_UNICODE key-down/key-up pair - a surrogate pair is therefore two separate SendInput chunks",
|
||||
"clearedBeforeTyping": true,
|
||||
"utf16UnitsSent": 5,
|
||||
"expectedShape": {
|
||||
"utf16Units": 5,
|
||||
"codepoints": 4,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "466b637a36902077d734c147874d2a78a8449ad23e25e0fcc9322573276f30ec"
|
||||
},
|
||||
"observedShape": {
|
||||
"utf16Units": 5,
|
||||
"codepoints": 4,
|
||||
"surrogatePairs": 1,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "466b637a36902077d734c147874d2a78a8449ad23e25e0fcc9322573276f30ec"
|
||||
},
|
||||
"exactRoundTrip": true,
|
||||
"surrogatePairSurvived": true,
|
||||
"reReadMethod": "WM_GETTEXT against the control HWND resolved with GetDlgItem - a Win32 read, independent of both the injection path and of UIA",
|
||||
"verdict": "ok"
|
||||
}
|
||||
],
|
||||
"chord": {
|
||||
"chordSent": true,
|
||||
"keys": "Ctrl down, A down, A up, C down, C up, Ctrl up - six discrete SendInput calls, each individually foreground-gated",
|
||||
"sourceTypedShape": {
|
||||
"utf16Units": 16,
|
||||
"codepoints": 16,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "ac4b5a78fbe066f9b012bb77a4cdc6699889391dbccff4c5b58bdb5d3ba6ad55"
|
||||
},
|
||||
"sourceObservedShape": {
|
||||
"utf16Units": 16,
|
||||
"codepoints": 16,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "ac4b5a78fbe066f9b012bb77a4cdc6699889391dbccff4c5b58bdb5d3ba6ad55"
|
||||
},
|
||||
"sourceRoundTrip": true,
|
||||
"clipboardReadOk": true,
|
||||
"clipboardObservedShape": {
|
||||
"utf16Units": 16,
|
||||
"codepoints": 16,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "ac4b5a78fbe066f9b012bb77a4cdc6699889391dbccff4c5b58bdb5d3ba6ad55"
|
||||
},
|
||||
"verifiedByShape": true,
|
||||
"verificationRule": "the clipboard is verified by SHAPE against the hash of the string the probe itself typed into the control, never against the observed clipboard value. The operator clipboard could hold a secret; comparing hashes means a non-probe value simply fails to match and nothing about it is recorded beyond its length and hash.",
|
||||
"verdict": "ok - Ctrl+A then Ctrl+C put exactly the typed string on the clipboard"
|
||||
},
|
||||
"modifierStateAfterChord": [
|
||||
{
|
||||
"key": "VK_SHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_CONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_MENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LWIN",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RWIN",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LSHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RSHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LCONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RCONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LMENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RMENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
}
|
||||
],
|
||||
"modifierStateAfterChordVerdict": "no modifier left down by the chord"
|
||||
}
|
||||
70
probes/windows/captures/06-input-synthesis/mouse.json
Normal file
70
probes/windows/captures/06-input-synthesis/mouse.json
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"probe": "06-input-synthesis",
|
||||
"question": "do SendInput absolute moves land where asked, and do click, wheel and drag register as real input on the target",
|
||||
"stack": "win32-SendInput",
|
||||
"scope": "system",
|
||||
"coordinateModel": "MOUSEEVENTF_ABSOLUTE normalises against GetSystemMetrics(SM_CXSCREEN/SM_CYSCREEN), i.e. the PRIMARY monitor only. This box has one display; a multi-monitor adapter must use MOUSEEVENTF_VIRTUALDESK instead, and the single-display limit here is why that is a DEFERRED row rather than a measured one.",
|
||||
"interference": null,
|
||||
"click": {
|
||||
"targetControl": "btnAction (control id 1005)",
|
||||
"targetRect": "340,235,104,26",
|
||||
"requestedPoint": "392,248",
|
||||
"cursorLandedPoint": "392,248",
|
||||
"absoluteMoveExact": true,
|
||||
"absoluteMoveNote": "the landing point is read back with GetCursorPos. The 0..65535 normalisation is lossy at odd screen widths, so an off-by-one landing is expected and is recorded rather than asserted away.",
|
||||
"statusBefore": "status:ready",
|
||||
"statusAfter": "action:1",
|
||||
"registered": true,
|
||||
"reReadMethod": "WM_GETTEXT on lblStatus (control id 1020), a different control than the one clicked"
|
||||
},
|
||||
"wheel": {
|
||||
"targetControl": "pnlScroll (control id 1011)",
|
||||
"targetRect": "124,363,340,180",
|
||||
"ticks": 3,
|
||||
"deltaPerTick": -120,
|
||||
"sinkBefore": "scroll:0",
|
||||
"sinkAfter": "scroll:140",
|
||||
"scrollPixelsBefore": 0,
|
||||
"scrollPixelsAfter": 140,
|
||||
"deltaScrollPixels": 140,
|
||||
"registered": true,
|
||||
"patternCounterpart": "the pattern-scroll arm is in captures/05-interactions/interactions.json. It could NOT be run on this control: the managed client cannot acquire ScrollPattern on pnlScroll in either fixture mode even though the UIA3 COM census records Scroll on that exact provider. The wheel path drives the control the pattern path could not, and both are observed through the same lblScrollPos sink.",
|
||||
"measuredFactNaming": "the observed delta is named deltaScrollPixels, not \"y\" or \"height\", so the KTD9 normalizer - which buckets any key named x/y/left/top/right/bottom/width/height to 8 px - cannot canonicalize the measurement away."
|
||||
},
|
||||
"drag": {
|
||||
"targetControl": "tbSlider (control id 1008), Minimum 0 Maximum 100",
|
||||
"targetRect": "124,307,320,45",
|
||||
"method": "mouse-down on the thumb at the left end, six absolute moves rightwards, mouse-up; the fixture label lblSliderValue is read after every step",
|
||||
"sliderValueBefore": 0,
|
||||
"sliderValueAfter": 90,
|
||||
"samples": [
|
||||
{
|
||||
"step": 1,
|
||||
"sliderValue": 15
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"sliderValue": 30
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"sliderValue": 45
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"sliderValue": 60
|
||||
},
|
||||
{
|
||||
"step": 5,
|
||||
"sliderValue": 75
|
||||
},
|
||||
{
|
||||
"step": 6,
|
||||
"sliderValue": 90
|
||||
}
|
||||
],
|
||||
"monotonicNonDecreasing": true,
|
||||
"increased": true,
|
||||
"verdict": "ok - monotonic increase under drag"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"probe": "06-input-synthesis",
|
||||
"question": "do SendInput absolute moves land where asked, and do click, wheel and drag register as real input on the target",
|
||||
"stack": "win32-SendInput",
|
||||
"scope": "system",
|
||||
"coordinateModel": "MOUSEEVENTF_ABSOLUTE normalises against GetSystemMetrics(SM_CXSCREEN/SM_CYSCREEN), i.e. the PRIMARY monitor only. This box has one display; a multi-monitor adapter must use MOUSEEVENTF_VIRTUALDESK instead, and the single-display limit here is why that is a DEFERRED row rather than a measured one.",
|
||||
"interference": null,
|
||||
"click": {
|
||||
"targetControl": "btnAction (control id 1005)",
|
||||
"targetRect": "340,235,104,26",
|
||||
"requestedPoint": "392,248",
|
||||
"cursorLandedPoint": "392,248",
|
||||
"absoluteMoveExact": true,
|
||||
"absoluteMoveNote": "the landing point is read back with GetCursorPos. The 0..65535 normalisation is lossy at odd screen widths, so an off-by-one landing is expected and is recorded rather than asserted away.",
|
||||
"statusBefore": "status:ready",
|
||||
"statusAfter": "action:1",
|
||||
"registered": true,
|
||||
"reReadMethod": "WM_GETTEXT on lblStatus (control id 1020), a different control than the one clicked"
|
||||
},
|
||||
"wheel": {
|
||||
"targetControl": "pnlScroll (control id 1011)",
|
||||
"targetRect": "124,363,340,180",
|
||||
"ticks": <duration>,
|
||||
"deltaPerTick": -120,
|
||||
"sinkBefore": "scroll:0",
|
||||
"sinkAfter": "scroll:140",
|
||||
"scrollPixelsBefore": 0,
|
||||
"scrollPixelsAfter": 140,
|
||||
"deltaScrollPixels": 140,
|
||||
"registered": true,
|
||||
"patternCounterpart": "the pattern-scroll arm is in captures/05-interactions/interactions.json. It could NOT be run on this control: the managed client cannot acquire ScrollPattern on pnlScroll in either fixture mode even though the UIA3 COM census records Scroll on that exact provider. The wheel path drives the control the pattern path could not, and both are observed through the same lblScrollPos sink.",
|
||||
"measuredFactNaming": "the observed delta is named deltaScrollPixels, not \"y\" or \"height\", so the KTD9 normalizer - which buckets any key named x/y/left/top/right/bottom/width/height to 8 px - cannot canonicalize the measurement away."
|
||||
},
|
||||
"drag": {
|
||||
"targetControl": "tbSlider (control id 1008), Minimum 0 Maximum 100",
|
||||
"targetRect": "124,307,320,45",
|
||||
"method": "mouse-down on the thumb at the left end, six absolute moves rightwards, mouse-up; the fixture label lblSliderValue is read after every step",
|
||||
"sliderValueBefore": 0,
|
||||
"sliderValueAfter": 90,
|
||||
"samples": [
|
||||
{
|
||||
"step": 1,
|
||||
"sliderValue": 15
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"sliderValue": 30
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"sliderValue": 45
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"sliderValue": 60
|
||||
},
|
||||
{
|
||||
"step": 5,
|
||||
"sliderValue": 75
|
||||
},
|
||||
{
|
||||
"step": 6,
|
||||
"sliderValue": 90
|
||||
}
|
||||
],
|
||||
"monotonicNonDecreasing": true,
|
||||
"increased": true,
|
||||
"verdict": "ok - monotonic increase under drag"
|
||||
}
|
||||
}
|
||||
77
probes/windows/captures/06-input-synthesis/postmessage.json
Normal file
77
probes/windows/captures/06-input-synthesis/postmessage.json
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"probe": "06-input-synthesis",
|
||||
"question": "does posting WM_KEYDOWN/WM_KEYUP to a control actually register the keystroke - on a classic Win32 control and on Chromium",
|
||||
"stack": "win32-PostMessage",
|
||||
"scope": "system",
|
||||
"why": "Engineering Invariant #5 asserts the message-posting path is dead for Chromium and UWP. Nothing else in this corpus tests it, so the invariant has been carried on assumption. These rows file it from observation.",
|
||||
"foregroundIrrelevantNote": "PostMessage is targeted at a window, not at the desktop input queue, so these rows are deliberately NOT foreground-gated. That is the whole appeal of the path and the reason it is worth measuring rather than assuming.",
|
||||
"rows": [
|
||||
{
|
||||
"target": "scratch WinForms Edit (control id 1003), same session, same integrity level as the probe",
|
||||
"stack": "win32-PostMessage",
|
||||
"integrityRelation": "High -\u003e High. 09-elevation-uipi measured the Medium -\u003e High case for PostMessage(WM_CHAR) and got false with error 5 (ERROR_ACCESS_DENIED). This row is the different question: with UIPI out of the way entirely, does a posted key message register at all?",
|
||||
"keyDownPostResult": "true",
|
||||
"keyUpPostResult": "true",
|
||||
"charPostResult": "true",
|
||||
"resultFormat": "\"true\" on success, \"false:\u003cGetLastError\u003e\" on failure. The error code is only meaningful when the call failed, so it is only recorded then.",
|
||||
"textBeforeShape": {
|
||||
"utf16Units": 0,
|
||||
"codepoints": 0,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"textAfterKeyDownShape": {
|
||||
"utf16Units": 1,
|
||||
"codepoints": 1,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "c03ad57f5b1b1bddf59ddb5c973b459f432291edad374fe9d051c59d102d8742"
|
||||
},
|
||||
"textAfterKeyUpShape": {
|
||||
"utf16Units": 1,
|
||||
"codepoints": 1,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "c03ad57f5b1b1bddf59ddb5c973b459f432291edad374fe9d051c59d102d8742"
|
||||
},
|
||||
"textAfterCharShape": {
|
||||
"utf16Units": 2,
|
||||
"codepoints": 2,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "219173f2f738427068d89f63e4ebe7b9d440ec37c08234356c276d6eafc77eca"
|
||||
},
|
||||
"keyDownRegistered": true,
|
||||
"keyUpAddedAnything": false,
|
||||
"charMessageRegistered": true,
|
||||
"measuredResult": "the posted WM_KEYDOWN alone inserted one character into the Edit. WM_KEYUP added nothing, and a subsequently posted WM_CHAR inserted a second character.",
|
||||
"interpretation": "this contradicts the usual reading that a posted WM_KEYDOWN is inert because it carries a virtual key rather than a character. TranslateMessage runs inside the TARGET thread\u0027s own message pump and does not care whether the message it retrieved was posted or came from the input queue, so it synthesises the WM_CHAR itself. The message-posting path is therefore alive - not dead - for a classic Win32 Edit at equal integrity. What it cannot do is carry modifier state: TranslateMessage reads the target thread\u0027s keyboard state, which the poster cannot set, so a posted chord or a shifted character is unreachable by this path even where a plain character is not.",
|
||||
"invariantRelevance": "Engineering Invariant #5 is about Chromium and UWP, and this row does not weaken it. It does remove the convenient generalisation that message posting never types anything: on classic Win32 controls it does, which is exactly why the Chromium row below has to be measured rather than inferred from the same mechanism."
|
||||
},
|
||||
{
|
||||
"target": "chromium/electron (Obsidian, launched by this probe)",
|
||||
"stack": "win32-PostMessage",
|
||||
"obsidianVersion": "1.12.7",
|
||||
"topLevelClassName": "Chrome_WidgetWin_1",
|
||||
"childWindowClasses": "Chrome_RenderWidgetHostHWND",
|
||||
"renderWidgetChildFound": true,
|
||||
"topLevelKeyDownPostResult": "true",
|
||||
"topLevelKeyUpPostResult": "true",
|
||||
"renderWidgetKeyDownPostResult": "true",
|
||||
"renderWidgetKeyUpPostResult": "true",
|
||||
"renderWidgetCharPostResult": "true",
|
||||
"observable": "a bounded RawView UIA walk of the Chromium top-level window (250 nodes, depth 10) reduced to a SHA-256 of the ControlType|Name stream. Only the derived booleans and node counts are recorded - neither the hash nor any Chromium Name value reaches the capture (R11), and the hash would in any case be run-varying and break the KTD9 twin.",
|
||||
"controlPass": "the fingerprint is first polled every 2 s until two consecutive reads agree, so the measurement starts from a quiet tree rather than a loading one. A further idle fingerprint is then taken five seconds later with NO input at all, and only then are the messages posted. Without that control an Electron tree still growing on its own reads as a registered keystroke - which is exactly what the first run of this probe recorded before the control was added: 13 nodes to 15 with nothing posted.",
|
||||
"timingStabilizationPasses": 2,
|
||||
"nodesAtSettle": 15,
|
||||
"nodesAfterIdleWait": 15,
|
||||
"nodesAfterPostedKeys": 15,
|
||||
"treeChurnWithoutInput": false,
|
||||
"treeChangedAfterPostedKeys": false,
|
||||
"keystrokeRegistered": false,
|
||||
"verdict": "not registered - every PostMessage call returned success and the Chromium tree was byte-identical before and after, over the same window and the same bounded walk",
|
||||
"caveat": "an unchanged fingerprint is evidence of no observable effect within the bounded walk, not a proof that no byte anywhere changed. It is recorded as such."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"probe": "06-input-synthesis",
|
||||
"question": "does posting WM_KEYDOWN/WM_KEYUP to a control actually register the keystroke - on a classic Win32 control and on Chromium",
|
||||
"stack": "win32-PostMessage",
|
||||
"scope": "system",
|
||||
"why": "Engineering Invariant #5 asserts the message-posting path is dead for Chromium and UWP. Nothing else in this corpus tests it, so the invariant has been carried on assumption. These rows file it from observation.",
|
||||
"foregroundIrrelevantNote": "PostMessage is targeted at a window, not at the desktop input queue, so these rows are deliberately NOT foreground-gated. That is the whole appeal of the path and the reason it is worth measuring rather than assuming.",
|
||||
"rows": [
|
||||
{
|
||||
"target": "scratch WinForms Edit (control id 1003), same session, same integrity level as the probe",
|
||||
"stack": "win32-PostMessage",
|
||||
"integrityRelation": "High -\u003e High. 09-elevation-uipi measured the Medium -\u003e High case for PostMessage(WM_CHAR) and got false with error 5 (ERROR_ACCESS_DENIED). This row is the different question: with UIPI out of the way entirely, does a posted key message register at all?",
|
||||
"keyDownPostResult": "true",
|
||||
"keyUpPostResult": "true",
|
||||
"charPostResult": "true",
|
||||
"resultFormat": "\"true\" on success, \"false:\u003cGetLastError\u003e\" on failure. The error code is only meaningful when the call failed, so it is only recorded then.",
|
||||
"textBeforeShape": {
|
||||
"utf16Units": 0,
|
||||
"codepoints": 0,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"textAfterKeyDownShape": {
|
||||
"utf16Units": 1,
|
||||
"codepoints": 1,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "c03ad57f5b1b1bddf59ddb5c973b459f432291edad374fe9d051c59d102d8742"
|
||||
},
|
||||
"textAfterKeyUpShape": {
|
||||
"utf16Units": 1,
|
||||
"codepoints": 1,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "c03ad57f5b1b1bddf59ddb5c973b459f432291edad374fe9d051c59d102d8742"
|
||||
},
|
||||
"textAfterCharShape": {
|
||||
"utf16Units": 2,
|
||||
"codepoints": 2,
|
||||
"surrogatePairs": 0,
|
||||
"replacementChars": 0,
|
||||
"sha256Utf16": "219173f2f738427068d89f63e4ebe7b9d440ec37c08234356c276d6eafc77eca"
|
||||
},
|
||||
"keyDownRegistered": true,
|
||||
"keyUpAddedAnything": false,
|
||||
"charMessageRegistered": true,
|
||||
"measuredResult": "the posted WM_KEYDOWN alone inserted one character into the Edit. WM_KEYUP added nothing, and a subsequently posted WM_CHAR inserted a second character.",
|
||||
"interpretation": "this contradicts the usual reading that a posted WM_KEYDOWN is inert because it carries a virtual key rather than a character. TranslateMessage runs inside the TARGET thread\u0027s own message pump and does not care whether the message it retrieved was posted or came from the input queue, so it synthesises the WM_CHAR itself. The message-posting path is therefore alive - not dead - for a classic Win32 Edit at equal integrity. What it cannot do is carry modifier state: TranslateMessage reads the target thread\u0027s keyboard state, which the poster cannot set, so a posted chord or a shifted character is unreachable by this path even where a plain character is not.",
|
||||
"invariantRelevance": "Engineering Invariant #5 is about Chromium and UWP, and this row does not weaken it. It does remove the convenient generalisation that message posting never types anything: on classic Win32 controls it does, which is exactly why the Chromium row below has to be measured rather than inferred from the same mechanism."
|
||||
},
|
||||
{
|
||||
"target": "chromium/electron (Obsidian, launched by this probe)",
|
||||
"stack": "win32-PostMessage",
|
||||
"obsidianVersion": "1.12.7",
|
||||
"topLevelClassName": "Chrome_WidgetWin_1",
|
||||
"childWindowClasses": "Chrome_RenderWidgetHostHWND",
|
||||
"renderWidgetChildFound": true,
|
||||
"topLevelKeyDownPostResult": "true",
|
||||
"topLevelKeyUpPostResult": "true",
|
||||
"renderWidgetKeyDownPostResult": "true",
|
||||
"renderWidgetKeyUpPostResult": "true",
|
||||
"renderWidgetCharPostResult": "true",
|
||||
"observable": "a bounded RawView UIA walk of the Chromium top-level window (250 nodes, depth 10) reduced to a SHA-256 of the ControlType|Name stream. Only the derived booleans and node counts are recorded - neither the hash nor any Chromium Name value reaches the capture (R11), and the hash would in any case be run-varying and break the KTD9 twin.",
|
||||
"controlPass": "the fingerprint is first polled every 2 s until two consecutive reads agree, so the measurement starts from a quiet tree rather than a loading one. A further idle fingerprint is then taken five seconds later with NO input at all, and only then are the messages posted. Without that control an Electron tree still growing on its own reads as a registered keystroke - which is exactly what the first run of this probe recorded before the control was added: 13 nodes to 15 with nothing posted.",
|
||||
"timingStabilizationPasses": <duration>,
|
||||
"nodesAtSettle": 15,
|
||||
"nodesAfterIdleWait": 15,
|
||||
"nodesAfterPostedKeys": 15,
|
||||
"treeChurnWithoutInput": false,
|
||||
"treeChangedAfterPostedKeys": false,
|
||||
"keystrokeRegistered": false,
|
||||
"verdict": "not registered - every PostMessage call returned success and the Chromium tree was byte-identical before and after, over the same window and the same bounded walk",
|
||||
"caveat": "an unchanged fingerprint is evidence of no observable effect within the bounded walk, not a proof that no byte anywhere changed. It is recorded as such."
|
||||
}
|
||||
]
|
||||
}
|
||||
148
probes/windows/captures/06-input-synthesis/teardown.json
Normal file
148
probes/windows/captures/06-input-synthesis/teardown.json
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
{
|
||||
"probe": "06-input-synthesis",
|
||||
"question": "does the probe leave the desktop exactly as it found it - no stuck modifier, the original clipboard, the original cursor position, no surviving process",
|
||||
"why": "KTD5. A probe that types into a shared desktop and leaves a modifier down or a clipboard overwritten has corrupted the box for everything that runs after it, including the rest of this corpus.",
|
||||
"modifierSweep": {
|
||||
"policy": "conditional by design: modifier state is read first and a key-up is injected only for a modifier actually found down. On the normal path no key is injected at all, so the sweep does not need the foreground gate it would otherwise require.",
|
||||
"stateBeforeSweep": [
|
||||
{
|
||||
"key": "VK_SHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_CONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_MENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LWIN",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RWIN",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LSHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RSHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LCONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RCONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LMENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RMENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
}
|
||||
],
|
||||
"keysSwept": [
|
||||
|
||||
],
|
||||
"stateAfterSweep": [
|
||||
{
|
||||
"key": "VK_SHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_CONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_MENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LWIN",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RWIN",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LSHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RSHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LCONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RCONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LMENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RMENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
}
|
||||
],
|
||||
"allClear": true
|
||||
},
|
||||
"clipboard": {
|
||||
"snapshotTaken": true,
|
||||
"originalFormats": [
|
||||
"text"
|
||||
],
|
||||
"originalTextPresent": true,
|
||||
"restoredExactly": true,
|
||||
"verificationMethod": "the restored clipboard is read back and compared to the snapshot by SHA-256 of its UTF-16 bytes, in memory. Neither the value nor its hash reaches the capture: the operator clipboard can hold a secret, and a hash of a secret is still a fingerprint of it.",
|
||||
"formatCaveat": "only text is snapshotted and restored. originalFormats records what was actually on the clipboard so a non-text loss would be visible in the capture rather than silent."
|
||||
},
|
||||
"cursor": {
|
||||
"snapshotTaken": true,
|
||||
"offsetAfterRestore": "0,0",
|
||||
"restoredExactly": true,
|
||||
"recordingChoice": "the offset from the snapshot is recorded rather than the absolute positions: an absolute origin is a run-varying value that would break the KTD9 twin, while \"0,0\" proves exact restoration. It is a comma-joined string rather than numeric x/y keys because the normalizer buckets any numeric key named x or y to 8 px, which would have hidden an 8 px restoration error."
|
||||
},
|
||||
"processes": {
|
||||
"spawnedCount": 5,
|
||||
"survivorCount": 0,
|
||||
"confirmedGone": true,
|
||||
"confirmationMethod": "Stop-ScratchProcess terminates and then re-reads the process list until the pid is gone or a 10 s deadline expires; the survivor count above is a second, independent re-read after all teardown."
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
{
|
||||
"probe": "06-input-synthesis",
|
||||
"question": "does the probe leave the desktop exactly as it found it - no stuck modifier, the original clipboard, the original cursor position, no surviving process",
|
||||
"why": "KTD5. A probe that types into a shared desktop and leaves a modifier down or a clipboard overwritten has corrupted the box for everything that runs after it, including the rest of this corpus.",
|
||||
"modifierSweep": {
|
||||
"policy": "conditional by design: modifier state is read first and a key-up is injected only for a modifier actually found down. On the normal path no key is injected at all, so the sweep does not need the foreground gate it would otherwise require.",
|
||||
"stateBeforeSweep": [
|
||||
{
|
||||
"key": "VK_SHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_CONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_MENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LWIN",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RWIN",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LSHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RSHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LCONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RCONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LMENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RMENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
}
|
||||
],
|
||||
"keysSwept": [
|
||||
|
||||
],
|
||||
"stateAfterSweep": [
|
||||
{
|
||||
"key": "VK_SHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_CONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_MENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LWIN",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RWIN",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LSHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RSHIFT",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LCONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RCONTROL",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_LMENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
},
|
||||
{
|
||||
"key": "VK_RMENU",
|
||||
"asyncKeyIsDown": false,
|
||||
"keyStateIsDown": false
|
||||
}
|
||||
],
|
||||
"allClear": true
|
||||
},
|
||||
"clipboard": {
|
||||
"snapshotTaken": true,
|
||||
"originalFormats": [
|
||||
"text"
|
||||
],
|
||||
"originalTextPresent": true,
|
||||
"restoredExactly": true,
|
||||
"verificationMethod": "the restored clipboard is read back and compared to the snapshot by SHA-256 of its UTF-16 bytes, in memory. Neither the value nor its hash reaches the capture: the operator clipboard can hold a secret, and a hash of a secret is still a fingerprint of it.",
|
||||
"formatCaveat": "only text is snapshotted and restored. originalFormats records what was actually on the clipboard so a non-text loss would be visible in the capture rather than silent."
|
||||
},
|
||||
"cursor": {
|
||||
"snapshotTaken": true,
|
||||
"offsetAfterRestore": "0,0",
|
||||
"restoredExactly": true,
|
||||
"recordingChoice": "the offset from the snapshot is recorded rather than the absolute positions: an absolute origin is a run-varying value that would break the KTD9 twin, while \"0,0\" proves exact restoration. It is a comma-joined string rather than numeric x/y keys because the normalizer buckets any numeric key named x or y to 8 px, which would have hidden an 8 px restoration error."
|
||||
},
|
||||
"processes": {
|
||||
"spawnedCount": 5,
|
||||
"survivorCount": 0,
|
||||
"confirmedGone": true,
|
||||
"confirmationMethod": "Stop-ScratchProcess terminates and then re-reads the process list until the pid is gone or a 10 s deadline expires; the survivor count above is a second, independent re-read after all teardown."
|
||||
}
|
||||
}
|
||||
105
probes/windows/captures/07-hittest/hittest.json
Normal file
105
probes/windows/captures/07-hittest/hittest.json
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"probe": "07-hittest",
|
||||
"question": "do the hit-test primitives 2.6 needs behave correctly against occluded, zero-size and minimized targets",
|
||||
"stack": "managed-System.Windows.Automation AutomationElement.FromPoint, cross-checked with Win32 WindowFromPoint",
|
||||
"scope": "system",
|
||||
"safety": "both windows are launched by this probe at deterministic --pos origins; nothing else on the desktop is touched, moved or raised.",
|
||||
"occlusion": {
|
||||
"question": "at a point covered by a second window, does ElementFromPoint return the occluder or the covered target",
|
||||
"why": "2.6\u0027s occlusion gate decides whether an element is actually clickable. If ElementFromPoint answered with the covered element the gate would have no primitive to build on and every actionability check would be a guess.",
|
||||
"underWindowTitle": "AgentDesktop Scratch WinForms [u6-under]",
|
||||
"overWindowTitle": "AgentDesktop Scratch WinForms [u6-over]",
|
||||
"underWindowRect": "100,100,776,559",
|
||||
"overWindowRect": "260,180,776,559",
|
||||
"raiseMethod": "ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) on the probe-owned over window. SetForegroundWindow is never called (KTD5).",
|
||||
"overlapRect": "260,180,616,479",
|
||||
"probePoint": "568,420",
|
||||
"probePointInsideUnder": true,
|
||||
"probePointInsideOver": true,
|
||||
"elementFromPoint": {
|
||||
"resolved": true,
|
||||
"controlType": "Pane",
|
||||
"automationId": "tbSlider",
|
||||
"className": "WindowsForms10.msctls_trackbar32.app.0.34f5582_r8_ad1",
|
||||
"topLevelWindow": "AgentDesktop Scratch WinForms [u6-over]",
|
||||
"rect": "284,387,320,45"
|
||||
},
|
||||
"win32WindowFromPointRootTitle": "AgentDesktop Scratch WinForms [u6-over]",
|
||||
"resolvedToOccluder": true,
|
||||
"resolvedToOccluded": false,
|
||||
"win32AgreesWithUia": true,
|
||||
"titleEvidenceNote": "the two windows are distinguished by the --tag switch, so the identity that proves which window answered survives KTD9 normalization - process ids and window handles do not.",
|
||||
"verdict": "ok - ElementFromPoint returned the occluder"
|
||||
},
|
||||
"zeroSize": {
|
||||
"question": "what does a 0x0 control look like to a hit test, and is it reachable at all",
|
||||
"controlId": 1007,
|
||||
"automationId": "btnZeroSize",
|
||||
"reachableByGetDlgItem": true,
|
||||
"win32Rect": "248,271,0,0",
|
||||
"win32ClassName": "WindowsForms10.BUTTON.app.0.34f5582_r8_ad1",
|
||||
"win32IsWindowVisible": true,
|
||||
"foundByFindFirstAutomationId": false,
|
||||
"controlViewNodeCount": 24,
|
||||
"rawViewNodeCount": 24,
|
||||
"fromHandleResolved": true,
|
||||
"fromHandleControlType": "Pane",
|
||||
"fromHandleRect": "\u003cempty\u003e",
|
||||
"fromHandleIsOffscreen": false,
|
||||
"elementFromPointAtItsOrigin": {
|
||||
"resolved": true,
|
||||
"controlType": "Window",
|
||||
"automationId": "frmScratchMain",
|
||||
"className": "WindowsForms10.Window.8.app.0.34f5582_r8_ad1",
|
||||
"topLevelWindow": "AgentDesktop Scratch WinForms [u6-under]",
|
||||
"rect": "100,100,776,559"
|
||||
},
|
||||
"priorEvidence": "U7 recorded this control as absent from BOTH the COM RawView and ControlView walks of the same fixture. The rows above are the managed-side confirmation plus the two things the COM census could not answer: whether the control is still reachable by handle, and what a hit test at its coordinates returns.",
|
||||
"consequence": "a zero-size control is addressable through Win32 and through AutomationElement.FromHandle, but it can be enumerated by no tree walk and hit by no point. Any Windows actionability check that treats \"not hit-testable\" as \"does not exist\" will be right about the click and wrong about the element."
|
||||
},
|
||||
"minimized": {
|
||||
"question": "what does a minimized window report, and what does a hit test at its former coordinates return",
|
||||
"priorEvidence": "U3 measured that minimizing degenerates geometry in two different shapes: only the TOP-LEVEL window reports an empty rect, while descendants report REAL dimensions anchored at -32000, and every node still reports IsOffscreen false. An occlusion gate testing emptiness or IsOffscreen alone accepts both as visible.",
|
||||
"pointProbed": "130,380",
|
||||
"pointChoice": "a point inside the under window but outside the over window, so the only thing that can change the answer is the minimize itself",
|
||||
"topLevelRectBefore": "100,100,776,559",
|
||||
"topLevelRectAfterMinimize": "\u003cempty\u003e",
|
||||
"topLevelRectAfterRestore": "100,100,776,559",
|
||||
"topLevelIsOffscreenBefore": false,
|
||||
"topLevelIsOffscreenAfterMinimize": false,
|
||||
"descendantAutomationId": "btnAction",
|
||||
"descendantFoundAfterMinimize": true,
|
||||
"descendantRectBefore": "340,235,104,26",
|
||||
"descendantRectAfterMinimize": "-31768,-31896,104,26",
|
||||
"descendantIsOffscreenBefore": false,
|
||||
"descendantIsOffscreenAfterMinimize": false,
|
||||
"win32IsIconic": true,
|
||||
"elementFromPointBefore": {
|
||||
"resolved": true,
|
||||
"controlType": "Pane",
|
||||
"automationId": "pnlScroll",
|
||||
"className": "WindowsForms10.Window.8.app.0.34f5582_r8_ad1",
|
||||
"topLevelWindow": "AgentDesktop Scratch WinForms [u6-under]",
|
||||
"rect": "124,363,340,180"
|
||||
},
|
||||
"elementFromPointAfterMinimize": {
|
||||
"resolved": true,
|
||||
"controlType": "Pane",
|
||||
"automationId": "1",
|
||||
"className": "SysListView32",
|
||||
"topLevelWindow": "Program Manager",
|
||||
"rect": "0,0,1639,732"
|
||||
},
|
||||
"elementFromPointAtDescendantAnchor": {
|
||||
"resolved": true,
|
||||
"controlType": "Pane",
|
||||
"automationId": "",
|
||||
"className": "#32769",
|
||||
"topLevelWindow": "\u003cno-top-level-reached\u003e",
|
||||
"rect": "0,0,1639,732"
|
||||
},
|
||||
"hitTestFreedTheCoordinates": true,
|
||||
"gateConsequence": "ElementFromPoint is the one primitive here that degrades cleanly: after the minimize the coordinates resolve to whatever is genuinely on top instead of to the minimized window. Emptiness and IsOffscreen do not - the top-level rect empties while its descendants keep real dimensions at -32000, and IsOffscreen stays false on both. 2.6 should gate on a hit test, and where it must reason about geometry it has to test the -32000 anchor explicitly, because a -32000 rect is neither empty nor offscreen by either property.",
|
||||
"rectRecordingNote": "every rect here is a comma-joined string. As numeric x/y/width/height keys the KTD9 normalizer would bucket them to 8 px, which rounds -32000 into a neighbouring bucket and erases exactly the anchor this row exists to record."
|
||||
}
|
||||
}
|
||||
105
probes/windows/captures/07-hittest/hittest.json.normalized
Normal file
105
probes/windows/captures/07-hittest/hittest.json.normalized
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"probe": "07-hittest",
|
||||
"question": "do the hit-test primitives 2.6 needs behave correctly against occluded, zero-size and minimized targets",
|
||||
"stack": "managed-System.Windows.Automation AutomationElement.FromPoint, cross-checked with Win32 WindowFromPoint",
|
||||
"scope": "system",
|
||||
"safety": "both windows are launched by this probe at deterministic --pos origins; nothing else on the desktop is touched, moved or raised.",
|
||||
"occlusion": {
|
||||
"question": "at a point covered by a second window, does ElementFromPoint return the occluder or the covered target",
|
||||
"why": "2.6\u0027s occlusion gate decides whether an element is actually clickable. If ElementFromPoint answered with the covered element the gate would have no primitive to build on and every actionability check would be a guess.",
|
||||
"underWindowTitle": "AgentDesktop Scratch WinForms [u6-under]",
|
||||
"overWindowTitle": "AgentDesktop Scratch WinForms [u6-over]",
|
||||
"underWindowRect": "100,100,776,559",
|
||||
"overWindowRect": "260,180,776,559",
|
||||
"raiseMethod": "ShowWindow(SW_MINIMIZE) then ShowWindow(SW_RESTORE) on the probe-owned over window. SetForegroundWindow is never called (KTD5).",
|
||||
"overlapRect": "260,180,616,479",
|
||||
"probePoint": "568,420",
|
||||
"probePointInsideUnder": true,
|
||||
"probePointInsideOver": true,
|
||||
"elementFromPoint": {
|
||||
"resolved": true,
|
||||
"controlType": "Pane",
|
||||
"automationId": "tbSlider",
|
||||
"className": "WindowsForms10.msctls_trackbar32.app.0.34f5582_r8_ad1",
|
||||
"topLevelWindow": "AgentDesktop Scratch WinForms [u6-over]",
|
||||
"rect": "284,387,320,45"
|
||||
},
|
||||
"win32WindowFromPointRootTitle": "AgentDesktop Scratch WinForms [u6-over]",
|
||||
"resolvedToOccluder": true,
|
||||
"resolvedToOccluded": false,
|
||||
"win32AgreesWithUia": true,
|
||||
"titleEvidenceNote": "the two windows are distinguished by the --tag switch, so the identity that proves which window answered survives KTD9 normalization - process ids and window handles do not.",
|
||||
"verdict": "ok - ElementFromPoint returned the occluder"
|
||||
},
|
||||
"zeroSize": {
|
||||
"question": "what does a 0x0 control look like to a hit test, and is it reachable at all",
|
||||
"controlId": 1007,
|
||||
"automationId": "btnZeroSize",
|
||||
"reachableByGetDlgItem": true,
|
||||
"win32Rect": "248,271,0,0",
|
||||
"win32ClassName": "WindowsForms10.BUTTON.app.0.34f5582_r8_ad1",
|
||||
"win32IsWindowVisible": true,
|
||||
"foundByFindFirstAutomationId": false,
|
||||
"controlViewNodeCount": 24,
|
||||
"rawViewNodeCount": 24,
|
||||
"fromHandleResolved": true,
|
||||
"fromHandleControlType": "Pane",
|
||||
"fromHandleRect": "\u003cempty\u003e",
|
||||
"fromHandleIsOffscreen": false,
|
||||
"elementFromPointAtItsOrigin": {
|
||||
"resolved": true,
|
||||
"controlType": "Window",
|
||||
"automationId": "frmScratchMain",
|
||||
"className": "WindowsForms10.Window.8.app.0.34f5582_r8_ad1",
|
||||
"topLevelWindow": "AgentDesktop Scratch WinForms [u6-under]",
|
||||
"rect": "100,100,776,559"
|
||||
},
|
||||
"priorEvidence": "U7 recorded this control as absent from BOTH the COM RawView and ControlView walks of the same fixture. The rows above are the managed-side confirmation plus the two things the COM census could not answer: whether the control is still reachable by handle, and what a hit test at its coordinates returns.",
|
||||
"consequence": "a zero-size control is addressable through Win32 and through AutomationElement.FromHandle, but it can be enumerated by no tree walk and hit by no point. Any Windows actionability check that treats \"not hit-testable\" as \"does not exist\" will be right about the click and wrong about the element."
|
||||
},
|
||||
"minimized": {
|
||||
"question": "what does a minimized window report, and what does a hit test at its former coordinates return",
|
||||
"priorEvidence": "U3 measured that minimizing degenerates geometry in two different shapes: only the TOP-LEVEL window reports an empty rect, while descendants report REAL dimensions anchored at -32000, and every node still reports IsOffscreen false. An occlusion gate testing emptiness or IsOffscreen alone accepts both as visible.",
|
||||
"pointProbed": "130,380",
|
||||
"pointChoice": "a point inside the under window but outside the over window, so the only thing that can change the answer is the minimize itself",
|
||||
"topLevelRectBefore": "100,100,776,559",
|
||||
"topLevelRectAfterMinimize": "\u003cempty\u003e",
|
||||
"topLevelRectAfterRestore": "100,100,776,559",
|
||||
"topLevelIsOffscreenBefore": false,
|
||||
"topLevelIsOffscreenAfterMinimize": false,
|
||||
"descendantAutomationId": "btnAction",
|
||||
"descendantFoundAfterMinimize": true,
|
||||
"descendantRectBefore": "340,235,104,26",
|
||||
"descendantRectAfterMinimize": "-31768,-31896,104,26",
|
||||
"descendantIsOffscreenBefore": false,
|
||||
"descendantIsOffscreenAfterMinimize": false,
|
||||
"win32IsIconic": true,
|
||||
"elementFromPointBefore": {
|
||||
"resolved": true,
|
||||
"controlType": "Pane",
|
||||
"automationId": "pnlScroll",
|
||||
"className": "WindowsForms10.Window.8.app.0.34f5582_r8_ad1",
|
||||
"topLevelWindow": "AgentDesktop Scratch WinForms [u6-under]",
|
||||
"rect": "124,363,340,180"
|
||||
},
|
||||
"elementFromPointAfterMinimize": {
|
||||
"resolved": true,
|
||||
"controlType": "Pane",
|
||||
"automationId": "1",
|
||||
"className": "SysListView32",
|
||||
"topLevelWindow": "Program Manager",
|
||||
"rect": "0,0,1639,732"
|
||||
},
|
||||
"elementFromPointAtDescendantAnchor": {
|
||||
"resolved": true,
|
||||
"controlType": "Pane",
|
||||
"automationId": "",
|
||||
"className": "#32769",
|
||||
"topLevelWindow": "\u003cno-top-level-reached\u003e",
|
||||
"rect": "0,0,1639,732"
|
||||
},
|
||||
"hitTestFreedTheCoordinates": true,
|
||||
"gateConsequence": "ElementFromPoint is the one primitive here that degrades cleanly: after the minimize the coordinates resolve to whatever is genuinely on top instead of to the minimized window. Emptiness and IsOffscreen do not - the top-level rect empties while its descendants keep real dimensions at -32000, and IsOffscreen stays false on both. 2.6 should gate on a hit test, and where it must reason about geometry it has to test the -32000 anchor explicitly, because a -32000 rect is neither empty nor offscreen by either property.",
|
||||
"rectRecordingNote": "every rect here is a comma-joined string. As numeric x/y/width/height keys the KTD9 normalizer would bucket them to 8 px, which rounds -32000 into a neighbouring bucket and erases exactly the anchor this row exists to record."
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue