ci: add bounded complete release gate
This commit is contained in:
parent
0a934cf578
commit
2ac054864d
11 changed files with 772 additions and 102 deletions
|
|
@ -750,7 +750,18 @@ public sealed class LauncherProcessSupervisorTests
|
|||
{
|
||||
try
|
||||
{
|
||||
last = await File.ReadAllTextAsync(path);
|
||||
// This is a live-file poll, not a post-close read. Match
|
||||
// the production status tailer's sharing contract so the
|
||||
// assertion cannot transiently deny the stderr callback's
|
||||
// final append and make the no-throw capture sink latch
|
||||
// itself off before the expected line arrives.
|
||||
using var stream = new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite | FileShare.Delete);
|
||||
using var reader = new StreamReader(stream);
|
||||
last = await reader.ReadToEndAsync();
|
||||
if (last.Contains(expectedFragment, StringComparison.Ordinal))
|
||||
{
|
||||
return last;
|
||||
|
|
|
|||
|
|
@ -128,32 +128,39 @@ public sealed class MainWindowViewTests
|
|||
{
|
||||
using LauncherWindowViewModel viewModel = CreateViewModel();
|
||||
var window = new MainWindow { DataContext = viewModel };
|
||||
window.Show();
|
||||
try
|
||||
{
|
||||
window.Show();
|
||||
|
||||
viewModel.EditorDialog.Open(kind, "Fixture title", _ => { });
|
||||
Assert.True(viewModel.EditorDialog.IsOpen);
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
viewModel.EditorDialog.Open(kind, "Fixture title", _ => { });
|
||||
Assert.True(viewModel.EditorDialog.IsOpen);
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
|
||||
Control expectedFocus = (Control)GetNamedField(window, expectedFocusFieldName)!;
|
||||
Assert.Same(expectedFocus, CurrentFocus(window));
|
||||
Control expectedFocus = (Control)GetNamedField(window, expectedFocusFieldName)!;
|
||||
Assert.Same(expectedFocus, CurrentFocus(window));
|
||||
|
||||
viewModel.EditorDialog.Close();
|
||||
Assert.False(viewModel.EditorDialog.IsOpen);
|
||||
viewModel.EditorDialog.Close();
|
||||
Assert.False(viewModel.EditorDialog.IsOpen);
|
||||
|
||||
// Nothing held focus before the dialog opened, so
|
||||
// OnViewModelPropertyChanged's close branch posts the fallback
|
||||
// (ProfilesTree.Focus()). Pumping the dispatcher is what actually
|
||||
// *runs* FocusActiveModal's caller and its ProfilesTree
|
||||
// dereference — this is the #398 defect class: with
|
||||
// AvaloniaXamlLoader.Load(this) instead of InitializeComponent(),
|
||||
// ProfilesTree is null here and this throws
|
||||
// NullReferenceException out of the dispatcher. TreeView's Fluent
|
||||
// template sets Focusable="False" (focus lives on TreeViewItem
|
||||
// rows, not the tree itself), so a successful, non-throwing
|
||||
// ProfilesTree.Focus() call still leaves focus at null — that is
|
||||
// expected, not a failure.
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
Assert.NotSame(expectedFocus, CurrentFocus(window));
|
||||
// Nothing held focus before the dialog opened, so
|
||||
// OnViewModelPropertyChanged's close branch posts the fallback
|
||||
// (ProfilesTree.Focus()). Pumping the dispatcher is what actually
|
||||
// *runs* FocusActiveModal's caller and its ProfilesTree
|
||||
// dereference — this is the #398 defect class: with
|
||||
// AvaloniaXamlLoader.Load(this) instead of InitializeComponent(),
|
||||
// ProfilesTree is null here and this throws
|
||||
// NullReferenceException out of the dispatcher. TreeView's Fluent
|
||||
// template sets Focusable="False" (focus lives on TreeViewItem
|
||||
// rows, not the tree itself), so a successful, non-throwing
|
||||
// ProfilesTree.Focus() call still leaves focus at null — that is
|
||||
// expected, not a failure.
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
Assert.NotSame(expectedFocus, CurrentFocus(window));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseTestWindow(window);
|
||||
}
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
|
|
@ -161,22 +168,29 @@ public sealed class MainWindowViewTests
|
|||
{
|
||||
using LauncherWindowViewModel viewModel = CreateViewModel();
|
||||
var window = new MainWindow { DataContext = viewModel };
|
||||
window.Show();
|
||||
try
|
||||
{
|
||||
window.Show();
|
||||
|
||||
viewModel.FirstRunWizardShell.OpenCommand.Execute(null);
|
||||
Assert.True(viewModel.FirstRunWizardShell.IsOpen);
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
viewModel.FirstRunWizardShell.OpenCommand.Execute(null);
|
||||
Assert.True(viewModel.FirstRunWizardShell.IsOpen);
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
|
||||
Control datDirectoryBox = (Control)GetNamedField(window, "FirstRunDatDirectoryTextBox")!;
|
||||
Assert.Same(datDirectoryBox, CurrentFocus(window));
|
||||
Control datDirectoryBox = (Control)GetNamedField(window, "FirstRunDatDirectoryTextBox")!;
|
||||
Assert.Same(datDirectoryBox, CurrentFocus(window));
|
||||
|
||||
viewModel.FirstRunWizardShell.CloseCommand.Execute(null);
|
||||
Assert.False(viewModel.FirstRunWizardShell.IsOpen);
|
||||
viewModel.FirstRunWizardShell.CloseCommand.Execute(null);
|
||||
Assert.False(viewModel.FirstRunWizardShell.IsOpen);
|
||||
|
||||
// See the comment in the editor-kind theory above: this pump is
|
||||
// what actually executes the ProfilesTree.Focus() fallback.
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
Assert.NotSame(datDirectoryBox, CurrentFocus(window));
|
||||
// See the comment in the editor-kind theory above: this pump is
|
||||
// what actually executes the ProfilesTree.Focus() fallback.
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
Assert.NotSame(datDirectoryBox, CurrentFocus(window));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseTestWindow(window);
|
||||
}
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
|
|
@ -184,22 +198,29 @@ public sealed class MainWindowViewTests
|
|||
{
|
||||
using LauncherWindowViewModel viewModel = CreateViewModel();
|
||||
var window = new MainWindow { DataContext = viewModel };
|
||||
window.Show();
|
||||
try
|
||||
{
|
||||
window.Show();
|
||||
|
||||
await viewModel.UpdatePrompt.OpenCommand.ExecuteAsync();
|
||||
Assert.True(viewModel.UpdatePrompt.IsOpen);
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
await viewModel.UpdatePrompt.OpenCommand.ExecuteAsync();
|
||||
Assert.True(viewModel.UpdatePrompt.IsOpen);
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
|
||||
Control closeButton = (Control)GetNamedField(window, "UpdateCloseButton")!;
|
||||
Assert.Same(closeButton, CurrentFocus(window));
|
||||
Control closeButton = (Control)GetNamedField(window, "UpdateCloseButton")!;
|
||||
Assert.Same(closeButton, CurrentFocus(window));
|
||||
|
||||
viewModel.UpdatePrompt.CloseCommand.Execute(null);
|
||||
Assert.False(viewModel.UpdatePrompt.IsOpen);
|
||||
viewModel.UpdatePrompt.CloseCommand.Execute(null);
|
||||
Assert.False(viewModel.UpdatePrompt.IsOpen);
|
||||
|
||||
// See the comment in the editor-kind theory above: this pump is
|
||||
// what actually executes the ProfilesTree.Focus() fallback.
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
Assert.NotSame(closeButton, CurrentFocus(window));
|
||||
// See the comment in the editor-kind theory above: this pump is
|
||||
// what actually executes the ProfilesTree.Focus() fallback.
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
Assert.NotSame(closeButton, CurrentFocus(window));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseTestWindow(window);
|
||||
}
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
|
|
@ -207,33 +228,54 @@ public sealed class MainWindowViewTests
|
|||
{
|
||||
using LauncherWindowViewModel viewModel = CreateViewModel();
|
||||
var window = new MainWindow { DataContext = viewModel };
|
||||
window.Show();
|
||||
try
|
||||
{
|
||||
window.Show();
|
||||
|
||||
// ProfilesTree itself is not a Fluent focus target (its template
|
||||
// sets Focusable="False"; individual TreeViewItem rows are the
|
||||
// real tab stops), so use another genuinely focusable, always
|
||||
// visible control from the same non-modal chrome as the
|
||||
// "previously focused" anchor for the _focusBeforeModal != null
|
||||
// branch of MainWindow.OnViewModelPropertyChanged.
|
||||
Control addServerButton = window
|
||||
.GetVisualDescendants()
|
||||
.OfType<Button>()
|
||||
.First(button => Equals(button.Content, "+ Server"));
|
||||
addServerButton.Focus();
|
||||
Assert.Same(addServerButton, CurrentFocus(window));
|
||||
// ProfilesTree itself is not a Fluent focus target (its template
|
||||
// sets Focusable="False"; individual TreeViewItem rows are the
|
||||
// real tab stops), so use another genuinely focusable, always
|
||||
// visible control from the same non-modal chrome as the
|
||||
// "previously focused" anchor for the _focusBeforeModal != null
|
||||
// branch of MainWindow.OnViewModelPropertyChanged.
|
||||
Control addServerButton = window
|
||||
.GetVisualDescendants()
|
||||
.OfType<Button>()
|
||||
.First(button => Equals(button.Content, "+ Server"));
|
||||
addServerButton.Focus();
|
||||
Assert.Same(addServerButton, CurrentFocus(window));
|
||||
|
||||
viewModel.EditorDialog.Open(ProfileEditorKind.AddServer, "Fixture title", _ => { });
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
Control serverName = (Control)GetNamedField(window, "ServerNameTextBox")!;
|
||||
Assert.Same(serverName, CurrentFocus(window));
|
||||
|
||||
viewModel.EditorDialog.Close();
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
|
||||
// addServerButton was focused before the dialog opened, so the
|
||||
// restore branch (focusToRestore.Focus()) is what ran here, not
|
||||
// the ProfilesTree.Focus() fallback exercised by the tests above.
|
||||
Assert.Same(addServerButton, CurrentFocus(window));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseTestWindow(window);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CloseTestWindow(MainWindow window)
|
||||
{
|
||||
// Avalonia objects are thread-affine. A shown test window must be
|
||||
// closed and its compositor cleanup pumped by the same Avalonia test
|
||||
// session that created it; leaving it for runner/GC teardown made the
|
||||
// full parallel solution intermittently clean it from another thread.
|
||||
if (window.IsVisible)
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
|
||||
viewModel.EditorDialog.Open(ProfileEditorKind.AddServer, "Fixture title", _ => { });
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
Control serverName = (Control)GetNamedField(window, "ServerNameTextBox")!;
|
||||
Assert.Same(serverName, CurrentFocus(window));
|
||||
|
||||
viewModel.EditorDialog.Close();
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
|
||||
// addServerButton was focused before the dialog opened, so the
|
||||
// restore branch (focusToRestore.Focus()) is what ran here, not
|
||||
// the ProfilesTree.Focus() fallback exercised by the tests above.
|
||||
Assert.Same(addServerButton, CurrentFocus(window));
|
||||
}
|
||||
|
||||
private static Control? CurrentFocus(MainWindow window) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue