Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/dependabot-commit-lowercase.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
name: Fix Dependabot Commit Casing

on:
pull_request:
pull_request_target:
types: [opened, edited]

permissions:
pull-requests: write

jobs:
fix-commit-description:
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Make commit description commitlint compliant
uses: actions/github-script@v7
uses: actions/github-script@v9
with:
script: |
const title = context.payload.pull_request.title;
Expand Down
171 changes: 171 additions & 0 deletions Panels.Tests/JsonParserTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
using System.Reflection;

Check failure on line 1 in Panels.Tests/JsonParserTests.cs

View workflow job for this annotation

GitHub Actions / checks (10.0)

Fix whitespace formatting.
using Newtonsoft.Json;
using Panels;
using Panels.Configuration;
using Panels.Elements;

namespace Panels.Tests;

public class JsonParserTests
{
private static string ProjectRoot =>
Directory.GetParent(Environment.CurrentDirectory)?.Parent?.Parent?.FullName
?? throw new InvalidOperationException("Project root not found");

[Fact]
public void Read_loads_comic_from_file()
{
var jsonPath = Path.Combine(ProjectRoot, "Snapshot/testjson01/bd.json");

var comic = JsonParser.Read(jsonPath);

Assert.Equal(10, comic.FontSize);
Assert.Equal(2, comic.children.Count);
Assert.IsType<Slot>(comic.children[0]);
}

[Fact]
public void ReadFromString_deserializes_typed_children()
{
const string json = """
{
"fontSize": 12,
"children": [
{
"$type": "slot",
"panels": [
{
"image": "0000.png",
"elements": [
{ "$type": "text", "text": "Hello" },
{ "$type": "description", "text": "Alt text" }
]
}
]
},
{ "$type": "newpage" }
]
}
""";

var comic = JsonParser.ReadFromString(json);

Assert.Equal(12, comic.FontSize);
Assert.Equal(2, comic.children.Count);
Assert.IsType<Slot>(comic.children[0]);
Assert.IsType<NewPage>(comic.children[1]);

var slot = (Slot)comic.children[0];
Assert.Single(slot.panels);
Assert.Equal(2, slot.panels[0].elements.Count);
Assert.IsType<Text>(slot.panels[0].elements[0]);
Assert.IsType<Description>(slot.panels[0].elements[1]);
}

[Fact]
public void Write_round_trips_comic_through_file()
{
var sourcePath = Path.Combine(ProjectRoot, "Snapshot/testjson01/bd.json");
var outputPath = Path.Combine(Path.GetTempPath(), $"panels-json-{Guid.NewGuid():N}.json");

try
{
var comic = JsonParser.Read(sourcePath);
JsonParser.Write(comic, outputPath);

var roundTrip = JsonParser.Read(outputPath);

Assert.Equal(comic.FontSize, roundTrip.FontSize);
Assert.Equal(comic.MarginTop, roundTrip.MarginTop);
Assert.Equal(comic.RowsPerPage, roundTrip.RowsPerPage);
Assert.Equal(comic.children.Count, roundTrip.children.Count);
}
finally
{
File.Delete(outputPath);
}
}

[Fact]
public void ReadFromString_throws_when_json_is_null()
{
var exception = Assert.Throws<InvalidOperationException>(() => JsonParser.ReadFromString("null"));
Assert.Equal("Failed to deserialize JSON", exception.Message);
}

[Fact]
public void ReadFromString_throws_when_type_is_unknown()
{
const string json = """
{
"$type": "unknown",
"children": []
}
""";

var exception = Assert.Throws<JsonSerializationException>(() => JsonParser.ReadFromString(json));
Assert.IsType<InvalidOperationException>(exception.InnerException);
Assert.Contains("Type not found: unknown", exception.InnerException!.Message);
}

[Fact]
public void CustomSerializationBinder_uses_short_names_for_known_types()
{
var binder = CreateSerializationBinder();
InvokeBindToName(binder, typeof(Slot), out var assemblyName, out var typeName);

Assert.Null(assemblyName);
Assert.Equal("slot", typeName);
}

[Fact]
public void CustomSerializationBinder_falls_back_to_full_name_for_unknown_types()
{
var binder = CreateSerializationBinder();
InvokeBindToName(binder, typeof(string), out var assemblyName, out var typeName);

Assert.Null(assemblyName);
Assert.Equal(typeof(string).FullName, typeName);
}

[Fact]
public void CustomSerializationBinder_resolves_known_type_names()
{
var binder = CreateSerializationBinder();
var bindToType = binder.GetType().GetMethod("BindToType")
?? throw new InvalidOperationException("BindToType not found");

var type = bindToType.Invoke(binder, new object?[] { null, "panel" });

Assert.Equal(typeof(Panel), type);
}

[Fact]
public void CustomSerializationBinder_resolves_assembly_qualified_type_names()
{
var binder = CreateSerializationBinder();
var bindToType = binder.GetType().GetMethod("BindToType")
?? throw new InvalidOperationException("BindToType not found");

var type = bindToType.Invoke(binder, new object?[] { typeof(string).Assembly.FullName, typeof(string).FullName });

Assert.Equal(typeof(string), type);
}

private static object CreateSerializationBinder()
{
var binderType = typeof(JsonParser).GetNestedType("CustomSerializationBinder", BindingFlags.NonPublic)
?? throw new InvalidOperationException("CustomSerializationBinder not found");
return Activator.CreateInstance(binderType)!;
}

private static void InvokeBindToName(object binder, Type serializedType, out string? assemblyName, out string? typeName)
{
var method = binder.GetType().GetMethod("BindToName")
?? throw new InvalidOperationException("BindToName not found");
var parameters = new object?[] { serializedType, null, null };
method.Invoke(binder, parameters);
assemblyName = (string?)parameters[1];
typeName = (string?)parameters[2];
}
}

