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:
4 files changed
+82
-10
No files matched your search
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user