test(documents): drive owned native controls after navigation

Reacquire browser glyphs, click unsupported menu providers physically and retain exact picker diagnostics. Keep individual waits unchanged; allow the measured expanded full suite to emit its TRX within a bounded 600-second process budget.

Refs #1, #14

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <jmaes@dmdbrands.com>
This commit is contained in:
James MaesandCodex committed 2026-10-11 13:20:03 -05:00
1 parent 1cfc07f900
commit 841798c750
4 files changed
+82 -10

No files matched your search

+2 -2
View File
@@ -185,7 +185,7 @@ try
if($loose.Installed -or $loose.AppKitSupported -or $null -ne $loose.Version) { throw 'Loose publish output unexpectedly supports updates.' }
$records.Add(@{ kind = 'loose'; result = $loose })
$env:K22_UITEST_EXE = Join-Path $buildRoot "publish/0.1.0/$mainExe"
Invoke-SmokeProcess -File 'dotnet' -Arguments @('test', "$repositoryRoot/tests/$name.UiTests", '--no-build', '--logger', 'trx;LogFileName=published-ui.trx', '--results-directory', $outputRoot) -TimeoutSeconds 300
Invoke-SmokeProcess -File 'dotnet' -Arguments @('test', "$repositoryRoot/tests/$name.UiTests", '--no-build', '--logger', 'trx;LogFileName=published-ui.trx', '--results-directory', $outputRoot) -TimeoutSeconds 600
foreach($initial in @($versions[0], $versions[2]))
{
$portableDirectory = Join-Path $outputRoot "portable-$($initial.channel)"
@@ -236,7 +236,7 @@ try
$after = Get-SmokeResult -Exe $installedProbe -ResultPath (Join-Path $outputRoot "$channel-after.json")
if($after.Version -ne $initial.target -or $after.Channel -ne $channel -or $null -ne $after.UpdateVersion -or $after.AppKitVersion -ne $initial.target) { throw 'Post-upgrade identity or channel is incorrect.' }
$env:K22_UITEST_EXE = $currentExe
Invoke-SmokeProcess -File 'dotnet' -Arguments @('test', "$repositoryRoot/tests/$name.UiTests", '--no-build', '--filter', 'FullyQualifiedName!~CheckForUpdates_InALooseBuild', '--logger', "trx;LogFileName=installed-$channel-ui.trx", '--results-directory', $outputRoot) -TimeoutSeconds 300
Invoke-SmokeProcess -File 'dotnet' -Arguments @('test', "$repositoryRoot/tests/$name.UiTests", '--no-build', '--filter', 'FullyQualifiedName!~CheckForUpdates_InALooseBuild', '--logger', "trx;LogFileName=installed-$channel-ui.trx", '--results-directory', $outputRoot) -TimeoutSeconds 600
$records.Add(@{ kind = 'installed'; channel = $channel; before = $before; after = $after; restarted = $true; appSha256 = $installedHash })
}
finally
+13 -1
View File
@@ -76,7 +76,19 @@ protects sibling settings and cleared history from stale window snapshots.
Signed/DCOe48a08c implements this milestone. Local376 core tests/100% lines and
zero-warning build/format/docs/hooks pass. Three new hosted-only
native lifecycle cases compile; no local GUI input. ADR-0004 and threat model
updated. These tests have not run. Association/activation registration remains
updated. 1cfc07f / CI38129552334 native54/57, zero skips: all original54 pass,
three new cases fail. First caches a glyph across zoom navigation; second requests
unsupported Invoke from SourceToggle after actual New/Recent/Close/missing/clear
steps; third cannot find Open picker's filename field. Fresh glyph lookup and
verified owned physical menu input retain all state/reload/history assertions.
Picker diagnostics now retain owned tree and generic exception/HRESULT; no selector
change is guessed. Actual native foreground focus and source preference persistence are required.
The missing-file window loaded nonempty history before Clear; its later preference
save must not resurrect that snapshot.
Retain native red log: artifacts/windows-first-native-38129552334.log. Published process timed out at300seconds after two recorded new-case failures;
no TRX/installed-channel outcomes. Retained partial output/screenshots:
artifacts/windows-first-failed-installed-38129552334. Outer full-UI-suite budget
now600seconds; all individual10/30second waits/assertions unchanged. Association/activation registration remains
required. Full reference audit also retains theme ordering/enable/delete/cycle,
JSON import/export conflicts, all typography/global CSS/content width and four
configurable rendering extensions in PRODUCT/TODO; none is claimed complete.
+1 -1
View File
@@ -171,7 +171,7 @@ public sealed partial class MainWindow : Window, IDisposable
if(!_closed)
{
StatusText.Text = "The file picker could not complete. Please try again.";
App.Log.Warning("The native document picker could not complete.");
App.Log.Warning(FormattableString.Invariant($"The native document picker could not complete. Exception={exception.GetType().Name}; HRESULT=0x{exception.HResult:X8}."));
}
}
}
@@ -3,11 +3,13 @@
* SPDX-License-Identifier: MIT
*/
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using FlaUI.Core.AutomationElements;
using FlaUI.Core.Definitions;
using FlaUI.Core.Input;
using FlaUI.Core.Patterns;
using FlaUI.Core.WindowsAPI;
@@ -31,7 +33,11 @@ public partial class AppTests
Assert.Equal(2, DocumentWindows(session).Length);
first.SetForeground();
WaitForWindowElement(session, first, "ZoomInButton").Patterns.Invoke.Pattern.Invoke();
session.WaitFor(() => firstHeading.BoundingRectangle.Height > firstHeight && WaitForWindowElement(session, first, "ZoomResetButton").IsEnabled ? firstHeading : null, "the first window's actual larger glyph");
session.WaitFor(() =>
{
AutomationElement? current = first.FindFirstDescendant(cf => cf.ByAutomationId("mv-heading-0"));
return current is not null && current.BoundingRectangle.Height > firstHeight && WaitForWindowElement(session, first, "ZoomResetButton").IsEnabled ? current : null;
}, "the first window's actual larger glyph after its document navigation");
Assert.Equal(secondHeight, secondHeading.BoundingRectangle.Height);
Assert.Equal("Actual size (100%)", WaitForWindowElement(session, second, "ZoomResetButton").Name);
first.SetForeground();
@@ -68,6 +74,7 @@ public partial class AppTests
Assert.Null(blank.FindFirstDescendant(cf => cf.ByAutomationId("mv-heading-0")));
Assert.Equal(3, DocumentWindows(session).Length);
InvokeDocumentMenu(session, blank, "RecentDocument:" + Array.IndexOf(history, Path.Combine(session.DataDirectory, "one.md")), recent: true);
session.WaitFor(() => GetForegroundWindow() == first.Properties.NativeWindowHandle.Value ? first : null, "the actual original reader brought to the native foreground by its recent entry");
Assert.Equal(3, DocumentWindows(session).Length);
Assert.Equal(history, ReadRecentDocuments(session));
InvokeDocumentMenu(session, blank, "CloseWindowMenuItem");
@@ -81,9 +88,14 @@ public partial class AppTests
Assert.Equal(history, ReadRecentDocuments(session));
InvokeDocumentMenu(session, missing, "ClearRecentDocuments", recent: true);
Assert.Empty(ReadRecentDocuments(session));
InvokeDocumentMenu(session, first, "SourceToggle", menuName: "View");
session.WaitFor(() => first.FindFirstDescendant(cf => cf.ByAutomationId("SourceText")), "the first window saving its source preference");
// This window loaded the nonempty history before Clear; its next preference save must not resurrect that stale snapshot.
InvokeDocumentMenu(session, missing, "SourceToggle", menuName: "View");
session.WaitFor(() => missing.FindFirstDescendant(cf => cf.ByAutomationId("SourceText")), "the older-history window saving its source preference after Clear");
Assert.Empty(ReadRecentDocuments(session));
using(var saved = JsonDocument.Parse(session.ReadSettings()))
{
Assert.True(saved.RootElement.GetProperty("ShowSource").GetBoolean());
}
Assert.True(File.Exists(Path.Combine(session.DataDirectory, "one.md")));
RetainDocumentWindow(session, first, "cleared-history-reader");
InvokeDocumentMenu(session, first, "ExitMenuItem");
@@ -105,7 +117,7 @@ public partial class AppTests
Window original = WaitForDocumentWindow(session, "original.txt");
WaitForWindowHeading(session, original, "Original picker reader");
InvokeDocumentMenu(session, original, "OpenMenuItem");
AutomationElement filename = session.WaitFor(() => session.FindInApp(cf => cf.ByAutomationId("1001").And(cf.ByControlType(ControlType.Edit))), "the real Windows multi-file picker filename field");
AutomationElement filename = WaitForOpenPickerFilename(session);
string paths = "\"" + Path.Combine(session.DataDirectory, "picked & caf\u00e9.mdown") + "\" \"" + Path.Combine(session.DataDirectory, "picked two.mkd") + "\"";
EnterPickerFilename(session, filename, paths);
session.ClickButton("Open");
@@ -118,7 +130,7 @@ public partial class AppTests
string[] history = ReadRecentDocuments(session);
Assert.Equal(3, history.Length);
InvokeDocumentMenu(session, original, "OpenMenuItem");
session.WaitFor(() => session.FindInApp(cf => cf.ByAutomationId("1001").And(cf.ByControlType(ControlType.Edit))), "the reopened native file picker");
WaitForOpenPickerFilename(session);
session.ClickButton("Cancel");
session.WaitForElementGone("1001");
Assert.Equal(3, DocumentWindows(session).Length);
@@ -129,6 +141,12 @@ public partial class AppTests
/// <summary>Observes actual native foreground ownership without relying on a mirrored application property.</summary>
[LibraryImport("user32.dll")]
private static partial nint GetForegroundWindow();
/// <summary>Counts only real app document windows, excluding native menu, log and picker surfaces.</summary>
private static Window[] DocumentWindows(AppSession session)
=> session.TopLevelWindows().Where(window => window.FindFirstDescendant(cf => cf.ByAutomationId("DocumentView")) is not null).Select(window => window.AsWindow()).ToArray();
@@ -174,12 +192,54 @@ public partial class AppTests
submenu.Patterns.ExpandCollapse.Pattern.Expand();
}
AutomationElement item = session.WaitFor(() => session.TopLevelWindows().Select(root => root.FindFirstDescendant(cf => cf.ByAutomationId(id))).FirstOrDefault(element => element is not null), "the requested native " + id + " command");
item.Patterns.Invoke.Pattern.Invoke();
if(item.Patterns.Invoke.TryGetPattern(out IInvokePattern? invoke))
{
invoke.Invoke();
}
else
{
Assert.True(item.TryGetClickablePoint(out System.Drawing.Point point));
NativeMouse.MoveTo(point);
Wait.UntilInputIsProcessed();
AutomationElement? hit = session.Automation.FromPoint(Mouse.Position);
bool target = false;
for(int depth = 0; hit is not null && depth < 8; depth++, hit = hit.Parent)
{
if(hit.Equals(item))
{
target = true;
break;
}
}
Assert.True(target, "The actual native pointer must be over the requested menu item or its child.");
Assert.Equal(session.App.ProcessId, session.Automation.FromPoint(Mouse.Position).Properties.ProcessId.ValueOrDefault);
Mouse.Down(MouseButton.Left);
Wait.UntilInputIsProcessed();
Mouse.Up(MouseButton.Left);
}
Wait.UntilInputIsProcessed();
}
/// <summary>Retains actual owned native picker controls and nonsensitive app errors when the exact filename lookup fails.</summary>
private static AutomationElement WaitForOpenPickerFilename(AppSession session)
{
try
{
return session.WaitFor(() => session.FindInApp(cf => cf.ByAutomationId("1001").And(cf.ByControlType(ControlType.Edit))), "the real Windows multi-file picker filename field");
}
catch(TimeoutException exception)
{
string windows = string.Join(Environment.NewLine, session.TopLevelWindows().Select(window =>
"Window=" + window.Name + "; " + string.Join(Environment.NewLine, window.FindAllDescendants().Take(180).Select(element =>
"Id=" + element.Properties.AutomationId.ValueOrDefault + "; Name=" + element.Properties.Name.ValueOrDefault + "; Type=" + element.Properties.ControlType.ValueOrDefault))));
throw new TimeoutException(exception.Message + " Logs: " + session.ReadLogs() + "; Owned native windows: " + windows + "; Main tree: " + session.DescribeDocumentStatus(), exception);
}
}
/// <summary>Retains only the owned fixture window; hosted installed evidence collects these actual pixels.</summary>
private static void RetainDocumentWindow(AppSession session, Window window, string name)
{