fix(ui): reject timed-out stock and shape input
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using System.Collections;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Forms;
|
||||
|
||||
@@ -73,6 +75,117 @@ public class AutoNestFormStockOptionsTests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StockSizeRegexHasExplicitTimeout()
|
||||
{
|
||||
var regex = Assert.IsType<Regex>(typeof(AutoNestForm)
|
||||
.GetField("SizePattern", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null));
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(250), regex.MatchTimeout);
|
||||
Assert.Equal(RegexOptions.None, regex.Options);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("48 x")]
|
||||
[InlineData("abc")]
|
||||
[InlineData("0 x 96")]
|
||||
[InlineData("-1 x 96")]
|
||||
[InlineData("١ x 96")]
|
||||
public void NonblankInvalidRowRejectsWholeCollection(string invalid) => RunSta(() =>
|
||||
{
|
||||
using var form = new StockTestForm();
|
||||
form.LoadPlateOptions(new List<PlateOption> { new() { Width = 48, Length = 96, Cost = 25 } }, 0.5);
|
||||
var grid = GetGrid(form);
|
||||
var rows = Assert.IsAssignableFrom<IBindingList>(grid.DataSource);
|
||||
var row = rows.AddNew()!;
|
||||
SetValue(row, "Size", invalid);
|
||||
((ICancelAddNew)rows).EndNew(rows.Count - 1);
|
||||
|
||||
Assert.False(form.TryGetPlateOptions(out var options, out var error));
|
||||
Assert.Empty(options);
|
||||
Assert.Contains(invalid, error);
|
||||
Assert.Contains("row 2", error);
|
||||
Assert.Throws<FormatException>(() => form.GetPlateOptions());
|
||||
Assert.NotEmpty(grid.Rows[1].ErrorText);
|
||||
|
||||
SetValue(row, "Size", "72.5 × 144.25");
|
||||
Assert.True(form.TryGetPlateOptions(out options, out error));
|
||||
Assert.Null(error);
|
||||
Assert.Equal(2, options.Count);
|
||||
Assert.Equal(72.5, options[1].Width);
|
||||
Assert.Equal(144.25, options[1].Length);
|
||||
Assert.Equal(25, options[0].Cost);
|
||||
Assert.Empty(grid.Rows[1].ErrorText);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void TimeoutAfterValidRowPublishesNoPrefixAndDoesNotAcceptDialog() => RunSta(() =>
|
||||
{
|
||||
using var form = new StockTestForm();
|
||||
form.LoadPlateOptions(new List<PlateOption> { new() { Width = 48, Length = 96 } }, 0.5);
|
||||
var grid = GetGrid(form);
|
||||
var rows = Assert.IsAssignableFrom<IBindingList>(grid.DataSource);
|
||||
var row = rows.AddNew()!;
|
||||
SetValue(row, "Size", "timeout row");
|
||||
((ICancelAddNew)rows).EndNew(rows.Count - 1);
|
||||
form.FailOn = "timeout row";
|
||||
|
||||
Assert.False(form.TryGetPlateOptions(out var options, out var error));
|
||||
Assert.Empty(options);
|
||||
Assert.Contains("timeout row", error);
|
||||
Assert.True(form.TryClose(DialogResult.OK));
|
||||
Assert.Equal(DialogResult.None, form.DialogResult);
|
||||
Assert.Contains("timeout row", Assert.Single(form.Notices));
|
||||
Assert.True(form.MatchCalls > 1);
|
||||
|
||||
form.Notices.Clear();
|
||||
Assert.False(form.TryClose(DialogResult.Cancel));
|
||||
Assert.Empty(form.Notices);
|
||||
Assert.False(Assert.IsType<Button>(form.Controls.Find("cancelButton", true).Single()).CausesValidation);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void InvalidPendingEditCannotPublishOldBoundValue() => RunSta(() =>
|
||||
{
|
||||
using var form = new StockTestForm();
|
||||
form.LoadPlateOptions(new List<PlateOption> { new() { Width = 48, Length = 96 } }, 0.5);
|
||||
var grid = GetGrid(form);
|
||||
form.Show();
|
||||
grid.CurrentCell = grid.Rows[0].Cells[0];
|
||||
Assert.True(grid.BeginEdit(false));
|
||||
grid.EditingControl.Text = "48 x";
|
||||
|
||||
Assert.False(form.TryGetPlateOptions(out var options, out var error));
|
||||
Assert.Empty(options);
|
||||
Assert.Contains("48 x", error);
|
||||
grid.CancelEdit();
|
||||
});
|
||||
|
||||
private sealed class StockTestForm : AutoNestForm
|
||||
{
|
||||
public StockTestForm() : base(new Nest()) { }
|
||||
public string? FailOn { get; set; }
|
||||
public int MatchCalls { get; private set; }
|
||||
public List<string> Notices { get; } = new();
|
||||
|
||||
internal override Match MatchStockSize(string value)
|
||||
{
|
||||
MatchCalls++;
|
||||
if (value == FailOn)
|
||||
throw new RegexMatchTimeoutException();
|
||||
return base.MatchStockSize(value);
|
||||
}
|
||||
|
||||
internal override void ReportStockValidationFailure(string error) => Notices.Add(error);
|
||||
|
||||
public bool TryClose(DialogResult result)
|
||||
{
|
||||
DialogResult = result;
|
||||
var closing = new FormClosingEventArgs(CloseReason.UserClosing, false);
|
||||
OnFormClosing(closing);
|
||||
return closing.Cancel;
|
||||
}
|
||||
}
|
||||
|
||||
private static DataGridView GetGrid(AutoNestForm form)
|
||||
{
|
||||
// Binding needs a context even when the test does not show the form.
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Drawing;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Forms;
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Forms;
|
||||
|
||||
public class ShapeLibraryFormTests
|
||||
{
|
||||
[Fact]
|
||||
public void FriendlyNameRegexHasExplicitTimeout()
|
||||
{
|
||||
var regex = Assert.IsType<Regex>(typeof(ShapeLibraryForm)
|
||||
.GetField("FriendlyNamePattern", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null));
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(100), regex.MatchTimeout);
|
||||
Assert.Equal(RegexOptions.None, regex.Options);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("PipeFlangeShape", "Pipe Flange")]
|
||||
[InlineData("HolePatternDiameter", "Hole Pattern Diameter")]
|
||||
[InlineData("NgonShape", "Ngon")]
|
||||
[InlineData("ABCShape", "ABC")]
|
||||
public void FriendlyNamesKeepSuffixAndWordBoundaryBehavior(string input, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ShapeLibraryForm.FriendlyName(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("PipeFlangeShape", "PipeFlange")]
|
||||
[InlineData("HolePatternDiameter", "HolePatternDiameter")]
|
||||
public void FriendlyNameTimeoutDiscardsPartialFormatting(string input, string expected)
|
||||
{
|
||||
var notices = new List<string>();
|
||||
var result = ShapeLibraryForm.FriendlyName(input, name =>
|
||||
{
|
||||
Assert.Equal(expected, name);
|
||||
// Simulate a replacement failing after it has transformed a prefix internally.
|
||||
var partial = name.Insert(4, " ");
|
||||
Assert.NotEqual(expected, partial);
|
||||
throw new RegexMatchTimeoutException();
|
||||
}, notices.Add);
|
||||
Assert.Equal(expected, result);
|
||||
Assert.Contains(expected, Assert.Single(notices));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidShapeInputClearsOldPreviewAndBlocksAddUntilCorrected() => ArchUnitsTests.RunSta(() =>
|
||||
{
|
||||
using var form = new ShapeLibraryForm();
|
||||
var list = Find<ListBox>(form, "shapeListBox");
|
||||
list.SelectedItem = list.Items.Cast<object>().Single(entry =>
|
||||
(Type)entry.GetType().GetProperty("ShapeType")!.GetValue(entry)! == typeof(RectangleShape));
|
||||
var preview = Find<ShapePreviewControl>(form, "previewBox");
|
||||
var add = Find<Button>(form, "addButton");
|
||||
var input = Find<Panel>(form, "parametersPanel").Controls.OfType<TextBox>().First();
|
||||
var valid = input.Text;
|
||||
Assert.NotEmpty(preview.Plate.Parts);
|
||||
Assert.True(add.Enabled);
|
||||
|
||||
input.Text = "invalid dimension";
|
||||
Assert.Equal(Color.Red, input.ForeColor);
|
||||
Assert.Empty(preview.Plate.Parts);
|
||||
Assert.False(add.Enabled);
|
||||
InvokeAdd(form);
|
||||
Assert.Empty(form.GetDrawings());
|
||||
|
||||
input.Text = valid;
|
||||
Assert.Equal(SystemColors.WindowText, input.ForeColor);
|
||||
Assert.NotEmpty(preview.Plate.Parts);
|
||||
Assert.True(add.Enabled);
|
||||
InvokeAdd(form);
|
||||
Assert.Single(form.GetDrawings());
|
||||
});
|
||||
|
||||
private static T Find<T>(Control form, string name) where T : Control =>
|
||||
Assert.IsType<T>(form.Controls.Find(name, true).Single());
|
||||
|
||||
private static void InvokeAdd(ShapeLibraryForm form) => typeof(ShapeLibraryForm)
|
||||
.GetMethod("AddButton_Click", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.Invoke(form, new object?[] { null, EventArgs.Empty });
|
||||
}
|
||||
Reference in New Issue
Block a user