using System.Globalization; using System.Reflection; using System.Text; using System.Windows.Forms; using OpenNest.Controls; using OpenNest.Forms; using OpenNest.Geometry; using Size = OpenNest.Geometry.Size; namespace OpenNest.WinForms.Tests.Forms; /// A main window with one nest window open, for undo tests. internal sealed record UndoTestEnv(MainForm Host, EditNestForm Editor) { public PlateView View => Editor.PlateView; public Nest Nest => Editor.Nest; /// /// Runs on an STA thread with a nest of four plates holding 2, 2, 0 /// and 1 squares of drawing "square", plus the empty new-plate sentinel. /// public static void Run(Action body, Func? nest = null) => StaTestThread.Run(() => { using var host = new TestHost(); host.Show(); using var editor = new EditNestForm((nest ?? BuildNest)()) { MdiParent = host }; editor.PlateView.SetOverlapAutoCheck(null); editor.Show(); editor.Activate(); body(new UndoTestEnv(host, editor)); }, TimeSpan.FromMinutes(1), "The STA test did not complete."); /// /// Runs and checks it recorded exactly one entry named /// that changed the nest, and that Undo puts the nest back. /// public void AssertOneUndoableEdit(string label, Action edit) { var before = Describe(Nest); var count = Editor.History.UndoCount; edit(); Assert.Equal(count + 1, Editor.History.UndoCount); Assert.Equal(label, Editor.History.UndoLabel); var after = Describe(Nest); Assert.NotEqual(before, after); Assert.True(Host.TryUndo(Editor), "Undo was refused."); Assert.Equal(before, Describe(Nest)); Assert.True(Host.TryRedo(Editor), "Redo was refused."); Assert.Equal(after, Describe(Nest)); } /// /// Once a dialog is shown, runs on it /// and clicks its accept button (or sets OK), so a command that shows it modally goes on. /// public static void AcceptWhenShown(Action? setup = null, Func? ready = null) where TForm : Form => WhenShown(form => { setup?.Invoke(form); if (form.AcceptButton is { } accept) accept.PerformClick(); else form.DialogResult = DialogResult.OK; }, ready); /// /// Runs on the first visible once /// (if given) holds for it. /// public static void WhenShown(Action act, Func? ready = null) where TForm : Form { var timer = new System.Windows.Forms.Timer { Interval = 10 }; var deadline = DateTime.UtcNow.AddSeconds(20); timer.Tick += (_, _) => { var form = Application.OpenForms.OfType().FirstOrDefault(f => f.Visible && ready?.Invoke(f) != false); if (form == null && DateTime.UtcNow < deadline) return; timer.Stop(); timer.Dispose(); if (form != null) act(form); }; timer.Start(); } public static Nest BuildNest() { var nest = new Nest("undo"); var square = new Drawing("square", Square(10)); nest.Drawings.Add(square); int[] partsPerPlate = { 2, 2, 0, 1 }; foreach (var count in partsPerPlate) { var plate = nest.CreatePlate(); plate.Size = new Size(100, 100); for (var i = 0; i < count; i++) plate.Parts.Add(new Part(square, new Vector(5 + i * 20, 5))); } return nest; } public static CNC.Program Square(double side) { var program = new CNC.Program(); program.MoveTo(0, 0); program.LineTo(0, side); program.LineTo(side, side); program.LineTo(side, 0); program.LineTo(0, 0); return program; } /// /// What the nest holds, as text: plates with their stock, settings, parts, cut-offs and /// clamps, drawings with their quantities, and nest-level settings. Undo compares it. /// public static string Describe(Nest nest) { var text = new StringBuilder(); void Line(FormattableString line) => text.AppendLine(line.ToString(CultureInfo.InvariantCulture)); Line($"machine {nest.Machine?.Id} support {nest.SupportBed != null} units {nest.Units}"); Line($"material {nest.Material?.Name} {nest.Material?.Grade} thickness {nest.Thickness} customer {nest.Customer} notes {nest.Notes}"); Line($"defaults {nest.PlateDefaults.Size} {nest.PlateDefaults.PartSpacing} {nest.PlateDefaults.ClampRail != null}"); Line($"options {nest.PlateOptions.Count} salvage {nest.SalvageRate} remnant {nest.MinimumRemnantDimension}"); foreach (var drawing in nest.Drawings.OrderBy(d => d.Name)) { var box = drawing.Program.BoundingBox(); Line($"drawing {drawing.Name} required {drawing.Quantity.Required} codes {drawing.Program.Length} box {box.Width:F4}x{box.Length:F4} priority {drawing.Priority}"); } foreach (var plate in nest.Plates) { Line($"plate {plate.Size} x{plate.Quantity} spacing {plate.PartSpacing} edge {plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top} quadrant {plate.Quadrant}"); Line($" params {(plate.CuttingParameters == null ? "none" : IO.CuttingParametersSerializer.Serialize(plate.CuttingParameters))}"); var clamps = plate.ClampZones.Select(z => FormattableString.Invariant($"{z.Id}@{z.Bounds.X:F4},{z.Bounds.Y:F4},{z.Bounds.Width:F4},{z.Bounds.Length:F4}")); Line($" clamps [{string.Join(";", clamps)}] removed {plate.RemovedClamps.Count} support {plate.SupportBed != null} stock {string.Join(",", plate.StockItemCodes)}"); foreach (var part in plate.Parts) { // A cut-off's part is regenerated from its definition, listed below; only its place in the order counts. if (part.BaseDrawing.IsCutOff) Line($" cutoff part"); else Line($" part {part.BaseDrawing.Name} {part.Location.X:F4},{part.Location.Y:F4} r{part.Rotation:F6} leads {part.HasManualLeadIns} locked {part.LeadInsLocked} codes {part.Program.Length}"); } foreach (var cutOff in plate.CutOffs) Line($" cutoff {cutOff.Axis} {cutOff.Position.X:F4},{cutOff.Position.Y:F4}"); } return text.ToString(); } public static ToolStripMenuItem MenuItem(MainForm host, string name) => (ToolStripMenuItem)typeof(MainForm).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(host)!; /// Calls a private or internal instance method of . public static object? Invoke(object target, string method, params object?[] args) { for (var type = target.GetType(); type != null; type = type.BaseType) { var found = type.GetMethod(method, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly); if (found != null) return found.Invoke(target, args); } throw new MissingMethodException(target.GetType().Name, method); } /// Calls a MainForm menu handler with no sender. public void Click(string handler) => typeof(MainForm).GetMethod(handler, BindingFlags.Instance | BindingFlags.NonPublic)! .Invoke(Host, new object?[] { null, EventArgs.Empty }); public static void Mouse(PlateView view, string method, MouseButtons button, System.Drawing.Point location) => typeof(PlateView).GetMethod(method, BindingFlags.Instance | BindingFlags.NonPublic)! .Invoke(view, new object[] { new MouseEventArgs(button, 1, location.X, location.Y, 0) }); public static void SetBackingField(object target, string property, object value) { for (var type = target.GetType(); type != null; type = type.BaseType) { var field = type.GetField($"<{property}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); if (field == null) continue; field.SetValue(target, value); return; } throw new MissingFieldException(target.GetType().Name, property); } private sealed class TestHost : MainForm { // No startup migrations/automatic new document, and no settings save at close. protected override void OnLoad(EventArgs e) { } protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { } } }