The backend half of the sweep RD-18 did for the front end. git blame holds the provenance and stays correct when the code moves; the comment names a closed ticket and tells the reader nothing the sentence around it does not. public/letter.css and LetterHtml.golden.html change together, because the renderer inlines the CSS and the golden file snapshots the result. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
164 lines
5.9 KiB
C#
164 lines
5.9 KiB
C#
using System.Text.RegularExpressions;
|
|
using BigRegister.Api.Contracts;
|
|
using BigRegister.Api.Data;
|
|
using BigRegister.Domain.Letters;
|
|
|
|
namespace BigRegister.Tests;
|
|
|
|
/// <summary>
|
|
/// The fence against drift between the backend renderer and the FE letter
|
|
/// canvas: a golden-file snapshot of a fixed brief + template, and a class-parity
|
|
/// check that every `letter`-prefixed class the renderer emits exists in the
|
|
/// shared `public/letter.css` contract. Neither test launches a browser.
|
|
/// </summary>
|
|
public class LetterHtmlTests
|
|
{
|
|
private static BriefEntity FixtureBrief() => new()
|
|
{
|
|
BriefId = "golden-brief-1",
|
|
Owner = "golden",
|
|
Beroep = "arts",
|
|
TemplateId = "besluit-arts",
|
|
DrafterId = BriefStore.DrafterId,
|
|
Placeholders = new[]
|
|
{
|
|
new PlaceholderDefDto("naam_zorgverlener", "Naam zorgverlener", true),
|
|
new PlaceholderDefDto("datum", "Datum", true),
|
|
new PlaceholderDefDto("reden_besluit", "Reden besluit", false),
|
|
},
|
|
Sections = new()
|
|
{
|
|
new("aanhef", "Aanhef", true, new List<LetterBlockDto>
|
|
{
|
|
new("freeText", "aanhef-1", new RichTextBlockDto(new[]
|
|
{
|
|
new ParagraphDto(new[]
|
|
{
|
|
new RichTextNodeDto("text", Text: "Geachte heer/mevrouw "),
|
|
new RichTextNodeDto("placeholder", Key: "naam_zorgverlener"),
|
|
new RichTextNodeDto("text", Text: ","),
|
|
}),
|
|
})),
|
|
}, Locked: true),
|
|
new("kern", "Kern van het besluit", true, new List<LetterBlockDto>
|
|
{
|
|
new("freeText", "kern-1", new RichTextBlockDto(new[]
|
|
{
|
|
new ParagraphDto(new[] { new RichTextNodeDto("text", Text: "Op ") }),
|
|
new ParagraphDto(new RichTextNodeDto[]
|
|
{
|
|
new("text", Text: "Eerste punt: "),
|
|
new("placeholder", Key: "reden_besluit"),
|
|
}, List: "bullet"),
|
|
new ParagraphDto(new RichTextNodeDto[]
|
|
{
|
|
new("text", Text: "Tweede punt"),
|
|
}, List: "bullet"),
|
|
})),
|
|
}),
|
|
new("slot", "Slot", false, new List<LetterBlockDto>
|
|
{
|
|
new("freeText", "slot-1", new RichTextBlockDto(new[]
|
|
{
|
|
new ParagraphDto(new[] { new RichTextNodeDto("text", Text: "Met vriendelijke groet,") }),
|
|
})),
|
|
}, Locked: true),
|
|
},
|
|
Status = new BriefStatusDto("draft"),
|
|
};
|
|
|
|
private static readonly OrgTemplateDto Template = new(
|
|
"cibg-registers", "BIG-register",
|
|
"Retouradres: Postbus 00000, 2500 AA Den Haag",
|
|
LogoDocumentId: null,
|
|
"BIG-register · Postbus 00000, 2500 AA Den Haag · 070 000 00 00 · info@voorbeeld.example",
|
|
"Dit is een gegenereerd voorbeelddocument uit de register-reference PoC. Alle gegevens zijn fictief.",
|
|
"A. de Vries", "Hoofd Registratie, BIG-register", "Met vriendelijke groet,",
|
|
new MarginsDto(25, 20, 20, 25), Version: 1);
|
|
|
|
private const string At = "2026-07-05T12:00:00.0000000+00:00";
|
|
|
|
private static readonly string GoldenPath = Path.Combine(AppContext.BaseDirectory, "LetterHtml.golden.html");
|
|
|
|
// A minimal brief whose body renders the "datum" placeholder — the golden-file
|
|
// fixture above never uses it in the body, only in the letterhead, so it cannot
|
|
// exercise ResolveAuto's "datum" case (TE-007).
|
|
private static BriefEntity FixtureBriefWithDatumInBody() => new()
|
|
{
|
|
BriefId = "datum-brief-1",
|
|
Owner = "golden",
|
|
Beroep = "arts",
|
|
TemplateId = "besluit-arts",
|
|
DrafterId = BriefStore.DrafterId,
|
|
Placeholders = new[]
|
|
{
|
|
new PlaceholderDefDto("datum", "Datum", true),
|
|
},
|
|
Sections = new()
|
|
{
|
|
new("kern", "Kern van het besluit", true, new List<LetterBlockDto>
|
|
{
|
|
new("freeText", "kern-1", new RichTextBlockDto(new[]
|
|
{
|
|
new ParagraphDto(new[] { new RichTextNodeDto("placeholder", Key: "datum") }),
|
|
})),
|
|
}),
|
|
},
|
|
Status = new BriefStatusDto("draft"),
|
|
};
|
|
|
|
private static string ExtractLetterheadDate(string html) =>
|
|
Regex.Match(html, "<dt>Datum</dt><dd>([^<]+)</dd>").Groups[1].Value;
|
|
|
|
private static string ExtractBodyDatumParagraph(string html)
|
|
{
|
|
var bodyStart = html.IndexOf("<div class=\"letter__body\">", StringComparison.Ordinal);
|
|
var bodyEnd = html.IndexOf("<div class=\"letter__signature\">", StringComparison.Ordinal);
|
|
var body = html[bodyStart..bodyEnd];
|
|
return Regex.Match(body, "<p>([^<]+)</p>").Groups[1].Value;
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_matches_the_golden_file()
|
|
{
|
|
var html = LetterHtml.Render(FixtureBrief(), Template, At, watermark: true);
|
|
var golden = File.ReadAllText(GoldenPath);
|
|
Assert.Equal(golden, html);
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_resolves_the_body_datum_placeholder_from_the_given_at_not_the_wall_clock()
|
|
{
|
|
const string historicalAt = "2019-03-14T08:00:00.0000000+00:00";
|
|
|
|
var html = LetterHtml.Render(FixtureBriefWithDatumInBody(), Template, historicalAt, watermark: false);
|
|
|
|
Assert.Equal("14 maart 2019", ExtractBodyDatumParagraph(html));
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_keeps_the_letterhead_date_and_the_body_datum_in_agreement_for_a_historical_at()
|
|
{
|
|
// A historical `at` (an archive re-render, a back-dated letter) is the case
|
|
// where the letterhead and the body datum placeholder could disagree within
|
|
// one document, if the body still read the wall clock (TE-007).
|
|
const string historicalAt = "2019-03-14T08:00:00.0000000+00:00";
|
|
|
|
var html = LetterHtml.Render(FixtureBriefWithDatumInBody(), Template, historicalAt, watermark: false);
|
|
|
|
Assert.Equal(ExtractLetterheadDate(html), ExtractBodyDatumParagraph(html));
|
|
}
|
|
|
|
[Fact]
|
|
public void Every_letter_prefixed_class_exists_in_letter_css()
|
|
{
|
|
var html = LetterHtml.Render(FixtureBrief(), Template, At, watermark: true);
|
|
var classes = Regex.Matches(html, "class=\"([^\"]+)\"")
|
|
.SelectMany(m => m.Groups[1].Value.Split(' '))
|
|
.Where(c => c.StartsWith("letter"))
|
|
.Distinct();
|
|
Assert.NotEmpty(classes);
|
|
Assert.All(classes, c => Assert.Contains($".{c}", LetterHtml.StyleSheet));
|
|
}
|
|
}
|