feat(fp): WP-25 — server-rendered letter HTML preview
Adds LetterHtml.Render, a pure composer mirroring the FE letter canvas'
class vocabulary, behind two ExcludeFromDescription()'d endpoints
(GET /brief/preview, GET /admin/org-template/{subOrgId}/preview).
Auto-resolvable placeholders pull from seed/case data; unresolved
manual ones render as "[NOG IN TE VULLEN: label]". A sent brief
archives its composed HTML (BriefEntity.ArchivedHtml) so a later
org-template republish never changes it. FE gets a hand-written fetch
adapter (text/html, not JSON) and a "Voorbeeld" button that opens the
preview in a new tab.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
102
backend/tests/BigRegister.Tests/LetterHtmlTests.cs
Normal file
102
backend/tests/BigRegister.Tests/LetterHtmlTests.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using BigRegister.Api.Contracts;
|
||||
using BigRegister.Api.Data;
|
||||
using BigRegister.Domain.Letters;
|
||||
|
||||
namespace BigRegister.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// WP-25's 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");
|
||||
|
||||
[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 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user