Check failure on line 171 in Panels.Tests/JsonParserTests.cs

View workflow job for this annotation

GitHub Actions / checks (10.0)

Fix final newline. Delete 1 characters.
2 changes: 1 addition & 1 deletion Panels.Tests/Panels.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
<ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute</ExcludeByAttribute>
<ExcludeByFile>**/Properties/**</ExcludeByFile>
<Threshold>75</Threshold>
<Threshold>80</Threshold>
<ThresholdType>line</ThresholdType>
<ThresholdStat>total</ThresholdStat>
</PropertyGroup>
Expand Down
12 changes: 12 additions & 0 deletions Panels.Tests/Snapshot/Snapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ public Task Verify_testxml01(string configPath, string imagesPath)
return Verify(result, Settings);
}

[Theory]
[InlineData("Snapshot/testxml02/bd.xml", "Snapshot/testxml01/images")]
public Task Verify_testxml02_skipFirstRow(string configPath, string imagesPath)
{
var parentDirectory = Directory.GetParent(Environment.CurrentDirectory)?.Parent?.Parent?.FullName
?? throw new Exception("Parent directory not found");
var fullXmlPath = Path.Combine(parentDirectory, configPath);
var fullImagesPath = Path.Combine(parentDirectory, imagesPath);
var result = Program.GeneratePdf(fullXmlPath, fullImagesPath, @"./output.pdf");
return Verify(result, Settings);
}

[Theory]
[InlineData("Snapshot/testyaml01/bd.yaml", "Snapshot/testjson01/images")]
public Task Verify_testyaml01(string configPath, string imagesPath)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Starting PDF generation...
IMAGE - 161.91457x231.33333 at 25, 546.6667
TEXT - Hello ! at 30, 320.33337 with width 151.91457
IMAGE - 160.86644x231.33333 at 196.91457, 546.6667
TEXT - Bonjour ! at 201.91457, 320.33337 with width 150.86644
IMAGE - 161.91457x231.33333 at 25, 788
TEXT - Hello ! at 30, 561.6667 with width 151.91457
IMAGE - 160.86644x231.33333 at 196.91457, 788
TEXT - Bonjour ! at 201.91457, 561.6667 with width 150.86644
Generated ./output.pdf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Starting PDF generation...
IMAGE - 160.9627x231.33333 at 25, 546.6667
TEXT - Hi, I'm the first panel of this comic. at 30, 320.33337 with width 150.9627
IMAGE - 160.9627x231.33333 at 25, 788
TEXT - Hi, I'm the first panel of this comic. at 30, 561.6667 with width 150.9627
Generated ./output.pdf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Starting PDF generation...
IMAGE - 160.9627x231.33333 at 25, 546.6667
TEXT - Hi, I'm the first panel of this comic. at 30, 320.33337 with width 150.9627
Generated ./output.pdf
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Starting PDF generation...
IMAGE - 161.91457x231.33333 at 25, 546.6667
TEXT - Hello ! at 30, 320.33337 with width 151.91457
IMAGE - 160.86644x231.33333 at 196.91457, 546.6667
TEXT - Bonjour ! at 201.91457, 320.33337 with width 150.86644
IMAGE - 161.91457x231.33333 at 25, 788
TEXT - Hello ! at 30, 561.6667 with width 151.91457
IMAGE - 160.86644x231.33333 at 196.91457, 788
TEXT - Bonjour ! at 201.91457, 561.6667 with width 150.86644
Generated ./output.pdf
18 changes: 18 additions & 0 deletions Panels.Tests/Snapshot/testxml02/bd.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<comic
version="1.0"
marginTop="64"
marginBottom="64"
marginLeft="25"
marginRight="25"
horizontalPanelSpacing="10"
verticalPanelSpacing="10"
rowsPerPage="3"
skipFirstRow="true"
>
<slot>
<panel image="0000.png">
<description text="Hi, I'm the first panel of this comic." />
</panel>
</slot>
</comic>
107 changes: 105 additions & 2 deletions Panels.Tests/XmlParserTests.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
using Panels.Configuration;
using System.Reflection;

Check failure on line 1 in Panels.Tests/XmlParserTests.cs

View workflow job for this annotation

GitHub Actions / checks (10.0)

Fix whitespace formatting.
using System.Xml.Schema;
using Panels;
using Panels.Configuration;

namespace Panels.Tests;

public class XmlParserTests
{
private static string ProjectRoot =>
Directory.GetParent(Environment.CurrentDirectory)?.Parent?.Parent?.FullName
?? throw new InvalidOperationException("Project root not found");

[Fact]
public void Read_allows_whitespace_inside_empty_elements()
{
Expand Down Expand Up @@ -31,4 +38,100 @@
File.Delete(xmlPath);
}
}
}

[Fact]
public void Read_deserializes_comic_attributes()
{
var xmlPath = Path.Combine(Path.GetTempPath(), $"panels-test-{Guid.NewGuid():N}.xml");
File.WriteAllText(xmlPath, """
<?xml version="1.0" encoding="UTF-8"?>
<comic
version="1.0"
fontSize="14"
rowsPerPage="4"
showPageNumbers="false"
skipFirstRow="true"
marginTop="32"
verticalPanelSpacing="8"
>
<slot maxPaddingLeft="10%" maxPaddingRight="15%">
<panel image="0000.png">
<text text="Hello" character="Alice" left="5" top="10" width="100" />
</panel>
</slot>
</comic>
""");

try
{
var comic = XmlParser.Read(xmlPath);

Assert.Equal(14, comic.FontSize);
Assert.Equal(4, comic.RowsPerPage);
Assert.False(comic.ShowPageNumbers);
Assert.True(comic.SkipFirstRow);
Assert.Equal(32, comic.MarginTop);
Assert.Equal(8, comic.VerticalPanelSpacing);
Assert.Single(comic.children);

var slot = Assert.IsType<Slot>(comic.children[0]);
Assert.Equal("10%", slot.MaxLeftPadding);
Assert.Equal("15%", slot.MaxRightPadding);
}
finally
{
File.Delete(xmlPath);
}
}

[Fact]
public void Read_loads_snapshot_config()
{
var xmlPath = Path.Combine(ProjectRoot, "Snapshot/testxml01/bd.xml");

var comic = XmlParser.Read(xmlPath);

Assert.Equal(3, comic.RowsPerPage);
Assert.Single(comic.children);
}

[Fact]
public void ComicsValidationEventHandler_logs_warnings_and_errors()
{
var handler = typeof(XmlParser).GetMethod(
"ComicsValidationEventHandler",
BindingFlags.Static | BindingFlags.NonPublic
) ?? throw new InvalidOperationException("Validation handler not found");

var warning = CreateValidationEventArgs(new XmlSchemaException("schema warning"), XmlSeverityType.Warning);
var error = CreateValidationEventArgs(new XmlSchemaException("schema error"), XmlSeverityType.Error);

using var console = new StringWriter();
var originalOut = Console.Out;
try
{
Console.SetOut(console);
handler.Invoke(null, new object?[] { null, warning });
handler.Invoke(null, new object?[] { null, error });
}
finally
{
Console.SetOut(originalOut);
}

var output = console.ToString();
Assert.Contains("WARNING: schema warning", output);
Assert.Contains("ERROR: schema error", output);
}

private static ValidationEventArgs CreateValidationEventArgs(XmlSchemaException exception, XmlSeverityType severity)
{
return (ValidationEventArgs)Activator.CreateInstance(
typeof(ValidationEventArgs),
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public,
binder: null,
args: new object[] { exception, severity },
culture: null
)!;
}
}

Check failure on line 137 in Panels.Tests/XmlParserTests.cs

View workflow job for this annotation

GitHub Actions / checks (10.0)

Fix final newline. Delete 2 characters.
Loading
Loading