Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion Tools/windows/Invoke-UiCaptureWorker.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,11 @@ function Invoke-CapSceneAction {
Assert-CapWorkerDeadline "after action $Action"
}

function Send-CapMatchedEscape {
param([Parameter(Mandatory)]$AppRun)
return [GraphCodeUiaGateState]::PostKeyboard($AppRun.Window,0x1B)
}

function Measure-CapSceneZoom {
param(
[Parameter(Mandatory)]$AppRun,
Expand Down Expand Up @@ -2075,11 +2080,75 @@ function Invoke-CapMatchedScenes {
$zoom=Measure-CapSceneZoom -AppRun $app -Scene $probe -NaturalWidth 250
$app.SObservation=[pscustomobject]@{readSucceeded=$true;evidenceMode='OWNED_UIA_READ_NOT_VISUAL_PARITY'
fixtureState=$state[0];card=$card[0];wireState='HASH_BOUND_STATIC_PEER_NOT_LIVE_BACKEND'}
$recovery=$null
if ($scene -cmatch '^s-goal-unwired-') {
$wireIdentity="wire-node:$($fixture.graphs[0].project.path):$($state[0].nodeId)"
$markIdentity="mark-entry:$($fixture.graphs[0].project.path):$($state[0].nodeId)"
$wireId=Get-CapMatchedAutomationId $wireIdentity
$markId=Get-CapMatchedAutomationId $markIdentity
$graph=Find-CapAutomationElement -RootHandle $app.Window -ProcessIdentity $app.Run.Identity `
-ExpectedProcessId $app.Run.Identity.Pid -Name 'Graph' -AutomationId 'graph'
$children=@(Get-CapDirectChildren -Parent $graph)
$wire=@($children | Where-Object {$_.Current.AutomationId -ceq $wireId -and
$_.Current.Name -ceq 'Wire it up'})
$mark=@($children | Where-Object {$_.Current.AutomationId -ceq $markId -and
$_.Current.Name -ceq 'Mark as entry'})
if ($wire.Count -ne 1 -or $mark.Count -ne 1) {
throw "Matched unwired Goal omitted exact inline recovery controls for $scene."
}
foreach ($control in @($wire[0],$mark[0])) {
if ($control.Current.ProcessId -ne $app.Run.Identity.Pid -or
$control.Current.BoundingRectangle.Width -le 0 -or
$control.Current.BoundingRectangle.Height -le 0) {
throw "Matched unwired Goal recovery control was not owned and visible for $scene."
}
$null=$control.GetCurrentPattern([Windows.Automation.InvokePattern]::Pattern)
}
$recovery=[pscustomobject]@{wireIdentity=$wireIdentity;wireAutomationId=$wireId
markIdentity=$markIdentity;markAutomationId=$markId}
$app.SObservation | Add-Member -NotePropertyName recoveryControls -NotePropertyValue `
'REFERENCE_ONLY: Wire it up / Mark as entry; Windows control equivalence requires C8 review'
([pscustomobject]@{wire=[pscustomobject]@{name='Wire it up';automationId=$wireId
bounds=@($wire[0].Current.BoundingRectangle.Left,$wire[0].Current.BoundingRectangle.Top,
$wire[0].Current.BoundingRectangle.Right,$wire[0].Current.BoundingRectangle.Bottom)}
markEntry=[pscustomobject]@{name='Mark as entry';automationId=$markId
bounds=@($mark[0].Current.BoundingRectangle.Left,$mark[0].Current.BoundingRectangle.Top,
$mark[0].Current.BoundingRectangle.Right,$mark[0].Current.BoundingRectangle.Bottom)}})
}
Save-CapSceneImage -AppRun $app -SceneName $scene -ZoomEvidence $zoom -Phase $Phase
if ($null -ne $recovery) {
$null=Invoke-CapSceneAction -AppRun $app -Action 'wire-node' -SubjectId $recovery.wireIdentity
$wireStatus=Find-CapAutomationElement -RootHandle $app.Window -ProcessIdentity $app.Run.Identity `
-ExpectedProcessId $app.Run.Identity.Pid -Name 'Choose a target loop to wire this entry' `
-AutomationId 'status'
if ($null -eq $wireStatus) {throw "Wire it up did not expose its armed result for $scene."}
if (-not (Send-CapMatchedEscape -AppRun $app)) {
throw "Wire it up could not be cancelled after matched evidence for $scene."
}
Start-Sleep -Milliseconds 100
$null=Invoke-CapSceneAction -AppRun $app -Action 'mark-entry' -SubjectId $recovery.markIdentity
$entryStatus=Find-CapAutomationElement -RootHandle $app.Window -ProcessIdentity $app.Run.Identity `
-ExpectedProcessId $app.Run.Identity.Pid -Name 'Marked as an entry for this session' `
-AutomationId 'status'
if ($null -eq $entryStatus) {throw "Mark as entry did not expose its result for $scene."}
$graph=Find-CapAutomationElement -RootHandle $app.Window -ProcessIdentity $app.Run.Identity `
-ExpectedProcessId $app.Run.Identity.Pid -Name 'Graph' -AutomationId 'graph'
$after=@(Get-CapDirectChildren -Parent $graph)
if (@($after | Where-Object {$_.Current.AutomationId -cin @(
$recovery.wireAutomationId,$recovery.markAutomationId)}).Count -ne 0) {
throw "Mark as entry left stale unwired controls for $scene."
}
$cardAfter=@($after | Where-Object {$_.Current.AutomationId -ceq $cardId -and
$_.Current.Name -ceq $card[0].Name})
if ($cardAfter.Count -ne 1 -or $cardAfter[0].Current.ProcessId -ne $app.Run.Identity.Pid) {
throw "Mark as entry did not preserve the owned Goal card for $scene."
}
$app.SObservation | Add-Member -NotePropertyName recoveryRoutes -NotePropertyValue `
([pscustomobject]@{wire=[pscustomobject]@{automationId=$recovery.wireAutomationId
result='Choose a target loop to wire this entry';cancelledWith='Escape'}
markEntry=[pscustomobject]@{automationId=$recovery.markAutomationId
result='Marked as an entry for this session';controlsRemoved=$true
cardPreserved=$cardAfter[0].Current.AutomationId}})
}
} else {
$null=Invoke-CapSceneAction -AppRun $app -Action 'open-loop' -Name $node.title
$total=1;$selected=0;$title=switch ($nodeIndex) {0 {'Goal'};6 {'Loop'};7 {'Agent'};default {'Copilot CLI'}}
Expand Down
52 changes: 52 additions & 0 deletions Tools/windows/Tests/AccessibilityProvider.Native.Tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,57 @@ static void nativeBannerInvokeChecks(IRawElementProviderSimple *provider, HWND h
if (projects) projects->Release();
}

static void nativeUnwiredRecoveryChecks(IRawElementProviderSimple *provider, HWND hwnd,
IRawElementProviderFragment *root) {
const char *identities[] = {
"wire-node:C:\\fixture:goal",
"mark-entry:C:\\fixture:goal",
};
const char *names[] = {"Wire it up", "Mark as entry"};
const int parents[] = {4, 4};
const int none[] = {0, 0};
const int actions[] = {1, 1};
const int bounds[] = {300, 120, 373, 140, 379, 120, 465, 140};
auto update = reinterpret_cast<FullUpdate>(&gc_uia_update);
check(update(provider, "Unwired recovery", identities, names, parents, none, none, actions,
bounds, 2, 0, 1, 0, 0, 0, 0) == S_OK,
"unwired recovery publication succeeds");

auto *projects = child(root, NavigateDirection_FirstChild);
auto *loops = child(projects, NavigateDirection_NextSibling);
auto *worktrees = child(loops, NavigateDirection_NextSibling);
auto *graph = child(worktrees, NavigateDirection_NextSibling);
auto *wire = child(graph, NavigateDirection_FirstChild);
auto *mark_entry = child(wire, NavigateDirection_NextSibling);
check(stringProperty(wire, UIA_AutomationIdPropertyId).rfind(L"wire-node-", 0) == 0,
"Wire it up uses its stable AutomationId prefix");
check(stringProperty(mark_entry, UIA_AutomationIdPropertyId).rfind(L"mark-entry-", 0) == 0,
"Mark as entry uses its stable AutomationId prefix");
checkName(wire, L"Wire it up");
checkName(mark_entry, L"Mark as entry");
check(invokable(wire) && invokable(mark_entry),
"both unwired recovery controls expose Invoke");

auto *invoke = static_cast<IInvokeProvider *>(pattern(wire, UIA_InvokePatternId));
posted_commands.clear();
if (invoke) {
check(invoke->Invoke() == S_OK, "Wire it up Invoke succeeds");
invoke->Release();
}
drainMessages(hwnd);
const uint64_t expected = 0x8000000000000000ULL |
identityPayload("wire-node:C:\\fixture:goal");
check(posted_commands.size() == 1 && posted_commands[0] == expected,
"Wire it up posts its exact stable identity once");

if (mark_entry) mark_entry->Release();
if (wire) wire->Release();
if (graph) graph->Release();
if (worktrees) worktrees->Release();
if (loops) loops->Release();
if (projects) projects->Release();
}

static void nativeQuickChatRestartChecks(IRawElementProviderSimple *provider, HWND hwnd,
IRawElementProviderFragment *root) {
check(!IsWindowVisible(hwnd), "Quick Chat restart is tested against a hidden provider window");
Expand Down Expand Up @@ -692,6 +743,7 @@ static void nativeCommandAndScrollChecks() {
if (root) {
nativeHeaderOverflowChecks(provider, capture, root);
nativeBannerInvokeChecks(provider, capture, root);
nativeUnwiredRecoveryChecks(provider, capture, root);
nativeQuickChatRestartChecks(provider, capture, root);
nativeQuickChatWorkspaceBarChecks(provider, root);
nativeScrollChecks(provider, capture, root);
Expand Down
4 changes: 2 additions & 2 deletions Tools/windows/Tests/AccessibilityProvider.Native.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ exit /b %test_exit%
$setupPathLength = $setupPath[0].Substring(5).Length
Write-Host "[native-uia] post-vcvars PATH length=$setupPathLength (cmd limit=8191)"
if ($setupPathLength -ge 8191) { throw "Native accessibility provider post-vcvars PATH exceeds cmd limit: $setupPathLength" }
if (@($nativeOutput | Where-Object { $_ -eq "Native accessibility provider: 224 checks, 0 failures" }).Count -ne 1) {
throw "Native accessibility provider test-exe did not report exactly one 224-check, zero-failure summary"
if (@($nativeOutput | Where-Object { $_ -eq "Native accessibility provider: 240 checks, 0 failures" }).Count -ne 1) {
throw "Native accessibility provider test-exe did not report exactly one 240-check, zero-failure summary"
}
} finally {
$env:TEMP = $savedTemp
Expand Down
36 changes: 35 additions & 1 deletion Tools/windows/Tests/UiCaptureDriver.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4011,6 +4011,7 @@ function Invoke-CapMatchedSceneRoutingContract {
$captured=[Collections.Generic.List[object]]::new()
$seeded=[Collections.Generic.List[string]]::new()
$model=[pscustomobject]@{total=1;selected=0;node=$null;removed=$false;restart=0;hover=0;freezeVisibility=$false
wireArmed=$false;marked=$false
selections=[Collections.Generic.List[string]]::new()}
function Get-MockVisibleIndexes {
if ($model.freezeVisibility) {return @(4..11)}
Expand All @@ -4024,7 +4025,7 @@ function Invoke-CapMatchedSceneRoutingContract {
function Start-CapOfflineSeed {param($Roots,$Phase,$SessionName) $seeded.Add($SessionName);[pscustomobject]@{mode='SIMULATED_SEED'}}
function Start-CapGraphCodeApp {
param($Roots,$Phase,$PipeName,$SessionPrefix)
$model.total=1;$model.selected=0;$model.node=$null;$model.removed=$false
$model.total=1;$model.selected=0;$model.node=$null;$model.removed=$false;$model.wireArmed=$false;$model.marked=$false
[pscustomobject]@{Run=[pscustomobject]@{Identity=[pscustomobject]@{Pid=4242}};Window=[IntPtr]1;Phase=$Phase}
}
function Invoke-CapSceneAction {
Expand All @@ -4040,8 +4041,41 @@ function Invoke-CapMatchedSceneRoutingContract {
$model.selections.Add("${scene}:$SubjectId")
$model.selected=[int]$SubjectId
}
'wire-node' {$model.wireArmed=$true}
'mark-entry' {$model.marked=$true}
}
}
function New-CapMockElement([string]$Id,[string]$Name,[int]$ProcessId,[double[]]$Bounds) {
$current=[pscustomobject]@{AutomationId=$Id;Name=$Name;ProcessId=$ProcessId
BoundingRectangle=[pscustomobject]@{Left=$Bounds[0];Top=$Bounds[1];Right=$Bounds[2];Bottom=$Bounds[3]
Width=($Bounds[2]-$Bounds[0]);Height=($Bounds[3]-$Bounds[1])}}
$element=[pscustomobject]@{Current=$current}
$element | Add-Member -MemberType ScriptMethod -Name GetCurrentPattern -Value {param($Pattern) [pscustomobject]@{}}
return $element
}
function Find-CapAutomationElement {
param($RootHandle,$ProcessIdentity,$ExpectedProcessId,$Name,$AutomationId)
if ($AutomationId -ceq 'graph') {return (New-CapMockElement 'graph' 'Graph' 4242 @(0,0,1200,900))}
if ($AutomationId -ceq 'status') {return (New-CapMockElement 'status' $Name 4242 @(0,880,1200,900))}
throw "Unexpected matched routing automation lookup: $AutomationId / $Name"
}
function Get-CapDirectChildren {
param($Parent)
$rows=[Collections.Generic.List[object]]::new()
foreach ($node in $fixture.graphs[0].nodes) {
$rows.Add((New-CapMockElement (Get-CapMatchedAutomationId ("project-card:"+$fixture.graphs[0].project.path+":"+$node.id)) `
$node.title 4242 @(250,100,500,196)))
}
if ($scene -cmatch '^s-goal-unwired-' -and -not $model.marked) {
$nodeId=@($fixture.states | Where-Object scene -CEQ $scene)[0].nodeId
$rows.Add((New-CapMockElement (Get-CapMatchedAutomationId ("wire-node:"+$fixture.graphs[0].project.path+":"+$nodeId)) `
'Wire it up' 4242 @(261,131,334,151)))
$rows.Add((New-CapMockElement (Get-CapMatchedAutomationId ("mark-entry:"+$fixture.graphs[0].project.path+":"+$nodeId)) `
'Mark as entry' 4242 @(340,131,426,151)))
}
return $rows.ToArray()
}
function Send-CapMatchedEscape {param($AppRun) $model.wireArmed=$false;return $true}
function Read-CapMatchedRows {
param($AppRun)
if ($scene -cnotmatch '^s-tabs-') {
Expand Down
Loading
Loading