test(ui): cover shown BOM import and nest info persistence

This commit is contained in:
aj committed 2026-10-06 15:59:57 -04:00
1 parent b591528aa2
commit 71f41e23e4
3 files changed
+364

No files matched your search

@@ -1,13 +1,211 @@
using System.Data;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using OpenNest.Forms;
using OpenNest.IO.Bom;
namespace OpenNest.WinForms.Tests.Forms;
[Collection("Fill operation lifetime")]
public class BomImportFormTests
{
[Fact]
public void FileNameWithDxfExtension_RealRowBuilderShowsReady()
{
RunSta(() =>
{
using var files = new WindowsAcceptanceFiles();
var path = files.WriteSquare("acceptance-square");
var row = Assert.Single(BomImportRows.Build(
new List<BomItem> { Item("acceptance-square.dxf", qty: 2) }, files.Folder));
using var form = Show(out var parts);
form.LoadRows(new[] { row });
Assert.True(form.Visible);
Assert.Equal(path, row.DxfPath);
Assert.Same(row, parts.Rows[0].DataBoundItem);
Assert.Equal("acceptance-square.dxf", parts.Rows[0].Cells["colFileName"].Value);
Assert.Equal("Ready", parts.Rows[0].Cells["colStatus"].Value);
Assert.Equal(BomRowStatus.Ready, row.Status);
Assert.Single(GroupsTable(form).Rows);
Assert.Equal(2, GroupTotal(form, 0));
Assert.True(Field<Button>(form, "btnCreateNests").Enabled);
});
}
[Fact]
public void BlankThickness_RealRowBuilderResolvesDrawingAndGridEditMakesReadyGroup()
{
RunSta(() =>
{
using var files = new WindowsAcceptanceFiles();
var path = files.WriteSquare("acceptance-square");
var item = Item("acceptance-square", qty: 3);
item.Thickness = null;
var row = Assert.Single(BomImportRows.Build(new List<BomItem> { item }, files.Folder));
using var form = Show(out var parts);
form.LoadRows(new[] { row });
Assert.True(form.Visible);
Assert.Equal(path, row.DxfPath);
Assert.Null(row.Thickness);
Assert.True(row.IsEditable);
Assert.Equal("Needs thickness", parts.Rows[0].Cells["colStatus"].Value);
Assert.Empty(GroupsTable(form).Rows);
Assert.False(Field<Button>(form, "btnCreateNests").Enabled);
Assert.True(Edit(parts, 0, "colThickness", 0.25.ToString()));
Assert.Equal(0.25, row.Thickness);
Assert.Equal(BomRowStatus.Ready, row.Status);
Assert.Equal("Ready", parts.Rows[0].Cells["colStatus"].Value);
var group = Assert.Single(GroupsTable(form).Rows.Cast<DataRow>());
Assert.Equal("Stainless", group["Material"]);
Assert.Equal(0.25, group["Thickness"]);
Assert.Equal(1, group["Parts"]);
Assert.Equal(3, group["Total Qty"]);
Assert.Equal("1 ready", Field<Label>(form, "lblSummary").Text);
Assert.True(Field<Button>(form, "btnCreateNests").Enabled);
});
}
[Fact]
public void DuplicateRows_CreateNestsButtonImportsOneDrawingWithCombinedQuantity()
{
RunSta(() =>
{
using var files = new WindowsAcceptanceFiles();
var path = files.WriteSquare("acceptance-square");
var rows = BomImportRows.Build(
new List<BomItem>
{
Item("acceptance-square", qty: 2),
Item("acceptance-square.dxf", qty: 3),
}, files.Folder);
using var host = new Form { IsMdiContainer = true };
host.Show();
using var form = Show(out var parts);
form.MdiParentForm = host;
Field<TextBox>(form, "txtJobName").Text = "Acceptance job";
Field<TextBox>(form, "txtPlateWidth").Text = "10";
Field<TextBox>(form, "txtPlateLength").Text = "20";
form.LoadRows(rows);
Assert.Equal(2, parts.Rows.Count);
Assert.All(rows, row => Assert.Equal(path, row.DxfPath));
Assert.Equal(5, GroupTotal(form, 0));
var create = Field<Button>(form, "btnCreateNests");
Assert.True(create.Enabled);
// The production event ends in a native MessageBox. A timer in
// its modal loop clicks that thread's real OK button, retaining
// the actual result text; no builder or success result is stubbed.
var dialogTitle = "";
var dialogText = "";
var okPosted = false;
var deadline = DateTime.UtcNow.AddSeconds(10);
using var dismiss = new System.Windows.Forms.Timer { Interval = 20 };
dismiss.Tick += (_, _) =>
{
EnumThreadWindows(GetCurrentThreadId(), (window, _) =>
{
var className = new StringBuilder(256);
GetClassName(window, className, className.Capacity);
if (className.ToString() != "#32770")
return true;
dialogTitle = WindowText(window);
var text = FindWindowEx(window, IntPtr.Zero, "Static", null);
while (text != IntPtr.Zero)
{
var value = WindowText(text);
if (!string.IsNullOrEmpty(value))
dialogText += value;
text = FindWindowEx(window, text, "Static", null);
}
var ok = GetDlgItem(window, 1);
okPosted = ok != IntPtr.Zero && PostMessage(ok, 0x00F5, IntPtr.Zero, IntPtr.Zero);
dismiss.Stop();
return false;
}, IntPtr.Zero);
if (DateTime.UtcNow >= deadline && !okPosted)
throw new TimeoutException("The Create Nests result dialog could not be acknowledged.");
};
try
{
dismiss.Start();
create.PerformClick();
dismiss.Stop();
Assert.True(okPosted, "Create Nests did not show its result dialog.");
Assert.Equal("Import Complete", dialogTitle);
Assert.Equal("1 nest created.", dialogText);
Assert.False(form.Visible);
var editor = Assert.IsType<EditNestForm>(Assert.Single(host.MdiChildren));
Assert.True(editor.Visible);
var nest = editor.Nest;
var drawing = Assert.Single(nest.Drawings);
Assert.Equal("acceptance-square", drawing.Name);
Assert.Equal(5, drawing.Quantity.Required);
Assert.NotEmpty(drawing.Program.Codes);
Assert.Equal(4, drawing.Area, precision: 6);
Assert.Equal($"Acceptance job - {0.25:0.####} Stainless", nest.Name);
Assert.Equal("Stainless", nest.Material.Name);
Assert.Equal(0.25, nest.Thickness);
}
finally
{
dismiss.Stop();
foreach (var child in host.MdiChildren)
child.Dispose();
}
});
}
private static BomItem Item(string fileName, int qty) => new()
{
FileName = fileName,
Material = "Stainless",
Thickness = 0.25,
Qty = qty,
};
private static string WindowText(IntPtr window)
{
var text = new StringBuilder(1024);
GetWindowText(window, text, text.Capacity);
return text.ToString();
}
private delegate bool EnumThreadWindowCallback(IntPtr window, IntPtr parameter);
[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumThreadWindows(uint threadId, EnumThreadWindowCallback callback, IntPtr parameter);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetClassName(IntPtr window, StringBuilder className, int capacity);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetWindowText(IntPtr window, StringBuilder text, int capacity);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr FindWindowEx(IntPtr parent, IntPtr after, string className, string? title);
[DllImport("user32.dll")]
private static extern IntPtr GetDlgItem(IntPtr dialog, int id);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool PostMessage(IntPtr window, uint message, IntPtr wParam, IntPtr lParam);
[Fact]
public void PartColumns_KeepBomOrderAndOnlyMaterialThicknessAndQtyAreEditable()
{
@@ -1,9 +1,127 @@
using System.Runtime.ExceptionServices;
using System.Windows.Forms;
using OpenNest.Forms;
namespace OpenNest.WinForms.Tests.Forms;
[Collection("Fill operation lifetime")]
public class EditNestInfoFormTests
{
[Fact]
public void ShownEditorOk_FileRoundTripPreservesUnchangedMaterialGradeAndDensity()
{
RunSta(() =>
{
using var files = new WindowsAcceptanceFiles();
var nest = AcceptanceNest();
AcceptShownEditor(nest, materialName: null);
Assert.Equal("Stainless", nest.Material.Name);
Assert.Equal("304", nest.Material.Grade);
Assert.Equal(0.289, nest.Material.Density);
var restored = files.RoundTrip(nest);
Assert.Equal("Stainless", restored.Material.Name);
Assert.Equal("304", restored.Material.Grade);
Assert.Equal(0.289, restored.Material.Density);
Assert.Equal("Acceptance nest", restored.Name);
});
}
[Fact]
public void ShownEditorOk_FileRoundTripClearsOldMetadataWhenMaterialNameChanges()
{
RunSta(() =>
{
using var files = new WindowsAcceptanceFiles();
var nest = AcceptanceNest();
AcceptShownEditor(nest, materialName: "Aluminum");
Assert.Equal("Aluminum", nest.Material.Name);
Assert.True(string.IsNullOrEmpty(nest.Material.Grade));
Assert.Equal(0, nest.Material.Density);
var restored = files.RoundTrip(nest);
Assert.Equal("Aluminum", restored.Material.Name);
Assert.True(string.IsNullOrEmpty(restored.Material.Grade));
Assert.Equal(0, restored.Material.Density);
Assert.Equal("Acceptance nest", restored.Name);
});
}
private static Nest AcceptanceNest()
{
var nest = new Nest("Acceptance nest")
{
Material = new Material("Stainless", "304", 0.289),
Thickness = 0.25,
Units = Units.Inches,
};
nest.PlateDefaults.Size = new Geometry.Size(10, 20);
nest.CreatePlate();
return nest;
}
private static void AcceptShownEditor(Nest nest, string? materialName)
{
using var editor = new EditNestForm(nest);
editor.Show();
Assert.True(editor.Visible);
EditNestInfoForm? info = null;
Exception? failure = null;
var deadline = DateTime.UtcNow.AddSeconds(10);
using var accept = new System.Windows.Forms.Timer { Interval = 20 };
accept.Tick += (_, _) =>
{
info = Application.OpenForms.OfType<EditNestInfoForm>().SingleOrDefault();
if (info == null)
{
if (DateTime.UtcNow >= deadline)
throw new TimeoutException("The Nest Info editor was not shown.");
return;
}
accept.Stop();
try
{
Assert.True(info.Visible);
Assert.True(info.Modal);
if (materialName != null)
Assert.IsType<ComboBox>(info.Controls.Find("materialBox", true).Single()).Text = materialName;
info.EnableCheck();
var ok = Assert.IsType<Button>(info.Controls.Find("applyButton", true).Single());
Assert.True(ok.Enabled);
Assert.Equal(DialogResult.OK, ok.DialogResult);
ok.PerformClick();
}
catch (Exception ex)
{
failure = ex;
info.DialogResult = DialogResult.Cancel;
info.Close();
}
};
try
{
accept.Start();
// This is the live Nest Info entry point; it owns LoadNestInfo,
// ShowDialog and SaveNestInfo after the actual OK button accepts.
editor.ShowNestInfoEditor();
accept.Stop();
if (failure != null)
ExceptionDispatchInfo.Capture(failure).Throw();
Assert.NotNull(info);
Assert.Equal(DialogResult.OK, info.DialogResult);
Assert.False(info.Visible);
}
finally
{
accept.Stop();
info?.Dispose();
}
}
[Fact]
public void SaveWithUnchangedMaterial_KeepsGradeAndDensity()
{
@@ -0,0 +1,48 @@
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.WinForms.Tests.Forms;
/// <summary>Private, synthetic files only; removed after each acceptance case.</summary>
internal sealed class WindowsAcceptanceFiles : IDisposable
{
public string Folder { get; } = Path.Combine(Path.GetTempPath(), "opennest-acceptance-" + Guid.NewGuid().ToString("N"));
public WindowsAcceptanceFiles()
{
Directory.CreateDirectory(Folder);
}
public string WriteSquare(string name)
{
var shape = new Shape();
shape.Entities.Add(new Line(new Vector(0, 0), new Vector(2, 0)));
shape.Entities.Add(new Line(new Vector(2, 0), new Vector(2, 2)));
shape.Entities.Add(new Line(new Vector(2, 2), new Vector(0, 2)));
shape.Entities.Add(new Line(new Vector(0, 2), new Vector(0, 0)));
var path = Path.Combine(Folder, name + ".dxf");
Dxf.ExportProgram(ConvertGeometry.ToProgram(shape), path);
Assert.True(File.Exists(path));
return path;
}
public Nest RoundTrip(Nest nest)
{
var path = Path.Combine(Folder, "acceptance.nest");
Assert.True(new NestWriter(nest).Write(path));
Assert.True(File.Exists(path));
Assert.True(new FileInfo(path).Length > 0);
using var stream = File.OpenRead(path);
var reader = new NestReader(stream);
var restored = reader.Read();
Assert.Empty(reader.Warnings);
Assert.NotSame(nest, restored);
return restored;
}
public void Dispose()
{
Directory.Delete(Folder, recursive: true);
}
}