Files
OpenNest/OpenNest.WinForms.Tests/Forms/DrawingSetFormsTests.cs
T
ajandClaude Opus 5.5 87672543cd feat(desktop): import a BOM into a drawing set
Drawing Sets > More > Import BOM reads a BOM and its drawing folder,
matches each drawing to the library by its D4 identity and geometry
(reuse, new revision, or new drawing) and opens the set editor with a
new set prefilled with the BOM quantities. Reading only queries the
library; Create Set lists the uploads and revisions and asks before
storing them, and the set is stored only by the editor's Save.

Parts with no material or thickness still import and are flagged for
Edit Drawing. DrawingPackage.GeometryKey compares geometry independent
of the fresh entity ids each DXF import assigns. In Database mode File >
Import BOM opens this flow instead of creating nests directly; File mode
is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 15:33:13 -04:00

780 lines
37 KiB
C#

using System.Drawing;
using System.Reflection;
using System.Windows.Forms;
using OpenNest.Data;
using OpenNest.Forms;
using OpenNest.Tests.Data;
using DrawingSetBrowser = OpenNest.Controls.DrawingSetBrowser;
namespace OpenNest.WinForms.Tests.Forms;
public class DrawingSetFormsTests
{
private const string Server = "http://library.test:8090";
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(60);
[Fact]
public void DrawingSetsForm_LoadsWhenShown_AndNewSetIsSavedThroughTheEditor()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var bracket = library.Add("SYN-BRACKET", "Synthetic Steel", 0.25);
var gusset = library.Add("SYN-GUSSET", "Synthetic Aluminum", 0.125);
var sets = new InMemoryDrawingSets();
using var form = new DrawingSetsForm(sets, library, Server);
var browser = form.Browser;
browser.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
browser.EditSet = edit =>
{
using var editor = new EditDrawingSetForm(edit, library, Server);
editor.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
editor.Show();
var save = Field<Button>(editor, "saveButton");
var libraryGrid = Field<DataGridView>(editor, "libraryGrid");
PumpUntil(() => libraryGrid.Rows.Count == 2, "the editor's library pane");
Assert.False(save.Enabled);
Field<TextBox>(editor, "nameBox").Text = "Synthetic kit";
Field<TextBox>(editor, "customerBox").Text = "Synthetic customer";
libraryGrid.SelectAll();
Field<NumericUpDown>(editor, "quantityBox").Value = 3;
editor.AddSelectedDrawings();
Assert.True(save.Enabled);
Run(editor.SaveAsync());
return editor.Saved;
};
Assert.Empty(sets.Queries);
form.Show();
var grid = Field<DataGridView>(browser, "setGrid");
PumpUntil(() => sets.Queries.Count == 1 && !Field<Label>(browser, "summaryLabel").Text.StartsWith("Loading"), "the set list");
Assert.Contains("New Set", Field<Label>(browser, "summaryLabel").Text);
Run(browser.NewSetAsync());
PumpUntil(() => grid.Rows.Count == 1, "the saved set");
var stored = Assert.Single(sets.Current);
Assert.Equal(("Synthetic kit", "Synthetic customer", 1), (stored.Name, stored.Customer, stored.Revision));
Assert.Equal(new[] { (bracket.Id, 3), (gusset.Id, 3) }, stored.Members.Select(m => (m.DrawingId, m.Quantity)));
Assert.Equal("Synthetic kit", grid.Rows[0].Cells["nameColumn"].Value);
Assert.Equal(2, grid.Rows[0].Cells["membersColumn"].Value);
},
TestTimeout,
"The new set test did not complete.");
}
[Fact]
public void Editor_History_ShowsTheSavedSetsRevisions_AndIsHiddenForANewSet()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var a = library.Add("SYN-A", "Synthetic Steel", 0.25);
var sets = new InMemoryDrawingSets();
var created = Run(sets.CreateAsync(new DrawingSetRecord
{
Id = Guid.NewGuid(),
Name = "Synthetic kit",
Members = new[] { new DrawingSetMember { DrawingId = a.Id, Quantity = 2, Drawing = a } },
}));
var shown = new List<DrawingSetRecord>();
using (var editor = new EditDrawingSetForm(new DrawingSetEditSession(sets, created, "Synthetic operator"), library, Server))
{
editor.ShowHistory = shown.Add;
editor.Show();
var history = Field<Button>(editor, "historyButton");
Assert.True(history.Visible);
history.PerformClick();
Assert.Equal(created.Id, Assert.Single(shown).Id);
}
using var fresh = new EditDrawingSetForm(new DrawingSetEditSession(sets, null, "Synthetic operator"), library, Server);
fresh.Show();
Assert.False(Field<Button>(fresh, "historyButton").Visible);
},
TestTimeout,
"The editor history test did not complete.");
}
[Fact]
public void EditorLibraryPane_ListsEveryDrawing_AddsTheSelectionInListOrder_AndGreysMembers()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var a = library.Add("SYN-A", "Synthetic Steel", 0.25);
var b = library.Add("SYN-B", "Synthetic Aluminum", 0.125);
var c = library.Add("SYN-C", "Synthetic Steel", 0.25);
var sets = new InMemoryDrawingSets();
var created = Run(sets.CreateAsync(new DrawingSetRecord
{
Id = Guid.NewGuid(),
Name = "Synthetic kit",
Members = new[] { new DrawingSetMember { DrawingId = b.Id, Quantity = 2, Drawing = b } },
}));
using var editor = new EditDrawingSetForm(new DrawingSetEditSession(sets, created, "Synthetic operator"), library, Server);
editor.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
editor.Show();
var libraryGrid = Field<DataGridView>(editor, "libraryGrid");
var memberGrid = Field<DataGridView>(editor, "memberGrid");
var add = Field<Button>(editor, "addButton");
PumpUntil(() => libraryGrid.Rows.Count == 3, "the unfiltered library page");
Assert.Equal(("", "", (double?)null), (library.Queries[0].MaterialName, library.Queries[0].MaterialGrade, library.Queries[0].Thickness));
Assert.Equal(SystemColors.GrayText, libraryGrid.Rows[1].DefaultCellStyle.ForeColor);
Assert.Equal(Color.Empty, libraryGrid.Rows[0].DefaultCellStyle.ForeColor);
libraryGrid.ClearSelection();
libraryGrid.Rows[2].Selected = true;
libraryGrid.Rows[1].Selected = true;
libraryGrid.Rows[0].Selected = true;
Assert.True(add.Enabled);
Field<NumericUpDown>(editor, "quantityBox").Value = 4;
editor.AddSelectedDrawings();
Assert.Equal(new[] { ("SYN-B", 2), ("SYN-A", 4), ("SYN-C", 4) },
memberGrid.Rows.Cast<DataGridViewRow>().Select(r => ((string)r.Cells["drawingColumn"].Value, (int)r.Cells["quantityColumn"].Value)));
Assert.Contains("Already in the set: 'SYN-B'", Field<Label>(editor, "statusLabel").Text);
Assert.All(libraryGrid.Rows.Cast<DataGridViewRow>(), r => Assert.Equal(SystemColors.GrayText, r.DefaultCellStyle.ForeColor));
memberGrid.CurrentCell = memberGrid.Rows[1].Cells["drawingColumn"];
Field<Button>(editor, "removeButton").PerformClick();
Assert.Equal(Color.Empty, libraryGrid.Rows[0].DefaultCellStyle.ForeColor);
Run(editor.SaveAsync());
var stored = sets.Current.Single();
Assert.Equal(new[] { (b.Id, 2), (c.Id, 4) }, stored.Members.Select(m => (m.DrawingId, m.Quantity)));
},
TestTimeout,
"The editor library pane test did not complete.");
}
[Fact]
public void AddToSet_AppendsTheDrawingToTheChosenSet_AndRefusesADuplicate()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var existing = library.Add("SYN-A", "Synthetic Steel", 0.25);
var added = library.Add("SYN-B", "Synthetic Steel", 0.25);
var sets = new InMemoryDrawingSets();
var set = Run(sets.CreateAsync(new DrawingSetRecord
{
Id = Guid.NewGuid(),
Name = "Synthetic kit",
Members = new[] { new DrawingSetMember { DrawingId = existing.Id, Quantity = 1, Drawing = existing } },
}));
var messages = new List<string>();
var edited = new List<DrawingSetEditSession>();
using var form = new DrawingLibraryForm(library, Server, MatchingNest(), sets);
form.ShowMessage = (text, _, _) => { messages.Add(text); return DialogResult.OK; };
form.ChooseSet = () => (true, set);
form.EditSet = edit =>
{
edited.Add(edit);
return Run(edit.SaveAsync());
};
form.Show();
var grid = Field<DataGridView>(form, "drawingGrid");
PumpUntil(() => grid.Rows.Count == 2, "the library page");
grid.CurrentCell = grid.Rows[1].Cells[0];
Field<NumericUpDown>(form, "quantityBox").Value = 5;
Run(form.AddSelectedToSetAsync());
grid.CurrentCell = grid.Rows[0].Cells[0];
Run(form.AddSelectedToSetAsync());
var edit = Assert.Single(edited);
Assert.Equal(set.Id, edit.Id);
var stored = Assert.Single(sets.Current);
Assert.Equal(new[] { (existing.Id, 1), (added.Id, 5) }, stored.Members.Select(m => (m.DrawingId, m.Quantity)));
Assert.Equal(2, stored.Revision);
Assert.Contains("'SYN-A' is already in set 'Synthetic kit'", Assert.Single(messages));
},
TestTimeout,
"The add to set test did not complete.");
}
[Fact]
public void ChooseSet_ListsActiveSetsOnly_ChoosesTheSelectedOneOrNew()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var sets = new InMemoryDrawingSets();
var active = Run(sets.CreateAsync(new DrawingSetRecord { Id = Guid.NewGuid(), Name = "Synthetic kit" }));
var hidden = Run(sets.CreateAsync(new DrawingSetRecord { Id = Guid.NewGuid(), Name = "Synthetic old kit" }));
Run(sets.ArchiveAsync(hidden.Id, 1));
using (var picker = new ChooseDrawingSetForm(sets, library, Server))
{
picker.Show();
var grid = Field<DataGridView>(picker.Browser, "setGrid");
var choose = Field<Button>(picker, "chooseButton");
PumpUntil(() => grid.Rows.Count == 1 && choose.Enabled, "the active set");
Assert.False(Field<CheckBox>(picker.Browser, "showArchivedCheckBox").Visible);
// The picker keeps its own Choose/Cancel: none of the Sets tab's commands show.
Assert.Equal("New Set", Field<Button>(picker.Browser, "newButton").Text);
Assert.False(Field<Button>(picker.Browser, "editButton").Visible);
Assert.False(Field<Button>(picker.Browser, "moreButton").Visible);
Assert.False(Field<Button>(picker.Browser, "releaseButton").Visible);
Assert.Null(grid.ContextMenuStrip);
choose.PerformClick();
Assert.Equal(DialogResult.OK, picker.DialogResult);
Assert.Equal(active.Id, picker.PickedSet!.Id);
Assert.False(picker.PickedNew);
}
using (var picker = new ChooseDrawingSetForm(sets, library, Server))
{
picker.Show();
Run(picker.Browser.NewSetAsync());
Assert.Equal(DialogResult.OK, picker.DialogResult);
Assert.True(picker.PickedNew);
Assert.Null(picker.PickedSet);
}
},
TestTimeout,
"The choose set test did not complete.");
}
[Fact]
public void UsedInSets_ListsTheSetsContainingTheDrawing()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var drawing = library.Add("SYN-A", "Synthetic Steel", 0.25);
var sets = new InMemoryDrawingSets();
Run(sets.CreateAsync(new DrawingSetRecord
{
Id = Guid.NewGuid(),
Name = "Synthetic kit",
Customer = "Synthetic customer",
Members = new[] { new DrawingSetMember { DrawingId = drawing.Id, Quantity = 2, Drawing = drawing } },
}));
var messages = new List<string>();
using var form = new DrawingLibraryForm(library, Server, MatchingNest(), sets);
form.ShowMessage = (text, _, _) => { messages.Add(text); return DialogResult.OK; };
form.Show();
PumpUntil(() => Field<DataGridView>(form, "drawingGrid").Rows.Count == 1, "the library page");
Run(form.ShowUsedInSetsAsync());
Assert.Contains("is in 1 drawing set", Assert.Single(messages));
Assert.Contains("Synthetic kit (Synthetic customer)", messages[0]);
},
TestTimeout,
"The used in sets test did not complete.");
}
[Fact]
public void Browser_MoreMenu_ArchivesAfterConfirming_RestoresFromShowArchived_AndShowsHistory()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var sets = new InMemoryDrawingSets();
Run(sets.CreateAsync(new DrawingSetRecord { Id = Guid.NewGuid(), Name = "Synthetic kit" }));
var answers = new Queue<DialogResult>(new[] { DialogResult.Cancel, DialogResult.OK });
using var host = new Form();
var browser = new DrawingSetBrowser { Dock = DockStyle.Fill };
host.Controls.Add(browser);
browser.Initialize(sets, library, Server);
browser.ShowMessage = (_, _, _) => answers.Dequeue();
host.Show();
var grid = Field<DataGridView>(browser, "setGrid");
var more = Field<Button>(browser, "moreButton");
var archive = Field<ToolStripMenuItem>(browser, "archiveMenuItem");
var history = Field<ToolStripMenuItem>(browser, "historyMenuItem");
var releases = Field<ToolStripMenuItem>(browser, "releasesMenuItem");
Assert.Same(Field<ContextMenuStrip>(browser, "moreMenu"), grid.ContextMenuStrip);
Run(browser.RefreshAsync());
PumpUntil(() => grid.Rows.Count == 1, "the set list");
Assert.True(more.Enabled);
Assert.Equal("&Archive", archive.Text);
Assert.True(archive.Enabled);
Assert.True(history.Enabled);
// Releases needs a host that shows them (the Drawing Sets window).
Assert.False(releases.Enabled);
var shown = new List<DrawingSetRecord>();
browser.ShowHistory = shown.Add;
history.PerformClick();
Assert.Equal("Synthetic kit", Assert.Single(shown).Name);
Run(browser.ToggleArchivedAsync());
Assert.False(Assert.Single(sets.Current).Archived);
Run(browser.ToggleArchivedAsync());
PumpUntil(() => grid.Rows.Count == 0, "the archived set to leave the list");
Assert.True(Assert.Single(sets.Current).Archived);
Field<CheckBox>(browser, "showArchivedCheckBox").Checked = true;
PumpUntil(() => grid.Rows.Count == 1, "the archived set");
Assert.Equal("Archived", grid.Rows[0].Cells["stateColumn"].Value);
Assert.Equal("&Restore", archive.Text);
Assert.True(archive.Enabled);
Assert.True(history.Enabled);
Assert.False(Field<Button>(browser, "editButton").Enabled);
archive.PerformClick();
PumpUntil(() => !sets.Current.Single().Archived, "the set to be restored");
Assert.Equal(3, sets.Current.Single().Revision);
PumpUntil(() => archive.Text == "&Archive", "the restored set's menu");
// Nothing selected: only New Set and More's Import BOM stay enabled.
Field<CheckBox>(browser, "showArchivedCheckBox").Checked = false;
PumpUntil(() => grid.Rows.Count == 1, "the active set");
grid.CurrentCell = null;
Assert.True(Field<Button>(browser, "newButton").Enabled);
Assert.False(Field<Button>(browser, "editButton").Enabled);
Assert.True(more.Enabled);
Assert.True(Field<ToolStripMenuItem>(browser, "importBomMenuItem").Enabled);
Assert.False(archive.Enabled);
Assert.False(history.Enabled);
},
TestTimeout,
"The archive test did not complete.");
}
[Fact]
public void Editor_EditsQuantityAndMembers_KeepsTheirOrder_AndAsksBeforeDiscarding()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var a = library.Add("SYN-A", "Synthetic Steel", 0.25);
var b = library.Add("SYN-B", "Synthetic Steel", 0.25);
var c = library.Add("SYN-C", "Synthetic Steel", 0.25);
var sets = new InMemoryDrawingSets();
var created = Run(sets.CreateAsync(new DrawingSetRecord
{
Id = Guid.NewGuid(),
Name = "Synthetic kit",
Members = new[] { a, b, c }.Select(d => new DrawingSetMember { DrawingId = d.Id, Quantity = 1, Drawing = d }).ToList(),
}));
var questions = new List<string>();
using var editor = new EditDrawingSetForm(new DrawingSetEditSession(sets, created, "Synthetic operator"), library, Server);
editor.ShowMessage = (text, _, _) => { questions.Add(text); return DialogResult.No; };
editor.Show();
var grid = Field<DataGridView>(editor, "memberGrid");
var save = Field<Button>(editor, "saveButton");
Assert.False(save.Enabled);
grid.CurrentCell = grid.Rows[0].Cells["quantityColumn"];
Assert.True(grid.BeginEdit(true));
grid.EditingControl!.Text = "0";
grid.EndEdit();
Assert.Equal(1, grid.Rows[0].Cells["quantityColumn"].Value);
Assert.Contains("1 to 100,000", grid.Rows[0].ErrorText);
Assert.False(save.Enabled);
Assert.True(grid.BeginEdit(true));
grid.EditingControl!.Text = "12";
Assert.True(grid.EndEdit());
Assert.Equal("", grid.Rows[0].ErrorText);
// Member order changes nothing a release makes, so there is no Move Up/Down.
Assert.Null(typeof(EditDrawingSetForm).GetField("upButton", BindingFlags.Instance | BindingFlags.NonPublic));
Assert.Null(typeof(EditDrawingSetForm).GetField("downButton", BindingFlags.Instance | BindingFlags.NonPublic));
grid.CurrentCell = grid.Rows[1].Cells["drawingColumn"];
Field<Button>(editor, "removeButton").PerformClick();
Assert.True(save.Enabled);
editor.Close();
Assert.False(editor.IsDisposed);
Assert.Contains("Discard", Assert.Single(questions));
Run(editor.SaveAsync());
var stored = sets.Current.Single();
Assert.Equal(new[] { ("SYN-A", 12), ("SYN-C", 1) }, stored.Members.Select(m => (m.Drawing!.Name, m.Quantity)));
Assert.Equal((2, "Synthetic operator"), (stored.Revision, stored.SavedBy));
Assert.Equal(DialogResult.OK, editor.DialogResult);
},
TestTimeout,
"The set editor test did not complete.");
}
[Fact]
public void Editor_EditDrawing_ReadsTheCurrentRecord_AndShowsWhatWasSaved_WithoutChangingTheSet()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var a = library.Add("SYN-A", "", 0);
var b = library.Add("SYN-B", "Synthetic Steel", 0.25);
var sets = new InMemoryDrawingSets();
var created = Run(sets.CreateAsync(new DrawingSetRecord
{
Id = Guid.NewGuid(),
Name = "Synthetic kit",
Members = new[] { a, b }.Select(d => new DrawingSetMember { DrawingId = d.Id, Quantity = 2, Drawing = d }).ToList(),
}));
// The library moved on since the set was read: the edit starts from the current record.
library.Replace(a.Id, r => r.Notes = "Newer notes");
var edits = new List<DrawingLibraryEditSession>();
using var editor = new EditDrawingSetForm(new DrawingSetEditSession(sets, created, "Synthetic operator"), library, Server);
editor.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
editor.EditDrawing = edit =>
{
edits.Add(edit);
return Run(edit.SaveAsync(edit.Details with { MaterialName = "Synthetic Aluminum", Thickness = 0.125 }, edit.Listed.Notes, edit.Listed.Settings ?? new DrawingNestingSettings()));
};
editor.Show();
var grid = Field<DataGridView>(editor, "memberGrid");
Assert.Contains("double-click", (string)grid.Rows[0].Cells["warningColumn"].Value);
grid.CurrentCell = grid.Rows[0].Cells["drawingColumn"];
Assert.True(Field<Button>(editor, "editDrawingButton").Enabled);
Run(editor.EditSelectedDrawingAsync());
var edit = Assert.Single(edits);
Assert.Equal((a.Id, 2, "Newer notes"), (edit.Listed.Id, edit.Listed.Revision, edit.Listed.Notes));
Assert.Equal("Synthetic Aluminum Grade A 0.125 in", grid.Rows[0].Cells["materialColumn"].Value);
Assert.Equal("", grid.Rows[0].Cells["warningColumn"].Value);
Assert.Contains("revision 3", Field<Label>(editor, "statusLabel").Text);
// Members are listed by id, so the set has nothing new to save.
Assert.False(Field<Button>(editor, "saveButton").Enabled);
},
TestTimeout,
"The set editor edit drawing test did not complete.");
}
[Fact]
public void Editor_ShowsMaterialInOneColumn_AndCutTypeOnlyWhenMixed()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var a = library.Add("SYN-A", "Synthetic Steel", 0.25);
var b = library.Add("SYN-B", "", 0);
var c = library.Add("SYN-C", "Synthetic Steel", 6);
c.MaterialGrade = "";
c.Units = Units.Millimeters;
c.CutType = CutType.Waterjet;
var sets = new InMemoryDrawingSets();
var created = Run(sets.CreateAsync(new DrawingSetRecord
{
Id = Guid.NewGuid(),
Name = "Synthetic kit",
Members = new[] { a, b }.Select(d => new DrawingSetMember { DrawingId = d.Id, Quantity = 1, Drawing = d }).ToList(),
}));
using var editor = new EditDrawingSetForm(new DrawingSetEditSession(sets, created, "Synthetic operator"), library, Server);
editor.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
editor.Show();
var grid = Field<DataGridView>(editor, "memberGrid");
Assert.Equal(new[] { "drawingColumn", "materialColumn", "quantityColumn", "warningColumn" },
grid.Columns.Cast<DataGridViewColumn>().Where(col => col.Visible).OrderBy(col => col.DisplayIndex).Select(col => col.Name));
Assert.Equal("Synthetic Steel Grade A 0.25 in", grid.Rows[0].Cells["materialColumn"].Value);
// Grade A alone: no material name or thickness to show.
Assert.Equal("Grade A", grid.Rows[1].Cells["materialColumn"].Value);
Assert.Equal("Qty", grid.Columns["quantityColumn"].HeaderText);
// Another cut type in the set brings the column back.
var libraryGrid = Field<DataGridView>(editor, "libraryGrid");
PumpUntil(() => libraryGrid.Rows.Count == 3, "the editor's library pane");
libraryGrid.ClearSelection();
libraryGrid.Rows[2].Selected = true;
editor.AddSelectedDrawings();
Assert.Equal(3, grid.Rows.Count);
Assert.True(grid.Columns["cutTypeColumn"].Visible);
Assert.Equal("Synthetic Steel 6 mm", grid.Rows[2].Cells["materialColumn"].Value);
Assert.Equal("Waterjet", grid.Rows[2].Cells["cutTypeColumn"].Value);
grid.CurrentCell = grid.Rows[2].Cells["drawingColumn"];
Field<Button>(editor, "removeButton").PerformClick();
Assert.False(grid.Columns["cutTypeColumn"].Visible);
},
TestTimeout,
"The set editor columns test did not complete.");
}
[Fact]
public void History_ListsRevisionsNewestFirst_AndTheSelectedRevisionsMembers()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var a = library.Add("SYN-A", "Synthetic Steel", 0.25);
var sets = new InMemoryDrawingSets();
var created = Run(sets.CreateAsync(new DrawingSetRecord { Id = Guid.NewGuid(), Name = "Synthetic kit" }));
Run(sets.UpdateAsync(created.Id, new DrawingSetRecord
{
Name = "Synthetic kit",
Members = new[] { new DrawingSetMember { DrawingId = a.Id, Quantity = 6, Drawing = a } },
}, 1));
using var form = new DrawingSetHistoryForm(sets, created);
form.Show();
var revisions = Field<DataGridView>(form, "revisionGrid");
var members = Field<DataGridView>(form, "memberGrid");
PumpUntil(() => revisions.Rows.Count == 2 && members.Rows.Count == 1, "the history");
Assert.Equal(new object[] { 2, 1 }, revisions.Rows.Cast<DataGridViewRow>().Select(r => r.Cells["revisionColumn"].Value));
Assert.Equal(("SYN-A", 6), (members.Rows[0].Cells["drawingColumn"].Value, members.Rows[0].Cells["quantityColumn"].Value));
revisions.CurrentCell = revisions.Rows[1].Cells[0];
PumpUntil(() => members.Rows.Count == 0 && Field<GroupBox>(form, "membersGroup").Text.EndsWith("(0)"), "revision 1");
},
TestTimeout,
"The set history test did not complete.");
}
[Fact]
public void NestDrawingsToSet_AppendsTheNestsLibraryDrawings_KeepsSetQuantities_AndReportsSkips()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var existing = library.Add("SYN-A", "Synthetic Steel", 0.25);
var added = library.Add("SYN-B", "Synthetic Steel", 0.25);
var sets = new InMemoryDrawingSets();
var set = Run(sets.CreateAsync(new DrawingSetRecord
{
Id = Guid.NewGuid(),
Name = "Synthetic kit",
Members = new[] { new DrawingSetMember { DrawingId = existing.Id, Quantity = 1, Drawing = existing } },
}));
var nest = MatchingNest();
nest.Drawings.Add(LinkedDrawing(existing, 6));
nest.Drawings.Add(LinkedDrawing(added, 4));
nest.Drawings.Add(new Drawing("SYN-LOOSE") { Quantity = { Required = 2 } });
var messages = new List<string>();
var command = new NestDrawingsToSet(null, sets, library, Server);
command.ShowMessage = (text, buttons, _) =>
{
messages.Add(text);
Assert.Equal(MessageBoxButtons.OKCancel, buttons);
return DialogResult.OK;
};
command.ChooseSet = () => (true, set);
command.EditSet = edit => Run(edit.SaveAsync());
var saved = Run(command.RunAsync(nest));
Assert.Equal(2, saved.Revision);
Assert.Equal(new[] { (existing.Id, 1), (added.Id, 4) }, saved.Members.Select(m => (m.DrawingId, m.Quantity)));
var message = Assert.Single(messages);
Assert.StartsWith("Added 1 drawing to set 'Synthetic kit'.", message);
Assert.Contains("- SYN-A: set 1, nest 6", message);
Assert.Contains("- SYN-LOOSE: not in the drawing library", message);
},
TestTimeout,
"The nest drawings to set test did not complete.");
}
[Fact]
public void NestDrawingsToSet_NamesANewSetAfterTheNest_AndAsksNothingWhenEveryDrawingIsCurrent()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var a = library.Add("SYN-A", "Synthetic Steel", 0.25);
var sets = new InMemoryDrawingSets();
var nest = MatchingNest();
nest.Customer = "Synthetic customer";
nest.Drawings.Add(LinkedDrawing(a, 3));
DrawingSetEditSession edited = null;
var command = new NestDrawingsToSet(null, sets, library, Server);
command.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
command.ChooseSet = () => (true, null);
command.EditSet = edit =>
{
edited = edit;
return null;
};
Assert.Null(Run(command.RunAsync(nest)));
Assert.True(edited!.IsNew);
Assert.Equal(("Synthetic job", "Synthetic customer"), (edited.Name, edited.Customer));
Assert.Equal((a.Id, 3), edited.Members.Select(m => (m.DrawingId, m.Quantity)).Single());
Assert.Empty(sets.Creates);
},
TestTimeout,
"The nest drawings to new set test did not complete.");
}
[Fact]
public void NestDrawingsToSet_WithoutLibraryDrawings_SaysSoBeforeChoosingASet()
{
StaTestThread.Run(
() =>
{
var nest = MatchingNest();
nest.Drawings.Add(new Drawing("SYN-LOOSE") { Quantity = { Required = 1 } });
var messages = new List<string>();
var command = new NestDrawingsToSet(null, new InMemoryDrawingSets(), new FakeLibrary(), Server);
command.ShowMessage = (text, _, _) => { messages.Add(text); return DialogResult.OK; };
command.ChooseSet = () => throw new Xunit.Sdk.XunitException("No set should be chosen.");
Assert.Null(Run(command.RunAsync(nest)));
var message = Assert.Single(messages);
Assert.Contains("Save them to the drawing library first", message);
Assert.Contains("- SYN-LOOSE:", message);
},
TestTimeout,
"The nest without library drawings test did not complete.");
}
private static Drawing LinkedDrawing(DrawingLibraryRecord record, int required) => new(record.Name)
{
Quantity = { Required = required },
Library = new DrawingLibraryLink(Server, record.Id, record.Revision, record.CutType, record.Thickness,
record.MaterialName, record.MaterialGrade, record.Units, record.Notes, ""),
};
private static Nest MatchingNest() => new("Synthetic job")
{
Units = Units.Inches,
Thickness = 0.25,
Material = new Material("Synthetic Steel", "Grade A"),
};
private static void Run(Task task) => PumpUntil(() => task.IsCompleted, "the operation to finish");
private static T Run<T>(Task<T> task)
{
PumpUntil(() => task.IsCompleted, "the operation to finish");
return task.GetAwaiter().GetResult();
}
private static void PumpUntil(Func<bool> condition, string what)
{
var until = DateTime.UtcNow.AddSeconds(15);
while (!condition() && DateTime.UtcNow < until)
{
Application.DoEvents();
Thread.Sleep(10);
}
Assert.True(condition(), $"Timed out waiting for {what}.");
}
private static T Field<T>(object owner, string name) =>
(T)owner.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(owner)!;
/// <summary>Serves every record, ignoring filters; packages are never needed here.</summary>
private sealed class FakeLibrary : IDrawingLibraryStore
{
private readonly List<DrawingLibraryRecord> _records = new();
public List<DrawingLibraryQuery> Queries { get; } = new();
public DrawingLibraryRecord Add(string name, string material, double thickness)
{
var record = new DrawingLibraryRecord
{
Id = Guid.NewGuid(),
Revision = 1,
Name = name,
MaterialName = material,
MaterialGrade = "Grade A",
Thickness = thickness,
Units = Units.Inches,
CutType = CutType.Laser,
SavedAt = new DateTime(2026, 10, 1, 12, 0, 0, DateTimeKind.Utc),
};
_records.Add(record);
return record;
}
public Task<DrawingLibraryPage> QueryAsync(DrawingLibraryQuery query, CancellationToken cancellationToken = default)
{
Queries.Add(query);
return Task.FromResult(new DrawingLibraryPage
{
Items = _records.ToArray(),
Total = _records.Count,
Offset = query.Offset,
Limit = query.Limit,
});
}
public Task<byte[]?> GetRevisionPackageAsync(Guid id, int revision, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public Task<DrawingLibraryRecord?> GetAsync(Guid id, CancellationToken cancellationToken = default) =>
Task.FromResult(_records.FirstOrDefault(r => r.Id == id));
public Task<DrawingLibraryPage?> GetRevisionsAsync(Guid id, int offset = 0,
int limit = DrawingLibraryRules.DefaultRevisionLimit, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public Task<DrawingLibraryRecord> UploadAsync(DrawingLibraryRecord record, byte[] package,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public Task<DrawingLibraryRecord> UpdatePackageAsync(Guid id, DrawingLibraryRecord record, byte[] package,
int expectedRevision, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
/// <summary>Stores the next revision of a drawing, as another operator's save would.</summary>
public void Replace(Guid id, Action<DrawingLibraryRecord> edit)
{
var index = _records.FindIndex(r => r.Id == id);
var next = Copy(_records[index]);
next.Revision++;
edit(next);
_records[index] = next;
}
public Task<DrawingLibraryRecord> UpdateMetadataAsync(Guid id, DrawingLibraryRecord record, int expectedRevision,
CancellationToken cancellationToken = default)
{
var index = _records.FindIndex(r => r.Id == id);
if (_records[index].Revision != expectedRevision)
throw new DrawingLibraryConflictException(_records[index]);
var saved = Copy(record);
saved.Revision = expectedRevision + 1;
saved.Units = _records[index].Units;
saved.PackageSha256 = _records[index].PackageSha256;
_records[index] = saved;
return Task.FromResult(saved);
}
private static DrawingLibraryRecord Copy(DrawingLibraryRecord r) => new()
{
Id = r.Id,
Revision = r.Revision,
Name = r.Name,
Customer = r.Customer,
MaterialName = r.MaterialName,
MaterialGrade = r.MaterialGrade,
Thickness = r.Thickness,
Units = r.Units,
CutType = r.CutType,
Notes = r.Notes,
Settings = r.Settings,
PackageSha256 = r.PackageSha256,
SavedAt = r.SavedAt,
};
}
}