using EnvelopeRenderer.Desktop.Core.Design; namespace EnvelopeRenderer.Desktop.Tests; public class TemplateLayoutXmlSerializerTests : IDisposable { private readonly string _tempPath = Path.Combine(Path.GetTempPath(), $"envelope-designer-test-{Guid.NewGuid():N}.xml"); public void Dispose() { if (File.Exists(_tempPath)) { File.Delete(_tempPath); } } private static TemplateLayoutDocument BuildSampleDocument() { var document = new TemplateLayoutDocument(CanvasSettings.FromUnitValues(4.125, 9.5, CanvasUnit.Inches)); var staticElement = TextElementLayout.CreateStatic(120, 240, "Static label:", "Arial", 12, zOrder: 0); staticElement.Color = new RgbColor(200, 10, 30); var dynamicElement = TextElementLayout.CreateDynamic(120, 225, "Full Name", "Arial [Bold]", 14, zOrder: 1); document.Elements.Add(staticElement); document.Elements.Add(dynamicElement); return document; } [Fact] public void SaveThenLoad_RoundTripsCanvasSettingsExactly() { var original = BuildSampleDocument(); TemplateLayoutXmlSerializer.Save(original, _tempPath); var loaded = TemplateLayoutXmlSerializer.TryLoad(_tempPath, out var document, out var errors); Assert.True(loaded, string.Join("; ", errors)); Assert.Equal(original.Canvas.WidthPoints, document!.Canvas.WidthPoints, precision: 6); Assert.Equal(original.Canvas.HeightPoints, document.Canvas.HeightPoints, precision: 6); Assert.Equal(original.Canvas.DisplayUnit, document.Canvas.DisplayUnit); } [Fact] public void SaveThenLoad_RoundTripsElementPositionsAndProperties() { var original = BuildSampleDocument(); TemplateLayoutXmlSerializer.Save(original, _tempPath); TemplateLayoutXmlSerializer.TryLoad(_tempPath, out var document, out var errors); Assert.NotNull(document); Assert.Equal(2, document!.Elements.Count); var loadedStatic = document.Elements.Single(e => !e.IsDynamic); Assert.Equal(120, loadedStatic.X, precision: 6); Assert.Equal(240, loadedStatic.Y, precision: 6); Assert.Equal("Static label:", loadedStatic.StaticText); Assert.Equal("Arial", loadedStatic.FontFamily); Assert.Equal(12, loadedStatic.FontSize, precision: 6); Assert.Equal(new RgbColor(200, 10, 30), loadedStatic.Color); Assert.Equal(0, loadedStatic.ZOrder); var loadedDynamic = document.Elements.Single(e => e.IsDynamic); Assert.Equal("Full Name", loadedDynamic.ColumnName); Assert.Equal("Arial [Bold]", loadedDynamic.FontFamily); Assert.Equal(14, loadedDynamic.FontSize, precision: 6); Assert.Equal(1, loadedDynamic.ZOrder); } [Theory] // Sprint 3, Batch 3: the persisted `column` attribute must round-trip arbitrary, non-hardcoded // CSV header text exactly, including characters XML must escape (`&`, `'`, `"`) — proving the // designer doesn't just work for the couple of column names already used elsewhere as // examples (e.g. "Full Name"). [InlineData("Zip+4 Code")] [InlineData("Bill To & Ship To's Address")] [InlineData("Customer_Reference-No.")] [InlineData("Quoted \"Nickname\" Field")] public void SaveThenLoad_RoundTripsArbitraryColumnNameExactly(string columnName) { var document = new TemplateLayoutDocument(CanvasSettings.CreateDefault()); document.Elements.Add(TextElementLayout.CreateDynamic(10, 20, columnName)); TemplateLayoutXmlSerializer.Save(document, _tempPath); var loaded = TemplateLayoutXmlSerializer.TryLoad(_tempPath, out var reopened, out var errors); Assert.True(loaded, string.Join("; ", errors)); Assert.Equal(columnName, reopened!.Elements.Single().ColumnName); } [Fact] public void Save_DynamicElement_PersistsColumnAttributeInRenderTimeFormat() { // Confirms the designer writes the exact attribute shape TEMPLATE_FORMAT.md/ // TemplateXmlParser already consume at render time (`column="..."` on ``), not a // different token syntax invented for the designer. var document = new TemplateLayoutDocument(CanvasSettings.CreateDefault()); document.Elements.Add(TextElementLayout.CreateDynamic(10, 20, "Full Name")); TemplateLayoutXmlSerializer.Save(document, _tempPath); var xml = File.ReadAllText(_tempPath); Assert.Contains("column=\"Full Name\"", xml); } [Fact] public void Load_MissingFile_ReturnsFalseWithErrorNotException() { var result = TemplateLayoutXmlSerializer.TryLoad( Path.Combine(Path.GetTempPath(), $"does-not-exist-{Guid.NewGuid():N}.xml"), out var document, out var errors); Assert.False(result); Assert.Null(document); Assert.NotEmpty(errors); } [Fact] public void Load_MalformedXml_ReturnsFalseWithClearError() { File.WriteAllText(_tempPath, ""); var result = TemplateLayoutXmlSerializer.TryLoad(_tempPath, out var document, out var errors); Assert.False(result); Assert.Null(document); Assert.Contains(errors, e => e.Contains("not valid XML", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Load_WrongRootElement_ReturnsFalseWithClearError() { File.WriteAllText(_tempPath, ""); var result = TemplateLayoutXmlSerializer.TryLoad(_tempPath, out var document, out var errors); Assert.False(result); Assert.Null(document); Assert.Contains(errors, e => e.Contains("envelopeTemplate", StringComparison.Ordinal)); } [Fact] public void Load_MissingRequiredPageDimension_ReturnsFalseWithClearError() { File.WriteAllText(_tempPath, "Hi"); var result = TemplateLayoutXmlSerializer.TryLoad(_tempPath, out var document, out var errors); Assert.False(result); Assert.Contains(errors, e => e.Contains("pageWidth", StringComparison.Ordinal)); } [Fact] public void Load_TextElementWithBothColumnAndInlineText_ReturnsFalseWithClearError() { File.WriteAllText( _tempPath, "" + "Hi" + ""); var result = TemplateLayoutXmlSerializer.TryLoad(_tempPath, out var document, out var errors); Assert.False(result); Assert.Contains(errors, e => e.Contains("exactly one", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Load_InvalidColorValue_ReturnsFalseWithClearError() { File.WriteAllText( _tempPath, "" + "Hi" + ""); var result = TemplateLayoutXmlSerializer.TryLoad(_tempPath, out var document, out var errors); Assert.False(result); Assert.Contains(errors, e => e.Contains("color", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Load_TemplateWithoutDesignerAttributes_LoadsWithDefaults() { // Matches Sprint 1's plain render-time template shape (no canvasUnit/color/zOrder) — // the designer must still be able to open it, per this story's back-compat intent. File.WriteAllText( _tempPath, "" + "Static label:" + "" + ""); var result = TemplateLayoutXmlSerializer.TryLoad(_tempPath, out var document, out var errors); Assert.True(result, string.Join("; ", errors)); Assert.Equal(CanvasUnit.Points, document!.Canvas.DisplayUnit); Assert.Equal(2, document.Elements.Count); Assert.All(document.Elements, e => Assert.Equal(RgbColor.Black, e.Color)); Assert.Equal(0, document.Elements[0].ZOrder); Assert.Equal(1, document.Elements[1].ZOrder); } [Fact] public void Load_NegativeZOrder_ClampsToZeroRatherThanErroring() { File.WriteAllText( _tempPath, "" + "Hi" + ""); var result = TemplateLayoutXmlSerializer.TryLoad(_tempPath, out var document, out var errors); Assert.True(result, string.Join("; ", errors)); Assert.Equal(0, document!.Elements[0].ZOrder); } [Fact] public void Save_OverwritesExistingFile() { var first = new TemplateLayoutDocument(CanvasSettings.CreateDefault()); first.Elements.Add(TextElementLayout.CreateStatic(0, 0, "first")); TemplateLayoutXmlSerializer.Save(first, _tempPath); var second = new TemplateLayoutDocument(CanvasSettings.CreateDefault()); second.Elements.Add(TextElementLayout.CreateStatic(0, 0, "second")); TemplateLayoutXmlSerializer.Save(second, _tempPath); TemplateLayoutXmlSerializer.TryLoad(_tempPath, out var document, out _); Assert.Single(document!.Elements); Assert.Equal("second", document.Elements[0].StaticText); } }