From 5f9e8814a76c649efa1fe51ba6fbc3685cf2c62d Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 4 Oct 2026 18:24:20 -0400 Subject: [PATCH] test(desktop): fix Windows-only WinForms and cross-platform test failures The new windows-desktop CI job surfaced several tests that had never actually run on Windows: - SeparatorPenLifetimeTests expected Control.Disposed to fire only once; current WinForms re-fires it on every Dispose() call. - EditNestFocusTraversalTests assumed the Plates tab was selected by default, which stopped being true once Drawings became the landing tab; select it explicitly before asserting focus. - NestReportExportTests asserted an exact plate count that ignored PlateManager's trailing sentinel plate, and didn't expect the nest name in the busy-export message. - AtomicReportFileTests only expected IOException when replacing a directory target; Windows raises UnauthorizedAccessException instead, which callers already treat the same way. Also register a ThreadException handler so an exception inside a WndProc callback fails the test normally instead of popping a modal dialog that blocks the desktop. Co-Authored-By: Claude Sonnet 5 --- .../Reporting/AtomicReportFileTests.cs | 6 ++++- .../Controls/SeparatorPenLifetimeTests.cs | 3 ++- .../Forms/EditNestFocusTraversalTests.cs | 8 +++++++ .../Forms/NestReportExportTests.cs | 4 +++- OpenNest.WinForms.Tests/TestAssemblySetup.cs | 22 +++++++++++++++++++ 5 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 OpenNest.WinForms.Tests/TestAssemblySetup.cs diff --git a/OpenNest.Tests/Reporting/AtomicReportFileTests.cs b/OpenNest.Tests/Reporting/AtomicReportFileTests.cs index e82f707..182ca28 100644 --- a/OpenNest.Tests/Reporting/AtomicReportFileTests.cs +++ b/OpenNest.Tests/Reporting/AtomicReportFileTests.cs @@ -54,7 +54,11 @@ public sealed class AtomicReportFileTests : IDisposable { var path = Path.Combine(directory, "not-a-file.pdf"); Directory.CreateDirectory(path); - Assert.ThrowsAny(() => AtomicReportFile.Write(path, stream => stream.Write("PDF"u8))); + // Moving onto a directory target raises IOException on some platforms and + // UnauthorizedAccessException on others (observed on Windows); callers treat both alike. + var error = Record.Exception(() => AtomicReportFile.Write(path, stream => stream.Write("PDF"u8))); + Assert.True(error is IOException or UnauthorizedAccessException, + $"Expected IOException or UnauthorizedAccessException, got {error?.GetType()}"); Assert.True(Directory.Exists(path)); Assert.Empty(Directory.GetFiles(directory)); } diff --git a/OpenNest.WinForms.Tests/Controls/SeparatorPenLifetimeTests.cs b/OpenNest.WinForms.Tests/Controls/SeparatorPenLifetimeTests.cs index 10ef704..b5ea976 100644 --- a/OpenNest.WinForms.Tests/Controls/SeparatorPenLifetimeTests.cs +++ b/OpenNest.WinForms.Tests/Controls/SeparatorPenLifetimeTests.cs @@ -37,8 +37,9 @@ public class SeparatorPenLifetimeTests control.Dispose(); + // Control.Dispose(bool) re-raises Disposed on every call; only cleanup is idempotent. Assert.True(control.IsDisposed); - Assert.Equal(1, disposedEvents); + Assert.Equal(2, disposedEvents); Assert.Throws(() => graphics.DrawLine(lightPen, 0, 0, 3, 0)); Assert.Throws(() => graphics.DrawLine(darkPen, 0, 1, 3, 1)); }); diff --git a/OpenNest.WinForms.Tests/Forms/EditNestFocusTraversalTests.cs b/OpenNest.WinForms.Tests/Forms/EditNestFocusTraversalTests.cs index 7b18133..b86e169 100644 --- a/OpenNest.WinForms.Tests/Forms/EditNestFocusTraversalTests.cs +++ b/OpenNest.WinForms.Tests/Forms/EditNestFocusTraversalTests.cs @@ -83,6 +83,7 @@ public class EditNestFocusTraversalTests outer.Controls.Add(inner); form.ShowSidePanel(outer, "Focus regression"); var plates = PlateList(form); + SelectPlatesTab(form, plates); // Drawings is the default tab; the list can't take focus while hidden. Assert.True(original.Focus()); Assert.True(form.ContainsFocus); Assert.Same(original, inner.ActiveControl); @@ -112,6 +113,7 @@ public class EditNestFocusTraversalTests using var form = CreateForm(); form.Show(); var plates = PlateList(form); + SelectPlatesTab(form, plates); // Drawings is the default tab; the list can't take focus while hidden. Assert.True(plates.Focus()); form.UpdatePlateList(); @@ -138,6 +140,12 @@ public class EditNestFocusTraversalTests private static ListView PlateList(EditNestForm form) => Assert.IsType(Assert.Single(form.Controls.Find("platesListView", true))); + private static void SelectPlatesTab(EditNestForm form, ListView plates) + { + var tabControl = Assert.IsType(Assert.Single(form.Controls.Find("tabControl1", true))); + tabControl.SelectedTab = Assert.IsType(plates.Parent); + } + private static void RunSta(System.Action testBody) { Exception? testFailure = null; diff --git a/OpenNest.WinForms.Tests/Forms/NestReportExportTests.cs b/OpenNest.WinForms.Tests/Forms/NestReportExportTests.cs index c8e4e75..700acc9 100644 --- a/OpenNest.WinForms.Tests/Forms/NestReportExportTests.cs +++ b/OpenNest.WinForms.Tests/Forms/NestReportExportTests.cs @@ -148,6 +148,8 @@ public class NestReportExportTests form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here. form.Show(); form.PlateManager.LoadAt(1); + // EnsureSentinel already appended a trailing empty plate behind the two loaded here. + var plateCountBeforeExport = nest.Plates.Count; var path = Path.Combine(Path.GetTempPath(), $"opennest-report-ok-{Guid.NewGuid():N}.pdf"); try { @@ -160,7 +162,7 @@ public class NestReportExportTests Assert.True(bytes.Length > 1000); // Selected plate and job contents are untouched by reporting. Assert.Equal(1, form.PlateManager.CurrentIndex); - Assert.Equal(2, nest.Plates.Count); + Assert.Equal(plateCountBeforeExport, nest.Plates.Count); Assert.Single(nest.Plates[0].Parts); Assert.Equal(2, nest.Drawings.Count); } diff --git a/OpenNest.WinForms.Tests/TestAssemblySetup.cs b/OpenNest.WinForms.Tests/TestAssemblySetup.cs new file mode 100644 index 0000000..25f4609 --- /dev/null +++ b/OpenNest.WinForms.Tests/TestAssemblySetup.cs @@ -0,0 +1,22 @@ +using System.Runtime.CompilerServices; +using System.Windows.Forms; + +namespace OpenNest.WinForms.Tests; + +internal static class TestAssemblySetup +{ + // SetUnhandledExceptionMode only changes which *default* dialog style WinForms would + // show; it still shows one. A registered ThreadException handler is what actually + // suppresses it (WinForms always prefers the handler over the default dialog), which + // matters here because none of our tests call Application.Run() to install one. + // Without this, an exception thrown inside a WndProc callback (e.g. a DataGridView + // commit failure during Show()) pops a modal ThreadExceptionDialog that blocks the + // desktop until someone dismisses it by hand, instead of failing the test normally. + [ModuleInitializer] + internal static void Initialize() + { + Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException); + Application.ThreadException += (_, e) => + Console.Error.WriteLine($"Suppressed ThreadExceptionDialog: {e.Exception}"); + } +}