template engine
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
using System.Text.Json;
|
||||
using Inspectron.Epson.TemplateEngine.DataBinding;
|
||||
|
||||
namespace Inspectron.Epson.TemplateEngine.Tests;
|
||||
|
||||
public class ExpressionEvaluatorTests
|
||||
{
|
||||
private static DataContext CreateContext(string json)
|
||||
{
|
||||
var root = JsonDocument.Parse(json).RootElement;
|
||||
return new DataContext(root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_SimpleProperty()
|
||||
{
|
||||
var ctx = CreateContext("""{"Name":"John"}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.Equal("Hello John", eval.Evaluate("Hello {{Name}}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_NestedProperty()
|
||||
{
|
||||
var ctx = CreateContext("""{"Person":{"Name":"Alice"}}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.Equal("Hi Alice", eval.Evaluate("Hi {{Person.Name}}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_MissingProperty_ReturnsEmpty()
|
||||
{
|
||||
var ctx = CreateContext("""{"Name":"John"}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.Equal("Hi ", eval.Evaluate("Hi {{Missing}}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_NumberProperty()
|
||||
{
|
||||
var ctx = CreateContext("""{"Count":42}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.Equal("Count: 42", eval.Evaluate("Count: {{Count}}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_FormatString_Decimal()
|
||||
{
|
||||
var ctx = CreateContext("""{"Price":9.5}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.Equal("9.50", eval.Evaluate("{{Price:F2}}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_FormatString_DateTime()
|
||||
{
|
||||
var ctx = CreateContext("""{"Date":"2024-03-15T14:30:00Z"}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.Equal("15-Mar-24 14:30", eval.Evaluate("{{Date:dd-MMM-yy HH:mm}}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_MultipleExpressions()
|
||||
{
|
||||
var ctx = CreateContext("""{"First":"John","Last":"Doe"}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.Equal("John Doe", eval.Evaluate("{{First}} {{Last}}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_CaseInsensitivePropertyLookup()
|
||||
{
|
||||
var ctx = CreateContext("""{"name":"Alice"}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.Equal("Alice", eval.Evaluate("{{Name}}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_EmptyTemplate()
|
||||
{
|
||||
var ctx = CreateContext("""{}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.Equal("", eval.Evaluate(""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_NoExpressions()
|
||||
{
|
||||
var ctx = CreateContext("""{}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.Equal("plain text", eval.Evaluate("plain text"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_LoopVariable_Index()
|
||||
{
|
||||
var ctx = CreateContext("""{}""");
|
||||
ctx.SetLoopVariable("$index", 3);
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.Equal("Index: 3", eval.Evaluate("Index: {{$index}}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_TruthyString()
|
||||
{
|
||||
var ctx = CreateContext("""{"Name":"John"}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.True(eval.EvaluateCondition("Name"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_EmptyString_IsFalsy()
|
||||
{
|
||||
var ctx = CreateContext("""{"Name":""}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.False(eval.EvaluateCondition("Name"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_MissingProperty_IsFalsy()
|
||||
{
|
||||
var ctx = CreateContext("""{}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.False(eval.EvaluateCondition("Name"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_Negation()
|
||||
{
|
||||
var ctx = CreateContext("""{"Name":"John"}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.False(eval.EvaluateCondition("!Name"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_Negation_Missing()
|
||||
{
|
||||
var ctx = CreateContext("""{}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.True(eval.EvaluateCondition("!Name"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_NullValue_IsFalsy()
|
||||
{
|
||||
var ctx = CreateContext("""{"Name":null}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.False(eval.EvaluateCondition("Name"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_Comparison_Equal()
|
||||
{
|
||||
var ctx = CreateContext("""{"Count":5}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.True(eval.EvaluateCondition("Count==5"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_Comparison_NotEqual()
|
||||
{
|
||||
var ctx = CreateContext("""{"Count":5}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.True(eval.EvaluateCondition("Count!=3"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_Comparison_GreaterThan()
|
||||
{
|
||||
var ctx = CreateContext("""{"Count":5}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.True(eval.EvaluateCondition("Count>3"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_Comparison_StringEqual()
|
||||
{
|
||||
var ctx = CreateContext("""{"Status":"active"}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.True(eval.EvaluateCondition("Status=='active'"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_LoopVariable_First()
|
||||
{
|
||||
var ctx = CreateContext("""{}""");
|
||||
ctx.SetLoopVariable("$first", true);
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.True(eval.EvaluateCondition("$first"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_LoopVariable_NotLast()
|
||||
{
|
||||
var ctx = CreateContext("""{}""");
|
||||
ctx.SetLoopVariable("$last", false);
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.True(eval.EvaluateCondition("!$last"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_NonEmptyArray_IsTruthy()
|
||||
{
|
||||
var ctx = CreateContext("""{"Items":[1,2,3]}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.True(eval.EvaluateCondition("Items"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_EmptyArray_IsFalsy()
|
||||
{
|
||||
var ctx = CreateContext("""{"Items":[]}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.False(eval.EvaluateCondition("Items"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_BooleanTrue_IsTruthy()
|
||||
{
|
||||
var ctx = CreateContext("""{"Active":true}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.True(eval.EvaluateCondition("Active"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateCondition_BooleanFalse_IsFalsy()
|
||||
{
|
||||
var ctx = CreateContext("""{"Active":false}""");
|
||||
var eval = new ExpressionEvaluator(ctx);
|
||||
|
||||
Assert.False(eval.EvaluateCondition("Active"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataContext_ChildScope_InheritsParentVariables()
|
||||
{
|
||||
var ctx = CreateContext("""{}""");
|
||||
var item = JsonDocument.Parse("""{"Name":"Pizza"}""").RootElement;
|
||||
ctx.SetVariable("item", item);
|
||||
|
||||
var child = ctx.CreateChildScope();
|
||||
var childEval = new ExpressionEvaluator(child);
|
||||
|
||||
Assert.Equal("Pizza", childEval.Evaluate("{{item.Name}}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataContext_ChildScope_OverridesParentVariable()
|
||||
{
|
||||
var ctx = CreateContext("""{}""");
|
||||
var item1 = JsonDocument.Parse("""{"Name":"Pizza"}""").RootElement;
|
||||
ctx.SetVariable("item", item1);
|
||||
|
||||
var child = ctx.CreateChildScope();
|
||||
var item2 = JsonDocument.Parse("""{"Name":"Pasta"}""").RootElement;
|
||||
child.SetVariable("item", item2);
|
||||
|
||||
var childEval = new ExpressionEvaluator(child);
|
||||
Assert.Equal("Pasta", childEval.Evaluate("{{item.Name}}"));
|
||||
|
||||
var parentEval = new ExpressionEvaluator(ctx);
|
||||
Assert.Equal("Pizza", parentEval.Evaluate("{{item.Name}}"));
|
||||
}
|
||||
}
|
||||
1
Inspectron.Epson.TemplateEngine.Tests/GlobalUsings.cs
Normal file
1
Inspectron.Epson.TemplateEngine.Tests/GlobalUsings.cs
Normal file
@@ -0,0 +1 @@
|
||||
global using Xunit;
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RollForward>Major</RollForward>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Inspectron.Epson.TemplateEngine\Inspectron.Epson.TemplateEngine.csproj" />
|
||||
<ProjectReference Include="..\Inspectron.Epson\Inspectron.Epson.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Templates\*.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,298 @@
|
||||
using Inspectron.Epson.PrintServer.Printers.Utils;
|
||||
using Inspectron.Epson.PrintServer.Printers.Utils.KitchenReceipt;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Inspectron.Epson.TemplateEngine.Tests;
|
||||
|
||||
public class KitchenReceiptTemplateTests
|
||||
{
|
||||
private const int LineWidth = 42;
|
||||
private const int BigLineWidth = 22;
|
||||
|
||||
private readonly ReceiptTemplateEngine _engine = new();
|
||||
|
||||
private static string GetTemplatePath()
|
||||
{
|
||||
return Path.Combine(AppContext.BaseDirectory, "Templates", "KitchenReceipt.xml");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderKitchenReceipt_BasicStructure()
|
||||
{
|
||||
var template = File.ReadAllText(GetTemplatePath());
|
||||
|
||||
var json = """
|
||||
{
|
||||
"Title": "Test Restaurant",
|
||||
"TransactionDateTime": "2024-03-15T14:30:00Z",
|
||||
"ReceiptNumber": "42",
|
||||
"WaiterName": "Max",
|
||||
"TableNumber": "5",
|
||||
"SpecialInstruction": null,
|
||||
"HasGangs": false,
|
||||
"HasDishes": false,
|
||||
"Gangs": [],
|
||||
"Dishes": []
|
||||
}
|
||||
""";
|
||||
|
||||
var commands = _engine.Render(template, json, LineWidth, BigLineWidth);
|
||||
|
||||
Assert.True(commands.Count > 0);
|
||||
|
||||
// First line: title centered in big font, red
|
||||
Assert.True(commands[0].IsBig);
|
||||
Assert.True(commands[0].IsTall);
|
||||
Assert.True(commands[0].IsRed);
|
||||
Assert.Contains("Test Restaurant", commands[0].Text);
|
||||
|
||||
// Second line: separator
|
||||
Assert.Equal(new string('-', LineWidth), commands[1].Text);
|
||||
|
||||
// Empty line
|
||||
Assert.Equal("", commands[2].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderKitchenReceipt_WithSpecialInstruction()
|
||||
{
|
||||
var template = File.ReadAllText(GetTemplatePath());
|
||||
|
||||
var json = """
|
||||
{
|
||||
"Title": "Test Restaurant",
|
||||
"TransactionDateTime": "2024-03-15T14:30:00Z",
|
||||
"ReceiptNumber": "42",
|
||||
"WaiterName": "Max",
|
||||
"TableNumber": "5",
|
||||
"SpecialInstruction": "RUSH ORDER",
|
||||
"HasGangs": false,
|
||||
"HasDishes": false,
|
||||
"Gangs": [],
|
||||
"Dishes": []
|
||||
}
|
||||
""";
|
||||
|
||||
var commands = _engine.Render(template, json, LineWidth, BigLineWidth);
|
||||
|
||||
// Should contain the special instruction
|
||||
var specialCommands = commands.Where(c => c.Text.Contains("RUSH ORDER")).ToList();
|
||||
Assert.NotEmpty(specialCommands);
|
||||
Assert.True(specialCommands[0].IsBig);
|
||||
Assert.True(specialCommands[0].IsBold);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderKitchenReceipt_WithGangs()
|
||||
{
|
||||
var template = File.ReadAllText(GetTemplatePath());
|
||||
|
||||
var json = """
|
||||
{
|
||||
"Title": "Test Restaurant",
|
||||
"TransactionDateTime": "2024-03-15T14:30:00Z",
|
||||
"ReceiptNumber": "42",
|
||||
"WaiterName": "Max",
|
||||
"TableNumber": "5",
|
||||
"SpecialInstruction": null,
|
||||
"HasGangs": true,
|
||||
"HasDishes": false,
|
||||
"Gangs": [
|
||||
{
|
||||
"Id": 1,
|
||||
"Name": "Vorspeise",
|
||||
"Dishes": [
|
||||
{
|
||||
"Number": 2,
|
||||
"Name": "Caesar Salad",
|
||||
"GuestPrefix": "",
|
||||
"GuestId": null,
|
||||
"Modifications": { "Removed": [], "Added": [], "HasModifications": false },
|
||||
"Comment": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": 2,
|
||||
"Name": "Hauptgang",
|
||||
"Dishes": [
|
||||
{
|
||||
"Number": 1,
|
||||
"Name": "Wiener Schnitzel",
|
||||
"GuestPrefix": "",
|
||||
"GuestId": null,
|
||||
"Modifications": { "Removed": ["Pommes"], "Added": ["Reis"], "HasModifications": true },
|
||||
"Comment": "Well done"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Dishes": []
|
||||
}
|
||||
""";
|
||||
|
||||
var commands = _engine.Render(template, json, LineWidth, BigLineWidth);
|
||||
|
||||
// Should contain gang names
|
||||
Assert.Contains(commands, c => c.Text.Contains("1. Vorspeise"));
|
||||
Assert.Contains(commands, c => c.Text.Contains("2. Hauptgang"));
|
||||
|
||||
// Should contain dish names
|
||||
Assert.Contains(commands, c => c.Text.Contains("2x Caesar Salad"));
|
||||
Assert.Contains(commands, c => c.Text.Contains("1x Wiener Schnitzel"));
|
||||
|
||||
// Should contain modifications
|
||||
Assert.Contains(commands, c => c.Text.Contains("- Pommes") && c.IsBold);
|
||||
Assert.Contains(commands, c => c.Text.Contains("+ Reis") && c.IsBold);
|
||||
|
||||
// Should contain comment
|
||||
Assert.Contains(commands, c => c.Text.Contains("Comment: Well done") && c.IsBold);
|
||||
|
||||
// Should have cut between gangs (not after last)
|
||||
var cutCommands = commands.Where(c => c.IsCut).ToList();
|
||||
Assert.Single(cutCommands);
|
||||
|
||||
// Gang headers should be red and big
|
||||
var gangHeaders = commands.Where(c => c.Text.Contains("Vorspeise") || c.Text.Contains("Hauptgang")).ToList();
|
||||
Assert.All(gangHeaders, c =>
|
||||
{
|
||||
Assert.True(c.IsRed);
|
||||
Assert.True(c.IsBig);
|
||||
Assert.True(c.IsTall);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderKitchenReceipt_DishesAreTall()
|
||||
{
|
||||
var template = File.ReadAllText(GetTemplatePath());
|
||||
|
||||
var json = """
|
||||
{
|
||||
"Title": "Test",
|
||||
"TransactionDateTime": "2024-01-01T00:00:00Z",
|
||||
"ReceiptNumber": "1",
|
||||
"WaiterName": "W",
|
||||
"TableNumber": "1",
|
||||
"SpecialInstruction": null,
|
||||
"HasGangs": true,
|
||||
"HasDishes": false,
|
||||
"Gangs": [
|
||||
{
|
||||
"Id": 1,
|
||||
"Name": "Gang1",
|
||||
"Dishes": [
|
||||
{
|
||||
"Number": 1,
|
||||
"Name": "Item",
|
||||
"GuestPrefix": "",
|
||||
"GuestId": null,
|
||||
"Modifications": { "Removed": [], "Added": [], "HasModifications": false },
|
||||
"Comment": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Dishes": []
|
||||
}
|
||||
""";
|
||||
|
||||
var commands = _engine.Render(template, json, LineWidth, BigLineWidth);
|
||||
|
||||
// Dish line should be tall
|
||||
var dishCmd = commands.First(c => c.Text.Contains("1x Item"));
|
||||
Assert.True(dishCmd.IsTall);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderKitchenReceipt_TableLineCenteredBigBold()
|
||||
{
|
||||
var template = File.ReadAllText(GetTemplatePath());
|
||||
|
||||
var json = """
|
||||
{
|
||||
"Title": "Test",
|
||||
"TransactionDateTime": "2024-01-01T00:00:00Z",
|
||||
"ReceiptNumber": "1",
|
||||
"WaiterName": "W",
|
||||
"TableNumber": "12",
|
||||
"SpecialInstruction": null,
|
||||
"HasGangs": false,
|
||||
"HasDishes": false,
|
||||
"Gangs": [],
|
||||
"Dishes": []
|
||||
}
|
||||
""";
|
||||
|
||||
var commands = _engine.Render(template, json, LineWidth, BigLineWidth);
|
||||
|
||||
var tableCmd = commands.First(c => c.Text.Contains("Tisch: 12"));
|
||||
Assert.True(tableCmd.IsBig);
|
||||
Assert.True(tableCmd.IsBold);
|
||||
// Should be centered in big font width
|
||||
Assert.True(tableCmd.Text.StartsWith(" "));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompareWithExistingConverter_BasicReceipt()
|
||||
{
|
||||
// Create a KitchenReceipt via the existing converter
|
||||
var receipt = new KitchenReceipt
|
||||
{
|
||||
Title = "Test Restaurant",
|
||||
TransactionDateTime = new DateTime(2024, 3, 15, 14, 30, 0),
|
||||
ReceiptNumber = "42",
|
||||
WaiterName = "Max Mustermann",
|
||||
WaiterId = "W001",
|
||||
TableNumber = "5",
|
||||
SpecialInstruction = null,
|
||||
Gangs = new List<Gang>(),
|
||||
Dishes = new List<Dish>()
|
||||
};
|
||||
|
||||
var existingConverter = new KitchenReceiptConverter(LineWidth, BigLineWidth);
|
||||
var existingCommands = existingConverter.ConvertToPrintCommands(receipt);
|
||||
|
||||
// Render with template engine
|
||||
var template = File.ReadAllText(GetTemplatePath());
|
||||
var jsonData = JsonSerializer.Serialize(new
|
||||
{
|
||||
receipt.Title,
|
||||
TransactionDateTime = receipt.TransactionDateTime.ToString("o"),
|
||||
receipt.ReceiptNumber,
|
||||
receipt.WaiterName,
|
||||
receipt.TableNumber,
|
||||
receipt.SpecialInstruction,
|
||||
HasGangs = receipt.Gangs.Count > 0,
|
||||
HasDishes = receipt.Dishes != null && receipt.Dishes.Any(),
|
||||
receipt.Gangs,
|
||||
receipt.Dishes
|
||||
});
|
||||
|
||||
var templateCommands = _engine.Render(template, jsonData, LineWidth, BigLineWidth);
|
||||
|
||||
// Compare key structural elements
|
||||
// Title should match
|
||||
Assert.Equal(existingCommands[0].Text, templateCommands[0].Text);
|
||||
Assert.Equal(existingCommands[0].IsRed, templateCommands[0].IsRed);
|
||||
Assert.Equal(existingCommands[0].IsBig, templateCommands[0].IsBig);
|
||||
Assert.Equal(existingCommands[0].IsTall, templateCommands[0].IsTall);
|
||||
|
||||
// Separator should match
|
||||
Assert.Equal(existingCommands[1].Text, templateCommands[1].Text);
|
||||
|
||||
// Empty line
|
||||
Assert.Equal(existingCommands[2].Text, templateCommands[2].Text);
|
||||
|
||||
// Date/receipt number line should match formatting
|
||||
Assert.Equal(existingCommands[3].Text, templateCommands[3].Text);
|
||||
|
||||
// Waiter name
|
||||
Assert.Equal(existingCommands[4].Text, templateCommands[4].Text);
|
||||
|
||||
// Table number
|
||||
Assert.Equal(existingCommands[5].Text, templateCommands[5].Text);
|
||||
Assert.Equal(existingCommands[5].IsBig, templateCommands[5].IsBig);
|
||||
Assert.Equal(existingCommands[5].IsBold, templateCommands[5].IsBold);
|
||||
}
|
||||
}
|
||||
154
Inspectron.Epson.TemplateEngine.Tests/LayoutEngineTests.cs
Normal file
154
Inspectron.Epson.TemplateEngine.Tests/LayoutEngineTests.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using Inspectron.Epson.TemplateEngine.Rendering;
|
||||
|
||||
namespace Inspectron.Epson.TemplateEngine.Tests;
|
||||
|
||||
public class LayoutEngineTests
|
||||
{
|
||||
private readonly LayoutEngine _engine = new();
|
||||
|
||||
[Fact]
|
||||
public void AlignText_Center()
|
||||
{
|
||||
var result = _engine.AlignText("Hello", "center", 20);
|
||||
Assert.Equal(" Hello", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlignText_Right()
|
||||
{
|
||||
var result = _engine.AlignText("Hello", "right", 20);
|
||||
Assert.Equal(" Hello", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlignText_Left()
|
||||
{
|
||||
var result = _engine.AlignText("Hello", "left", 20);
|
||||
Assert.Equal("Hello", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlignText_TextExceedsWidth_NoChange()
|
||||
{
|
||||
var result = _engine.AlignText("Very long text", "center", 5);
|
||||
Assert.Equal("Very long text", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatTwoColumns_BasicLayout()
|
||||
{
|
||||
var result = _engine.FormatTwoColumns("Left", "Right", 20);
|
||||
Assert.Equal(20, result.Length);
|
||||
Assert.StartsWith("Left", result);
|
||||
Assert.EndsWith("Right", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatTwoColumnsWithWrap_ShortText_SingleLine()
|
||||
{
|
||||
var result = _engine.FormatTwoColumnsWithWrap("Left", "Right", 20);
|
||||
Assert.Single(result);
|
||||
Assert.Contains("Left", result[0]);
|
||||
Assert.Contains("Right", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatTwoColumnsWithWrap_LongLeft_MultipleLines()
|
||||
{
|
||||
var result = _engine.FormatTwoColumnsWithWrap(
|
||||
"This is a very long left column text that needs wrapping",
|
||||
"9.50",
|
||||
30);
|
||||
Assert.True(result.Count >= 1);
|
||||
Assert.Contains("9.50", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WrapText_ShortText_SingleLine()
|
||||
{
|
||||
var result = _engine.WrapText("Hello world", 20);
|
||||
Assert.Single(result);
|
||||
Assert.Equal("Hello world", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WrapText_LongText_MultipleLines()
|
||||
{
|
||||
var result = _engine.WrapText("The quick brown fox jumps over the lazy dog", 20);
|
||||
Assert.True(result.Count > 1);
|
||||
foreach (var line in result)
|
||||
{
|
||||
Assert.True(line.TrimStart().Length <= 20);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WrapText_WithIndent()
|
||||
{
|
||||
var result = _engine.WrapText("The quick brown fox jumps over the lazy dog", 20, indent: 4);
|
||||
Assert.True(result.Count > 1);
|
||||
// First line no indent
|
||||
Assert.False(result[0].StartsWith(" "));
|
||||
// Subsequent lines indented
|
||||
for (int i = 1; i < result.Count; i++)
|
||||
{
|
||||
Assert.StartsWith(" ", result[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateSeparator()
|
||||
{
|
||||
var result = _engine.CreateSeparator('-', 42);
|
||||
Assert.Equal(new string('-', 42), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateSeparator_CustomChar()
|
||||
{
|
||||
var result = _engine.CreateSeparator('*', 20);
|
||||
Assert.Equal(new string('*', 20), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMultiColumn_BasicLayout()
|
||||
{
|
||||
var columns = new List<(string text, int width, string align)>
|
||||
{
|
||||
("Col1", 10, "left"),
|
||||
("Col2", 10, "right"),
|
||||
("Col3", 10, "center")
|
||||
};
|
||||
|
||||
var result = _engine.FormatMultiColumn(columns, 30);
|
||||
Assert.Equal(30, result.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatTable_BasicTable()
|
||||
{
|
||||
var columns = new List<(string text, int width, string align)>
|
||||
{
|
||||
("Name", 10, "left"),
|
||||
("Value", 10, "right")
|
||||
};
|
||||
|
||||
var headers = new List<List<string>> { new() { "Name", "Value" } };
|
||||
var data = new List<List<string>>
|
||||
{
|
||||
new() { "Item1", "100" },
|
||||
new() { "Item2", "200" }
|
||||
};
|
||||
|
||||
var result = _engine.FormatTable(columns, headers, data, 23);
|
||||
|
||||
// Should have: top border, header row, separator, 2 data rows, bottom border
|
||||
Assert.Equal(6, result.Count);
|
||||
Assert.StartsWith("+", result[0]);
|
||||
Assert.StartsWith("|", result[1]);
|
||||
Assert.StartsWith("+", result[2]);
|
||||
Assert.StartsWith("|", result[3]);
|
||||
Assert.StartsWith("|", result[4]);
|
||||
Assert.StartsWith("+", result[5]);
|
||||
}
|
||||
}
|
||||
294
Inspectron.Epson.TemplateEngine.Tests/TemplateParserTests.cs
Normal file
294
Inspectron.Epson.TemplateEngine.Tests/TemplateParserTests.cs
Normal file
@@ -0,0 +1,294 @@
|
||||
using Inspectron.Epson.TemplateEngine.Parsing;
|
||||
|
||||
namespace Inspectron.Epson.TemplateEngine.Tests;
|
||||
|
||||
public class TemplateParserTests
|
||||
{
|
||||
private readonly TemplateParser _parser = new();
|
||||
|
||||
[Fact]
|
||||
public void Parse_EmptyReceipt()
|
||||
{
|
||||
var result = _parser.Parse("<receipt></receipt>");
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result.Children);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_InvalidXml_Throws()
|
||||
{
|
||||
Assert.Throws<TemplateParsingException>(() => _parser.Parse("not xml"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_WrongRoot_Throws()
|
||||
{
|
||||
Assert.Throws<TemplateParsingException>(() => _parser.Parse("<div></div>"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_UnknownElement_Throws()
|
||||
{
|
||||
Assert.Throws<TemplateParsingException>(() =>
|
||||
_parser.Parse("<receipt><unknown /></receipt>"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Line_BasicText()
|
||||
{
|
||||
var result = _parser.Parse("<receipt><line>Hello</line></receipt>");
|
||||
Assert.Single(result.Children);
|
||||
var line = Assert.IsType<LineNode>(result.Children[0]);
|
||||
Assert.Equal("Hello", line.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Line_WithFormatting()
|
||||
{
|
||||
var result = _parser.Parse(
|
||||
"""<receipt><line bold="true" big="true" tall="true" red="true" align="center">Text</line></receipt>""");
|
||||
var line = Assert.IsType<LineNode>(result.Children[0]);
|
||||
Assert.True(line.Bold);
|
||||
Assert.True(line.Big);
|
||||
Assert.True(line.Tall);
|
||||
Assert.True(line.Red);
|
||||
Assert.Equal("center", line.Align);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Line_WithWrap()
|
||||
{
|
||||
var result = _parser.Parse(
|
||||
"""<receipt><line wrap="true" wrapIndent="4">Long text</line></receipt>""");
|
||||
var line = Assert.IsType<LineNode>(result.Children[0]);
|
||||
Assert.True(line.Wrap);
|
||||
Assert.Equal(4, line.WrapIndent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Line_WithLineSpacing()
|
||||
{
|
||||
var result = _parser.Parse(
|
||||
"""<receipt><line lineSpacing="50">Text</line></receipt>""");
|
||||
var line = Assert.IsType<LineNode>(result.Children[0]);
|
||||
Assert.Equal(50, line.LineSpacing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_EmptyLine()
|
||||
{
|
||||
var result = _parser.Parse("<receipt><line /></receipt>");
|
||||
var line = Assert.IsType<LineNode>(result.Children[0]);
|
||||
Assert.Equal("", line.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Columns()
|
||||
{
|
||||
var result = _parser.Parse(
|
||||
"""<receipt><columns left="Name" right="Price" bold="true" /></receipt>""");
|
||||
var col = Assert.IsType<ColumnsNode>(result.Children[0]);
|
||||
Assert.Equal("Name", col.Left);
|
||||
Assert.Equal("Price", col.Right);
|
||||
Assert.True(col.Bold);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Row_WithColumns()
|
||||
{
|
||||
var result = _parser.Parse("""
|
||||
<receipt>
|
||||
<row bold="true">
|
||||
<col width="10" align="left">Name</col>
|
||||
<col width="10" align="right">Value</col>
|
||||
</row>
|
||||
</receipt>
|
||||
""");
|
||||
|
||||
var row = Assert.IsType<RowNode>(result.Children[0]);
|
||||
Assert.True(row.Bold);
|
||||
Assert.Equal(2, row.Columns.Count);
|
||||
Assert.Equal("Name", row.Columns[0].Text);
|
||||
Assert.Equal(10, row.Columns[0].Width);
|
||||
Assert.Equal("left", row.Columns[0].Align);
|
||||
Assert.Equal("Value", row.Columns[1].Text);
|
||||
Assert.Equal(10, row.Columns[1].Width);
|
||||
Assert.Equal("right", row.Columns[1].Align);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Separator()
|
||||
{
|
||||
var result = _parser.Parse("<receipt><separator /></receipt>");
|
||||
var sep = Assert.IsType<SeparatorNode>(result.Children[0]);
|
||||
Assert.Equal('-', sep.Character);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Separator_CustomChar()
|
||||
{
|
||||
var result = _parser.Parse("""<receipt><separator char="*" /></receipt>""");
|
||||
var sep = Assert.IsType<SeparatorNode>(result.Children[0]);
|
||||
Assert.Equal('*', sep.Character);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Cut()
|
||||
{
|
||||
var result = _parser.Parse("<receipt><cut /></receipt>");
|
||||
Assert.IsType<CutNode>(result.Children[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Feed()
|
||||
{
|
||||
var result = _parser.Parse("""<receipt><feed lines="3" /></receipt>""");
|
||||
var feed = Assert.IsType<FeedNode>(result.Children[0]);
|
||||
Assert.Equal(3, feed.Lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Feed_Default()
|
||||
{
|
||||
var result = _parser.Parse("<receipt><feed /></receipt>");
|
||||
var feed = Assert.IsType<FeedNode>(result.Children[0]);
|
||||
Assert.Equal(1, feed.Lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Foreach()
|
||||
{
|
||||
var result = _parser.Parse("""
|
||||
<receipt>
|
||||
<foreach items="Items" var="item">
|
||||
<line>{{item.Name}}</line>
|
||||
</foreach>
|
||||
</receipt>
|
||||
""");
|
||||
|
||||
var fe = Assert.IsType<ForeachNode>(result.Children[0]);
|
||||
Assert.Equal("Items", fe.Items);
|
||||
Assert.Equal("item", fe.Var);
|
||||
Assert.Single(fe.Children);
|
||||
Assert.IsType<LineNode>(fe.Children[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Foreach_MissingItems_Throws()
|
||||
{
|
||||
Assert.Throws<TemplateParsingException>(() =>
|
||||
_parser.Parse("""<receipt><foreach var="item"><line /></foreach></receipt>"""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Foreach_MissingVar_Throws()
|
||||
{
|
||||
Assert.Throws<TemplateParsingException>(() =>
|
||||
_parser.Parse("""<receipt><foreach items="Items"><line /></foreach></receipt>"""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_If()
|
||||
{
|
||||
var result = _parser.Parse("""
|
||||
<receipt>
|
||||
<if test="Name">
|
||||
<line>Has name</line>
|
||||
</if>
|
||||
</receipt>
|
||||
""");
|
||||
|
||||
var ifNode = Assert.IsType<IfNode>(result.Children[0]);
|
||||
Assert.Equal("Name", ifNode.Test);
|
||||
Assert.Single(ifNode.Children);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_If_MissingTest_Throws()
|
||||
{
|
||||
Assert.Throws<TemplateParsingException>(() =>
|
||||
_parser.Parse("<receipt><if><line /></if></receipt>"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_IfElse()
|
||||
{
|
||||
var result = _parser.Parse("""
|
||||
<receipt>
|
||||
<if test="Name">
|
||||
<line>Yes</line>
|
||||
</if>
|
||||
<else>
|
||||
<line>No</line>
|
||||
</else>
|
||||
</receipt>
|
||||
""");
|
||||
|
||||
Assert.Equal(2, result.Children.Count);
|
||||
Assert.IsType<IfNode>(result.Children[0]);
|
||||
Assert.IsType<ElseNode>(result.Children[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_NestedForeach()
|
||||
{
|
||||
var result = _parser.Parse("""
|
||||
<receipt>
|
||||
<foreach items="Gangs" var="gang">
|
||||
<foreach items="gang.Dishes" var="dish">
|
||||
<line>{{dish.Name}}</line>
|
||||
</foreach>
|
||||
</foreach>
|
||||
</receipt>
|
||||
""");
|
||||
|
||||
var outer = Assert.IsType<ForeachNode>(result.Children[0]);
|
||||
Assert.Equal("Gangs", outer.Items);
|
||||
var inner = Assert.IsType<ForeachNode>(outer.Children[0]);
|
||||
Assert.Equal("gang.Dishes", inner.Items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_Table()
|
||||
{
|
||||
var result = _parser.Parse("""
|
||||
<receipt>
|
||||
<table items="Data" var="row">
|
||||
<col width="10" align="left">Header1</col>
|
||||
<col width="10" align="right">Header2</col>
|
||||
</table>
|
||||
</receipt>
|
||||
""");
|
||||
|
||||
var table = Assert.IsType<TableNode>(result.Children[0]);
|
||||
Assert.Equal("Data", table.Items);
|
||||
Assert.Equal("row", table.Var);
|
||||
Assert.Equal(2, table.Columns.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ComplexTemplate()
|
||||
{
|
||||
var result = _parser.Parse("""
|
||||
<receipt>
|
||||
<line align="center" big="true">Title</line>
|
||||
<separator />
|
||||
<line />
|
||||
<foreach items="Items" var="item">
|
||||
<line bold="true">{{item.Name}}</line>
|
||||
<if test="item.HasDiscount">
|
||||
<line red="true">DISCOUNT</line>
|
||||
</if>
|
||||
</foreach>
|
||||
<cut />
|
||||
</receipt>
|
||||
""");
|
||||
|
||||
Assert.Equal(5, result.Children.Count);
|
||||
Assert.IsType<LineNode>(result.Children[0]);
|
||||
Assert.IsType<SeparatorNode>(result.Children[1]);
|
||||
Assert.IsType<LineNode>(result.Children[2]);
|
||||
Assert.IsType<ForeachNode>(result.Children[3]);
|
||||
Assert.IsType<CutNode>(result.Children[4]);
|
||||
}
|
||||
}
|
||||
445
Inspectron.Epson.TemplateEngine.Tests/TemplateRendererTests.cs
Normal file
445
Inspectron.Epson.TemplateEngine.Tests/TemplateRendererTests.cs
Normal file
@@ -0,0 +1,445 @@
|
||||
namespace Inspectron.Epson.TemplateEngine.Tests;
|
||||
|
||||
public class TemplateRendererTests
|
||||
{
|
||||
private readonly ReceiptTemplateEngine _engine = new();
|
||||
|
||||
[Fact]
|
||||
public void Render_EmptyReceipt()
|
||||
{
|
||||
var commands = _engine.Render("<receipt></receipt>", "{}", 42, 22);
|
||||
Assert.Empty(commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_SimpleLine()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"<receipt><line>Hello World</line></receipt>",
|
||||
"{}", 42, 22);
|
||||
|
||||
Assert.Single(commands);
|
||||
Assert.Equal("Hello World", commands[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_LineWithDataBinding()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"<receipt><line>Hello {{Name}}</line></receipt>",
|
||||
"""{"Name":"John"}""", 42, 22);
|
||||
|
||||
Assert.Single(commands);
|
||||
Assert.Equal("Hello John", commands[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_LineWithFormatting()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"""<receipt><line bold="true" big="true" tall="true" red="true">Text</line></receipt>""",
|
||||
"{}", 42, 22);
|
||||
|
||||
var cmd = commands[0];
|
||||
Assert.True(cmd.IsBold);
|
||||
Assert.True(cmd.IsBig);
|
||||
Assert.True(cmd.IsTall);
|
||||
Assert.True(cmd.IsRed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_LineWithCenterAlignment()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"""<receipt><line align="center">Test</line></receipt>""",
|
||||
"{}", 42, 22);
|
||||
|
||||
Assert.Equal(" Test", commands[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_LineWithCenterAlignment_BigFont()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"""<receipt><line align="center" big="true">Test</line></receipt>""",
|
||||
"{}", 42, 22);
|
||||
|
||||
Assert.Equal(" Test", commands[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_EmptyLine()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"<receipt><line /></receipt>",
|
||||
"{}", 42, 22);
|
||||
|
||||
Assert.Single(commands);
|
||||
Assert.Equal("", commands[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_Separator()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"<receipt><separator /></receipt>",
|
||||
"{}", 42, 22);
|
||||
|
||||
Assert.Single(commands);
|
||||
Assert.Equal(new string('-', 42), commands[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_Cut()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"<receipt><cut /></receipt>",
|
||||
"{}", 42, 22);
|
||||
|
||||
Assert.Single(commands);
|
||||
Assert.True(commands[0].IsCut);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_Feed()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"""<receipt><feed lines="3" /></receipt>""",
|
||||
"{}", 42, 22);
|
||||
|
||||
Assert.Equal(3, commands.Count);
|
||||
Assert.All(commands, c => Assert.Equal("", c.Text));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_Columns()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"""<receipt><columns left="Name" right="Price" /></receipt>""",
|
||||
"{}", 42, 22);
|
||||
|
||||
Assert.Single(commands);
|
||||
Assert.Equal(42, commands[0].Text.Length);
|
||||
Assert.StartsWith("Name", commands[0].Text);
|
||||
Assert.EndsWith("Price", commands[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_ColumnsWithDataBinding()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"""<receipt><columns left="{{Label}}" right="{{Value}}" /></receipt>""",
|
||||
"""{"Label":"Total","Value":"100.00"}""", 42, 22);
|
||||
|
||||
Assert.Single(commands);
|
||||
Assert.Contains("Total", commands[0].Text);
|
||||
Assert.Contains("100.00", commands[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_Foreach()
|
||||
{
|
||||
var commands = _engine.Render("""
|
||||
<receipt>
|
||||
<foreach items="Items" var="item">
|
||||
<line>{{item.Name}}</line>
|
||||
</foreach>
|
||||
</receipt>
|
||||
""",
|
||||
"""{"Items":[{"Name":"Pizza"},{"Name":"Pasta"},{"Name":"Salad"}]}""",
|
||||
42, 22);
|
||||
|
||||
Assert.Equal(3, commands.Count);
|
||||
Assert.Equal("Pizza", commands[0].Text);
|
||||
Assert.Equal("Pasta", commands[1].Text);
|
||||
Assert.Equal("Salad", commands[2].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_Foreach_EmptyArray()
|
||||
{
|
||||
var commands = _engine.Render("""
|
||||
<receipt>
|
||||
<foreach items="Items" var="item">
|
||||
<line>{{item.Name}}</line>
|
||||
</foreach>
|
||||
</receipt>
|
||||
""",
|
||||
"""{"Items":[]}""", 42, 22);
|
||||
|
||||
Assert.Empty(commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_Foreach_WithLoopVariables()
|
||||
{
|
||||
var commands = _engine.Render("""
|
||||
<receipt>
|
||||
<foreach items="Items" var="item">
|
||||
<line>{{$index}}: {{item.Name}}</line>
|
||||
</foreach>
|
||||
</receipt>
|
||||
""",
|
||||
"""{"Items":[{"Name":"A"},{"Name":"B"},{"Name":"C"}]}""",
|
||||
42, 22);
|
||||
|
||||
Assert.Equal(3, commands.Count);
|
||||
Assert.Equal("0: A", commands[0].Text);
|
||||
Assert.Equal("1: B", commands[1].Text);
|
||||
Assert.Equal("2: C", commands[2].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_NestedForeach()
|
||||
{
|
||||
var commands = _engine.Render("""
|
||||
<receipt>
|
||||
<foreach items="Groups" var="group">
|
||||
<line>{{group.Name}}</line>
|
||||
<foreach items="group.Items" var="item">
|
||||
<line> {{item.Name}}</line>
|
||||
</foreach>
|
||||
</foreach>
|
||||
</receipt>
|
||||
""",
|
||||
"""{"Groups":[{"Name":"G1","Items":[{"Name":"A"},{"Name":"B"}]},{"Name":"G2","Items":[{"Name":"C"}]}]}""",
|
||||
42, 22);
|
||||
|
||||
Assert.Equal(5, commands.Count);
|
||||
Assert.Equal("G1", commands[0].Text);
|
||||
Assert.Equal(" A", commands[1].Text);
|
||||
Assert.Equal(" B", commands[2].Text);
|
||||
Assert.Equal("G2", commands[3].Text);
|
||||
Assert.Equal(" C", commands[4].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_If_True()
|
||||
{
|
||||
var commands = _engine.Render("""
|
||||
<receipt>
|
||||
<if test="Name">
|
||||
<line>Has name: {{Name}}</line>
|
||||
</if>
|
||||
</receipt>
|
||||
""",
|
||||
"""{"Name":"John"}""", 42, 22);
|
||||
|
||||
Assert.Single(commands);
|
||||
Assert.Equal("Has name: John", commands[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_If_False()
|
||||
{
|
||||
var commands = _engine.Render("""
|
||||
<receipt>
|
||||
<if test="Name">
|
||||
<line>Has name</line>
|
||||
</if>
|
||||
</receipt>
|
||||
""",
|
||||
"""{}""", 42, 22);
|
||||
|
||||
Assert.Empty(commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_IfElse_True()
|
||||
{
|
||||
var commands = _engine.Render("""
|
||||
<receipt>
|
||||
<if test="Name">
|
||||
<line>Yes</line>
|
||||
</if>
|
||||
<else>
|
||||
<line>No</line>
|
||||
</else>
|
||||
</receipt>
|
||||
""",
|
||||
"""{"Name":"John"}""", 42, 22);
|
||||
|
||||
Assert.Single(commands);
|
||||
Assert.Equal("Yes", commands[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_IfElse_False()
|
||||
{
|
||||
var commands = _engine.Render("""
|
||||
<receipt>
|
||||
<if test="Name">
|
||||
<line>Yes</line>
|
||||
</if>
|
||||
<else>
|
||||
<line>No</line>
|
||||
</else>
|
||||
</receipt>
|
||||
""",
|
||||
"""{}""", 42, 22);
|
||||
|
||||
Assert.Single(commands);
|
||||
Assert.Equal("No", commands[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_If_NegatedCondition()
|
||||
{
|
||||
var commands = _engine.Render("""
|
||||
<receipt>
|
||||
<if test="!$last">
|
||||
<cut />
|
||||
</if>
|
||||
</receipt>
|
||||
""",
|
||||
"""{}""", 42, 22);
|
||||
|
||||
// $last is not set so not a boolean false, but the variable doesn't exist
|
||||
// When there's no foreach context, $last is null, so !null = true
|
||||
Assert.Single(commands);
|
||||
Assert.True(commands[0].IsCut);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_Foreach_WithIfNotLast()
|
||||
{
|
||||
var commands = _engine.Render("""
|
||||
<receipt>
|
||||
<foreach items="Items" var="item">
|
||||
<line>{{item.Name}}</line>
|
||||
<if test="!$last">
|
||||
<separator />
|
||||
</if>
|
||||
</foreach>
|
||||
</receipt>
|
||||
""",
|
||||
"""{"Items":[{"Name":"A"},{"Name":"B"},{"Name":"C"}]}""",
|
||||
42, 22);
|
||||
|
||||
// A, sep, B, sep, C = 5
|
||||
Assert.Equal(5, commands.Count);
|
||||
Assert.Equal("A", commands[0].Text);
|
||||
Assert.Equal(new string('-', 42), commands[1].Text);
|
||||
Assert.Equal("B", commands[2].Text);
|
||||
Assert.Equal(new string('-', 42), commands[3].Text);
|
||||
Assert.Equal("C", commands[4].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_LineWithWrap()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"""<receipt><line wrap="true">The quick brown fox jumps over the lazy dog and more text here</line></receipt>""",
|
||||
"{}", 30, 15);
|
||||
|
||||
Assert.True(commands.Count > 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_LineSpacing()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"""<receipt><line lineSpacing="50">Text</line></receipt>""",
|
||||
"{}", 42, 22);
|
||||
|
||||
Assert.Equal(50, commands[0].SetLineSpacing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_FormatString()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"<receipt><line>{{Price:F2}}</line></receipt>",
|
||||
"""{"Price":9.5}""", 42, 22);
|
||||
|
||||
Assert.Equal("9.50", commands[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_DateTimeFormat()
|
||||
{
|
||||
var commands = _engine.Render(
|
||||
"<receipt><line>{{Date:dd.MM.yyyy}}</line></receipt>",
|
||||
"""{"Date":"2024-03-15T14:30:00Z"}""", 42, 22);
|
||||
|
||||
Assert.Equal("15.03.2024", commands[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_Row()
|
||||
{
|
||||
var commands = _engine.Render("""
|
||||
<receipt>
|
||||
<row>
|
||||
<col width="14">Left</col>
|
||||
<col width="14" align="center">Center</col>
|
||||
<col width="14" align="right">Right</col>
|
||||
</row>
|
||||
</receipt>
|
||||
""",
|
||||
"{}", 42, 22);
|
||||
|
||||
Assert.Single(commands);
|
||||
Assert.Equal(42, commands[0].Text.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_InvalidXml_ThrowsParsingException()
|
||||
{
|
||||
Assert.Throws<TemplateParsingException>(() =>
|
||||
_engine.Render("not xml", "{}", 42, 22));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_InvalidJson_ThrowsRenderingException()
|
||||
{
|
||||
Assert.Throws<TemplateRenderingException>(() =>
|
||||
_engine.Render("<receipt></receipt>", "not json", 42, 22));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_ComplexTemplate()
|
||||
{
|
||||
var template = """
|
||||
<receipt>
|
||||
<line align="center" big="true" red="true">Restaurant</line>
|
||||
<separator />
|
||||
<line />
|
||||
<line align="center">{{DateTime:dd-MMM-yy HH:mm}} Nr.:{{Number}}</line>
|
||||
<line align="center" big="true" bold="true">Tisch: {{Table}}</line>
|
||||
<separator />
|
||||
<foreach items="Items" var="item">
|
||||
<line bold="true">{{item.Qty}}x {{item.Name}}</line>
|
||||
</foreach>
|
||||
<separator />
|
||||
<columns left="Total:" right="{{Total:F2}} CHF" bold="true" />
|
||||
<cut />
|
||||
</receipt>
|
||||
""";
|
||||
|
||||
var json = """
|
||||
{
|
||||
"DateTime": "2024-03-15T14:30:00Z",
|
||||
"Number": "42",
|
||||
"Table": "5",
|
||||
"Items": [
|
||||
{"Qty": 2, "Name": "Margherita"},
|
||||
{"Qty": 1, "Name": "Tiramisu"}
|
||||
],
|
||||
"Total": 45.50
|
||||
}
|
||||
""";
|
||||
|
||||
var commands = _engine.Render(template, json, 42, 22);
|
||||
|
||||
Assert.True(commands.Count >= 10);
|
||||
|
||||
// Header
|
||||
Assert.True(commands[0].IsRed);
|
||||
Assert.True(commands[0].IsBig);
|
||||
Assert.Contains("Restaurant", commands[0].Text);
|
||||
|
||||
// Last command is cut
|
||||
Assert.True(commands[^1].IsCut);
|
||||
}
|
||||
}
|
||||
115
Inspectron.Epson.TemplateEngine.Tests/Templates/FinalReceipt.xml
Normal file
115
Inspectron.Epson.TemplateEngine.Tests/Templates/FinalReceipt.xml
Normal file
@@ -0,0 +1,115 @@
|
||||
<receipt>
|
||||
<line align="center">{{CompanyName}}</line>
|
||||
<line align="center">{{Address1}}</line>
|
||||
<line align="center">{{Address2}}</line>
|
||||
<line align="center">{{Phone}}</line>
|
||||
<line />
|
||||
<line />
|
||||
|
||||
<if test="IsDebtor">
|
||||
<line>Debitorenrechnung</line>
|
||||
</if>
|
||||
|
||||
<columns left="{{ReceiptLabel}}" right="{{DateTime:HH:mm dd.MM.yyyy}}" bold="true" />
|
||||
<line>Guests: {{Guests}}</line>
|
||||
<line />
|
||||
|
||||
<foreach items="Items" var="item">
|
||||
<columns left="{{item.Quantity}}x {{item.Description}}" right="{{item.PriceDisplay}}" wrap="true" />
|
||||
<foreach items="item.SubItems" var="sub">
|
||||
<line> - {{sub}}</line>
|
||||
</foreach>
|
||||
</foreach>
|
||||
|
||||
<line />
|
||||
<line align="right">---------</line>
|
||||
<line />
|
||||
|
||||
<line align="center" big="true" bold="true">Summe: {{Total:F2}} {{Currency}}</line>
|
||||
<line />
|
||||
|
||||
<if test="TotalInAlternateCurrency">
|
||||
<line align="right">{{TotalInAlternateCurrency:F2}} {{AlternateCurrency}}</line>
|
||||
<line />
|
||||
</if>
|
||||
|
||||
<if test="DiscountInfo">
|
||||
<columns left="{{DiscountInfo.Description}}" right="{{DiscountInfo.Amount:F2}} {{DiscountInfo.Currency}}" wrap="true" />
|
||||
<line />
|
||||
</if>
|
||||
|
||||
<foreach items="SplitPayments" var="sp">
|
||||
<line align="right">{{sp.PaymentMethod}}: {{sp.Amount:F2}} {{sp.Currency}}</line>
|
||||
</foreach>
|
||||
<if test="HasSplitPayments">
|
||||
<line />
|
||||
</if>
|
||||
|
||||
<columns left="{{PaymentMethod}}" right="{{PaymentAmount:F2}} {{Currency}}" bold="true" />
|
||||
<line />
|
||||
|
||||
<foreach items="TaxBreakdown" var="tax">
|
||||
<if test="$first">
|
||||
<row>
|
||||
<col width="10" align="left">MwSt %</col>
|
||||
<col width="10" align="right"> Brutto</col>
|
||||
<col width="10" align="right"> Netto</col>
|
||||
<col align="right">MwSt</col>
|
||||
</row>
|
||||
</if>
|
||||
<row>
|
||||
<col width="10" align="left">{{tax.Category}}:{{tax.Rate}}%</col>
|
||||
<col width="10" align="right">{{tax.Gross:F2}} {{tax.Currency}}</col>
|
||||
<col width="10" align="right">{{tax.Net:F2}} {{tax.Currency}}</col>
|
||||
<col align="right">{{tax.TaxAmount:F2}} {{tax.Currency}}</col>
|
||||
</row>
|
||||
</foreach>
|
||||
|
||||
<if test="!HasTaxableCategories">
|
||||
<line>Nicht mehrwertsteuerpflichtig</line>
|
||||
</if>
|
||||
|
||||
<line />
|
||||
|
||||
<columns left="Bedient von:" right="{{WaiterName}}" />
|
||||
<if test="Terminal">
|
||||
<columns left="Terminal:" right="{{Terminal}}" />
|
||||
</if>
|
||||
<columns left="Tisch:" right="{{TableNumber}}" />
|
||||
<line />
|
||||
<line />
|
||||
|
||||
<line align="center">{{VatNumber}}</line>
|
||||
<line />
|
||||
|
||||
<foreach items="TerminalReceipts" var="tr">
|
||||
<line align="center">{{tr.ReceiptType}}</line>
|
||||
<line align="center">{{tr.BookingType}}</line>
|
||||
<line align="center">{{tr.PaymentSystem}}</line>
|
||||
<line>{{tr.TransactionNumber}}</line>
|
||||
<columns left="{{tr.TransactionDateTime:dd.MM.yyyy}}" right="{{tr.TransactionDateTime:HH:mm:ss}}" />
|
||||
<columns left="Trm-Id:" right="{{tr.TerminalId}}" />
|
||||
<columns left="AID:" right="{{tr.AID}}" />
|
||||
<columns left="Trx. Seq-Cnt:" right="{{tr.TransactionSeqCount}}" />
|
||||
<columns left="Trx. Ref-No:" right="{{tr.TransactionRefNo}}" />
|
||||
<columns left="Auth. Code:" right="{{tr.AuthCode}}" />
|
||||
<columns left="Acq-Id:" right="{{tr.AcquirerId}}" />
|
||||
<columns left="EFT {{tr.Currency}}:" right="{{tr.EftAmount:F2}}" />
|
||||
<columns left="Trinkgeld {{tr.Currency}}:" right="{{tr.TipAmount:F2}}" />
|
||||
<columns left="Total-EFT {{tr.Currency}}:" right="{{tr.TotalEftAmount:F2}}" />
|
||||
<separator />
|
||||
<line />
|
||||
</foreach>
|
||||
|
||||
<line align="center">{{ThankYouMessage}}</line>
|
||||
<line align="center">{{GoodbyeMessageLine1}}</line>
|
||||
<line align="center">{{GoodbyeMessageLine2}}</line>
|
||||
|
||||
<if test="IsDebtor">
|
||||
<line />
|
||||
<line />
|
||||
<line />
|
||||
<separator />
|
||||
<line>Unterschrift</line>
|
||||
</if>
|
||||
</receipt>
|
||||
@@ -0,0 +1,65 @@
|
||||
<receipt>
|
||||
<line align="center" big="true" tall="true" red="true">{{Title}}</line>
|
||||
<separator />
|
||||
<line />
|
||||
|
||||
<line align="center">{{TransactionDateTime:dd-MMM-yy HH:mm}} Nr.:{{ReceiptNumber}}</line>
|
||||
<line align="center">{{WaiterName}}</line>
|
||||
<line align="center" big="true" bold="true">Tisch: {{TableNumber}}</line>
|
||||
|
||||
<if test="SpecialInstruction">
|
||||
<line />
|
||||
<line align="center" big="true" bold="true">{{SpecialInstruction}}</line>
|
||||
<line />
|
||||
</if>
|
||||
|
||||
<separator />
|
||||
|
||||
<foreach items="Gangs" var="gang">
|
||||
<line align="center" big="true" tall="true" red="true">{{gang.Id}}. {{gang.Name}}</line>
|
||||
<foreach items="gang.Dishes" var="dish">
|
||||
<line tall="true" wrap="true">{{dish.GuestPrefix}}{{dish.Number}}x {{dish.Name}}</line>
|
||||
<if test="dish.Modifications.HasModifications">
|
||||
<foreach items="dish.Modifications.Removed" var="removed">
|
||||
<line tall="true" bold="true"> - {{removed}}</line>
|
||||
</foreach>
|
||||
<foreach items="dish.Modifications.Added" var="added">
|
||||
<line tall="true" bold="true"> + {{added}}</line>
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="dish.Comment">
|
||||
<line tall="true" bold="true">Comment: {{dish.Comment}}</line>
|
||||
</if>
|
||||
</foreach>
|
||||
<if test="!$last">
|
||||
<cut />
|
||||
<line />
|
||||
<line />
|
||||
</if>
|
||||
</foreach>
|
||||
|
||||
<if test="HasGangs">
|
||||
<separator />
|
||||
</if>
|
||||
|
||||
<foreach items="Dishes" var="dish">
|
||||
<line tall="true" wrap="true">{{dish.GuestPrefix}}{{dish.Number}}x {{dish.Name}}</line>
|
||||
<if test="dish.Modifications.HasModifications">
|
||||
<foreach items="dish.Modifications.Removed" var="removed">
|
||||
<line tall="true" bold="true"> - {{removed}}</line>
|
||||
</foreach>
|
||||
<foreach items="dish.Modifications.Added" var="added">
|
||||
<line tall="true" bold="true"> + {{added}}</line>
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="dish.Comment">
|
||||
<line tall="true" bold="true">Comment: {{dish.Comment}}</line>
|
||||
</if>
|
||||
</foreach>
|
||||
|
||||
<if test="HasDishes">
|
||||
<line />
|
||||
<separator />
|
||||
<line />
|
||||
</if>
|
||||
</receipt>
|
||||
@@ -0,0 +1,34 @@
|
||||
<receipt>
|
||||
<table items="Items" var="item">
|
||||
<col width="8" align="center">Order:</col>
|
||||
<col align="center">QR:</col>
|
||||
<col width="12" align="center">{{DateTime:dd.MM.yyyy}}</col>
|
||||
</table>
|
||||
|
||||
<if test="ClientNotes">
|
||||
<line>|Client Notes: {{ClientNotes}}</line>
|
||||
</if>
|
||||
|
||||
<separator />
|
||||
|
||||
<foreach items="Items" var="item">
|
||||
<line />
|
||||
<columns left="{{item.Number}} {{item.Name}}" right="{{item.SizePart}}x{{item.Quantity}}" />
|
||||
<foreach items="item.SubItems" var="sub">
|
||||
<line> - {{sub}}</line>
|
||||
</foreach>
|
||||
<if test="item.Modifications.HasModifications">
|
||||
<line> Change of</line>
|
||||
<line> Ingredients:</line>
|
||||
<foreach items="item.Modifications.Removed" var="removed">
|
||||
<line> - {{removed}}</line>
|
||||
</foreach>
|
||||
<foreach items="item.Modifications.Added" var="added">
|
||||
<line> + {{added}}</line>
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="item.Comment">
|
||||
<line> SpecialInstruction: {{item.Comment}}</line>
|
||||
</if>
|
||||
</foreach>
|
||||
</receipt>
|
||||
Reference in New Issue
Block a user