Recipes for document generators
Three common setups, each with a first test, how to keep its output the same from run to run, and which profile to start with. Whatever writes your documents, the first snapshot is worth reading through before you accept it: it is the baseline every later change is measured against.
HTML to PDF with Playwright or Chromium
Chromium prints a page to PDF, and with tagging turned on it writes the structure tree from the HTML: headings,
paragraphs, lists, tables and images with their alt text. Playwright 1.42 and later take Tagged and Outline for
PdfAsync:
[Fact]
public async Task TrailGuide_ShouldMatchApprovedSnapshot_WhenPrintedFromHtml()
{
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.SetContentAsync(TrailGuideHtml);
await page.EvaluateAsync("document.fonts.ready");
var pdf = await page.PdfAsync(new PagePdfOptions { Format = "Letter", Tagged = true, Outline = true });
await Snapshot.VerifyPdfAsync(pdf);
}
Outline = true turns the headings into bookmarks. From the command line, headless Chrome takes --export-tagged-pdf
for the tags and --generate-pdf-document-outline for the bookmarks, with --print-to-pdf=guide.pdf.
To keep the output the same from run to run:
- Fonts. Load web fonts from files in your project with
@font-face, and wait fordocument.fonts.readybefore printing. A font the machine doesn't have is replaced by another, which changes every line that uses it; see Troubleshooting. - Dates and IDs. A date printed in the page, such as in a footer template, changes every day. Use
ScrubDates(), or pass a fixed date into the page in tests. The PDF's own creation and modification dates don't matter: the snapshot records only whether they're present. - The browser. Each Playwright version pins its own Chromium build, so upgrading Playwright is a generator upgrade: run the tests before and after.
Start with Use(PdfProfile.Accessibility) if the tags are what you want to guard, or PdfProfile.Content for
the text and structure alone. A Chromium upgrade can move lines by a point or two, so leave layout in (Strict) only
where the page's look is what you're testing, and pin the browser there.
In the first snapshot, check the structure section: each HTML heading, paragraph, list and table should be there as
an element. Text in the untagged section usually means tagging is off.
PDFsharp
PDFsharp 6 tags content through its UAManager. Content drawn between BeginElement and End is tagged, content
between BeginArtifact and End is an artifact, and anything else is untagged:
[Fact]
public async Task TrailGuide_ShouldMatchApprovedSnapshot_WhenDrawnWithPdfSharp()
{
var document = new PdfDocument();
document.Info.Title = "Trail Guide";
var accessibility = UAManager.ForDocument(document);
accessibility.SetDocumentLanguage("en-US");
var tags = accessibility.StructureBuilder;
var page = XGraphics.FromPdfPage(document.AddPage());
tags.BeginElement(PdfBlockLevelElementTag.Heading1);
page.DrawString("Trail Guide", new XFont("Liberation Sans", 20), XBrushes.Black, 72, 72);
tags.End();
tags.BeginArtifact();
page.DrawString("Page 1", new XFont("Liberation Sans", 9), XBrushes.Black, 72, 760);
tags.End();
using var pdf = new MemoryStream();
document.Save(pdf);
await Snapshot.VerifyPdfAsync(pdf);
}
PDFsharp finds no fonts by itself on Linux, so set GlobalFontSettings.FontResolver to a resolver that returns font
files from your project. Every machine then draws with the same font, and the same document gives the same snapshot,
layout included, so Strict is a good start. MigraDoc draws through PDFsharp, so the same font resolver applies.
Text that shows up in the first snapshot's untagged section was drawn outside any element. PdfCheck.UntaggedContent
finds it too; see Check documents directly.
Word with the Open XML SDK
A document written with the Open XML SDK can be snapshotted straight from its stream:
[Fact]
public async Task TrailGuide_ShouldMatchApprovedSnapshot_WhenWrittenWithOpenXml()
{
using var docx = new MemoryStream();
using (var word = WordprocessingDocument.Create(docx, WordprocessingDocumentType.Document))
{
var main = word.AddMainDocumentPart();
var heading = new ParagraphProperties(new ParagraphStyleId { Val = "Heading1" });
main.Document = new Document(new Body(
new Paragraph(heading, new Run(new Text("Trail Guide"))),
new Paragraph(new Run(new Text("Start at the lodge.")))));
}
await Snapshot.VerifyDocxAsync(docx);
}
The DOCX snapshot doesn't print what changes from save to save without changing the document: editing-session IDs
(w:rsid...), relationship IDs, and the created and modified dates, which it records only as present. What's left to
keep steady is your own content: scrub dates and IDs your code writes into the text with ScrubDates() and
ScrubGuids().
Start with DocxProfile.Content to guard the text and its structure, or with no profile to include styles, fonts and
spacing. A document built on a template also snapshots the template's styles; leave them out with
Ignore(DocxSection.Styles) if the template is tested elsewhere. See Word documents.