Skip to content

Add HtmlRenderer.Test project, port applicable tests from PeachPDF - #262

Merged
eXpl0it3r merged 7 commits into
ArthurHub:masterfrom
jhaygood86:port-peachpdf-tests
Aug 27, 2026
Merged

Add HtmlRenderer.Test project, port applicable tests from PeachPDF#262
eXpl0it3r merged 7 commits into
ArthurHub:masterfrom
jhaygood86:port-peachpdf-tests

Conversation

@jhaygood86

@jhaygood86 jhaygood86 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports applicable tests from PeachPDF's test suite (PeachPDF forked from HTML-Renderer and has since built out a much larger feature set) into three MSTest projects, routed by what each test actually exercises:

  • HtmlRenderer.Test (new) — pure unit tests against the core library: CSS engine/parsing, DOM/layout utilities. Backed by a lightweight mock RAdapter/RGraphics test harness with no UI-framework dependency.
  • HtmlRenderer.IntegrationTest — end-to-end layout/paint behavior tests, using a similar harness backed by the real WinForms adapter.
  • HtmlRenderer.PdfSharp.Test — PDF-generation and PdfSharp-adapter tests.

This PR was opened when HTML-Renderer still had its old, much more limited hand-rolled CSS parser, so only ~41 of PeachPDF.Tests' ~483 files were portable at the time. Since then, HTML-Renderer absorbed a full CSS engine port from PeachPDF/ExCSS (Source/HtmlRenderer/Core/CssEngine/, a near-1:1 structural clone of PeachPDF's own CSS engine), which changed the picture substantially — most of PeachPDF.Tests/CSS/ tests the CSS parsing/CSSOM layer in isolation, and that layer is now close to feature-complete. This PR was updated to:

  1. Fix the ~20 files ported before the CSS engine landed, which broke against the new APIs (adapter signature drift, removed legacy parsing methods, a proprietary corner-radius mechanism replaced by real border-radius).
  2. Re-verify all 111 previously [Ignore("not yet spec compliant")] tests against the current codebase — 28 now genuinely pass (mostly CssLength unit-conversion coverage, several layout edge cases, and a couple of table visibility:collapse/vertical-align cases) and are un-ignored; the rest still fail for their originally-documented reasons, which the CSS engine port never touched (it replaced parsing, not the box/layout/paint engine).
  3. Re-triage and port the remaining ~86 files in PeachPDF.Tests/CSS/ against the new engine — the vast majority are now portable, since they test parsing/CSSOM in isolation rather than rendered behavior.

What's still excluded, and why: PeachPDF.Tests/Html/, Integration/, PdfSharpCore/, Svg/, and misc folders (~415 files) have not been re-triaged against the new engine in this pass — those test rendered behavior (layout/paint), and CSS-parsing support doesn't imply layout support. Confirmed directly: flexbox, grid, transform, box-shadow, and animations all parse correctly now but have zero consumption anywhere in the layout/paint engine (CssLayoutEngine.cs has no "flex"/"grid" references, no paint handler draws a shadow or applies a transform). border-radius is the one exception, genuinely painted end-to-end. Also still absent regardless of layer: WOFF/WOFF2 binary decoding, GCPM paged-media content functions (leader(), target-counter(), running(), etc.), SVG rendering, :has() with leading combinators, and font-variant granular sub-properties.

Where a ported test exercises a feature that does exist but isn't yet spec-compliant, it's still ported with its full original assertions intact — documenting the real target behavior — but marked [Ignore("not yet spec compliant")] with a doc comment citing the exact source evidence for the gap. This pass surfaced several new gaps this way: gap/row-gap/column-gap reject the normal keyword, font-weight only accepts the legacy CSS2.1 100-900 multiples (not the full [1,1000] range), GridTemplate/GridTrackSize have no value-equality, @supports/@container parse but are never consulted by the real per-box cascade, and cascade-layer precedence isn't implemented (rules in @layer are treated as ordinary unlayered rules).

Adds InternalsVisibleTo grants from HtmlRenderer/HtmlRenderer.WinForms/HtmlRenderer.PdfSharp to their respective test projects, matching the same grants PeachPDF's own .csproj already declares for PeachPDF.Tests. No behavioral changes to HTML-Renderer itself.

More tests will be ported incrementally as HTML-Renderer's layout/paint engine catches up to what the CSS engine can already parse, and as the remaining PeachPDF.Tests folders get their own re-triage pass.

Test plan

  • dotnet build Source/HtmlRenderer.sln (Release) — clean, 0 errors
  • dotnet test per project:
    • HtmlRenderer.Test: 2367 passed, 0 failed, 123 skipped (ignored, spec-compliance gaps)
    • HtmlRenderer.IntegrationTest (new tests): 92 passed, 0 failed, 74 skipped
    • HtmlRenderer.PdfSharp.Test: 15 passed, 0 failed
  • All 9 CI matrix jobs (Windows/Ubuntu/macOS × .NET 8/9/10) pass.

PeachPDF forked from HTML-Renderer and has since built out a much
larger CSS engine, SVG support, and PDF pipeline, with a large xUnit
test suite covering it. This adds a new HtmlRenderer.Test project
(MSTest, matching this repo's existing test style) and ports the
subset of PeachPDF.Tests that HTML-Renderer's current feature set can
actually exercise, routing tests to the appropriate project:

- HtmlRenderer.Test: CSS parsing/property and Core Dom/Utils unit
  tests, using a new lightweight mock RAdapter/RGraphics test harness
  (no dependency on any UI framework).
- HtmlRenderer.IntegrationTest: end-to-end layout/paint behavior
  tests, using a similar harness backed by the real WinForms adapter.
- HtmlRenderer.PdfSharp.Test: PDF-generation and PdfSharp-adapter
  tests.

Of the ~483 candidate test files, the large majority test PeachPDF
features HTML-Renderer doesn't have at all (its own CSS engine, SVG,
flexbox/grid, shadows/gradients, WOFF fonts, bidi text shaping, etc.)
and were left out. Where a ported test exercises a feature that does
exist in HTML-Renderer but whose current implementation isn't spec
compliant, the test is still ported (full assertions intact, so it
documents the real target behavior) but marked
[Ignore("not yet spec compliant")] rather than dropped or forced to
pass.

InternalsVisibleTo grants were added to HtmlRenderer/HtmlRenderer.WinForms/
HtmlRenderer.PdfSharp for their respective test projects, matching the
same grants PeachPDF's own csproj already declares for PeachPDF.Tests.

More tests will be ported incrementally from PeachPDF as HTML-Renderer
backports more of its standard/spec support.
dotnet test's underlying MSBuild VSTest target only accepts a single
project (MSB1008: Only one project can be specified), unlike dotnet
build. The non-Windows test step passed the *.Test.csproj glob
straight to dotnet test, which happened to work when only one project
matched it, but now that HtmlRenderer.Test.csproj also matches, dotnet
expands the glob to two paths and the step fails outright. Iterate and
test each matching project individually instead.
@eXpl0it3r
eXpl0it3r force-pushed the port-peachpdf-tests branch from 0571cb4 to 69f5ef0 Compare August 21, 2026 19:31
@eXpl0it3r

Copy link
Copy Markdown
Collaborator

Rebased onto master and removed Claude co-author attribution

Between opening this PR and now, HTML-Renderer's old hand-rolled CSS
parser/CssData was replaced by a real ExCSS-derived engine, and
RAdapter/RGraphicsPath/PdfGenerator gained new signatures (multi-stop
gradients, @font-face loading, elliptical ArcTo radii, async PDF
generation). Rebasing this branch onto that work left several ported
test files referencing now-removed APIs (CssParser.ParseCssBlock,
CssData.GetCssBlock/ContainsCssBlock, CssParser.ParseBorder, the
proprietary corner-radius/ActualCornerNw mechanism).

- MockAdapter/RecordingGraphics (both HtmlRenderer.Test and
  HtmlRenderer.IntegrationTest): implement the new
  CreateLinearGradientBrush(RPoint, RPoint, stops[]) and
  LoadFontFaceFontInt overloads, and the 5-arg ArcTo signature.
- PdfGeneratorTests: await the now-async PdfGenerator.GeneratePdf.
- The 7 CSS unit tests that parsed raw property strings directly now
  go through CssParser.ParseInlineStyle/IStyleRule for
  declaration-level checks, or the full LayoutHarness pipeline for
  cascade-level checks, matching how the rest of this port already
  verifies behavior.
- BorderRadiusIntegrationTests: rewritten against the new engine's
  real, spec-compliant border-radius properties
  (ActualBorderTopLeftRadiusX/Y etc.), replacing the now-removed
  proprietary corner-radius workaround this test previously had to use.

Since the real engine fixes several of the compliance gaps the
original port had to mark [Ignore("not yet spec compliant")] for,
those tests are un-ignored here (cascade specificity, media-query
not/only/comma-lists, several illegal-value rejections, all of
border-radius). One new regression the port surfaced (a NullReferenceException
in the new CSS-Nesting parser on malformed split <style> content) is
newly marked [Ignore] with the verified root cause, rather than fixed
here or silently dropped.
@jhaygood86
jhaygood86 force-pushed the port-peachpdf-tests branch from 24d0280 to bf2de64 Compare August 22, 2026 16:33
Re-ran all 111 previously [Ignore("not yet spec compliant")] tests
across HtmlRenderer.Test and HtmlRenderer.IntegrationTest against the
current codebase. 28 of them now genuinely pass:

- HtmlRenderer.Test: 17 of 26 (mostly CssLength unit-conversion/
  comparison-operator coverage, plus vertical-align keyword
  rejection).
- HtmlRenderer.IntegrationTest: 11 of 85 (float-adjacent placement/
  margin-collapse, table visibility:collapse edge cases, two CSS
  content-escaping/attribute-entity-decoding cases, vertical-align
  percentage/text-top metrics).

The remaining 83 still fail for their originally-documented reasons
(mostly CssBox/CssLayoutEngine layout-engine bugs unrelated to CSS
parsing, which this port never touched) and are left [Ignore]d with
their original, already-verified reasoning intact.
…engine

HTML-Renderer's CSS parsing has since been replaced by a full engine
port from PeachPDF/ExCSS (Source/HtmlRenderer/Core/CssEngine/, a
near-1:1 structural clone of PeachPDF's CSS engine). Re-triaged all 97
files in PeachPDF.Tests/CSS/ against the new engine and ported the ~86
files not already handled in the prior pass.

- Added Source/Test/HtmlRenderer.Test/CssEngineSupport/: helper
  classes (CssConstructionFunctions, TestExtensions,
  ObjectArrayComparer) mirroring PeachPDF's own test-construction
  helpers, adapted to the internal TheArtOfDev.HtmlRenderer.Core.CssEngine
  namespace (accessible via the existing InternalsVisibleTo grant).
- Added ~73 new test files under Source/Test/HtmlRenderer.Test/Css/
  (and a Css/Selectors/ subfolder) covering the CSS object model
  (stylesheet parsing, tokenization, selectors, colors, URLs),
  at-rules (@font-face, @Property, @container, @layer, @Keyframes,
  @supports, CSS nesting), grammar/value converters (aspect-ratio,
  basic-shape, box-shadow, calc(), gradients, grid), and the full
  property-test suite (flexbox, backgrounds, borders, fonts, columns,
  content, custom properties, etc.).
- Tests exercising features that still don't exist (GCPM paged-media
  content functions, SVG rendering, WOFF binary decoding, font-variant
  granular sub-properties, `:has()` leading combinators) are excluded
  entirely rather than ported. Tests exercising a real gap in an
  otherwise-working feature are ported with full original assertions
  intact but marked [Ignore("not yet spec compliant")], citing the
  exact source evidence - including several new gaps this pass found
  itself (missing `normal` keyword on gap/row-gap/column-gap,
  font-weight's CSS2.1-only 100-900 restriction, GridTemplate/
  GridTrackSize missing value-equality, @supports/@container never
  applied by the real per-box cascade, cascade-layer precedence not
  implemented).
- Also re-verified all 111 previously [Ignore]d tests against the
  current codebase: 28 now genuinely pass and are un-ignored (see the
  prior commit), the rest still fail for their originally-documented
  layout-engine reasons, which the CSS engine port never touched.

HtmlRenderer.Test: 2367 passed, 0 failed, 123 skipped.
Comment thread Source/HtmlRenderer.PdfSharp/HtmlRenderer.PdfSharp.csproj Outdated

@eXpl0it3r eXpl0it3r left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather verbose at times and quite a few ignored tests, but it's a good foundation to optimize on later

@eXpl0it3r
eXpl0it3r merged commit 2da9762 into ArthurHub:master Aug 27, 2026
9 checks passed
@eXpl0it3r

Copy link
Copy Markdown
Collaborator

Thank you for porting all these tests! 🎉

jhaygood86 added a commit to jhaygood86/HTML-Renderer that referenced this pull request Aug 29, 2026
…ted CssBox.Paint

PR ArthurHub#262's PaintHarness.PaintBox (merged into master while this branch was in
flight) called box.Paint(g) directly - the old live-tree paint entry point.
This branch's own F3 stage later deleted CssBox.Paint/PaintImp entirely once
FragmentPainter became the sole paint path, so rebasing this branch onto
master (which now carries PaintHarness) fails to compile: git's line-based
merge can't catch an API a fresh commit calls having been removed by an
earlier one in this branch's own history.

Fix: PaintHarness.PaintBox now locates the CssBox's own BoxFragment in
container.FragmentTree (searching every fragmentainer, in case the box was
relocated onto a later page) and paints it via a new FragmentPainter test
entry point, FragmentPainter.PaintFragmentSubtree - a thin wrapper that sets
the painter's band-top before delegating to its existing private PaintFragment,
letting a test get "just this box's draw calls" without duplicating the real
paint walk. This is the same widen-for-test-use precedent already applied to
CssBox.PaintBackground/PaintWord/PaintDecoration.

Full three-project suite passes after the rebase: HtmlRenderer.Test (2367),
HtmlRenderer.IntegrationTest (146), HtmlRenderer.PdfSharp.Test (30) - 0 failed.
jhaygood86 added a commit to jhaygood86/HTML-Renderer that referenced this pull request Aug 29, 2026
Ports PeachPDF.Tests/Integration/{RepeatedTableHeaderClipIntegrationTests,
RepeatingTableRelayoutTests,WholeTableRelocationTests,TableRowspanContinuationTests,
TableSpannedBandRepetitionTests,TableRepeatedGroupConditionsTests,
TableRowBreakValueTests,PageBreakTableKeepWithNextIntegrationTests,
BreakValueCascadeTests,StructuralCloneBreakValueBehaviourTests}.cs into
Source/Test/HtmlRenderer.IntegrationTest/Tables/, and revises the existing
PageBreakTableIntegrationTests.cs (an earlier ArthurHub#262 port, written before this
branch's own css-tables-3 6.1 row-preservation-by-default landed in 362dee9) to
match that table's now-current mechanics.

Two real, confirmed production bugs were found and fixed, not just documented:

1. CssBoxProperties.InheritStyle's "everything: true" branch (structural-clone
   copying) never copied break-before/break-after/break-inside, so both of its
   real callers - TableHeaderRepeat.CloneSubtree's repeated <thead> row clones,
   and DomParser.CorrectBlockSplitBadBox's block-in-inline split - silently
   produced auto/auto/auto clones regardless of what the source element
   declared. Fixed by adding the three fields to that branch.

2. CssLayoutEngineTable.LayoutCells's repeated-header loop seeded its own
   lastRepeatSlot from PageIndexOf(starty) - and for a border-collapse:collapse
   table (GetVerticalSpacing() is -1, a deliberate one-pixel row/border overlap)
   sitting flush at a page's own content top, starty lands one pixel below
   PageIndexOf's slot boundary, flooring into the slot BEFORE the one the table
   actually starts in. This made the loop see a spurious "transition" at the
   very first body row, painting the header twice on the table's own first page
   (confirmed empirically: two HEADERMARKER draws at nearly the same position)
   while, in a combined effect, one repeat later in the document went missing.
   Fixed with a new PageSlotOf helper that clamps to CssBox.ClientTop (immune to
   the collapsed-border overlap) - scoped to this loop alone, not the row-shift
   straddle check a few lines below, which reacts to the same misread with a
   harmless 1px nudge two existing tests depend on (see PageSlotOf's own
   remarks). CssLayoutEngineTablePageBreakTests.RepeatedThead_SinglePageBorder
   CollapseTable_PhantomHeaderRepeatDueToNegativeSlotRounding, Ignored since
   Batch 1 with this exact symptom already documented, is un-Ignored - it now
   passes.

Ported/revised, with test counts:
- RepeatedTableHeaderClipIntegrationTests (3/3 port) - a repeated header's
  overflow:hidden clip resolves correctly per-page by construction here (each
  repeat is an independent, already-positioned CssBox clone, not PeachPDF's
  shared-subtree-plus-proxy architecture); regression pins, not bug repros.
- RepeatingTableRelayoutTests (2/2 port, 4 cases) - a relocated card holding a
  repeating-header table is genuinely relaid out, not translated; the table's
  own RepeatedHeaderRows-reset-per-pass makes PeachPDF's excluding bug moot here.
- WholeTableRelocationTests (7/7 port) - adapted off PerformLayoutEpilogue onto
  the real trigger, BlockFragmentation.RelocateIfNeeded, called from the block
  child loop after a table finishes its own layout.
- TableRowspanContinuationTests (6 ported + 1 Ignored, down from 17) - PeachPDF's
  TableRowCursor/PageBreakBottoms/ShellIn/box-decoration-break-edge machinery has
  no counterpart; ported what's real here instead (ActualBottom extension on a
  rowspan cell whose ending row is shifted, deterministically). Also surfaces an
  unrelated, pre-existing InsertEmptyBoxes gap (a rowspan in a column past the
  ending row's own last existing cell gets no placeholder there at all, matching
  PeachPDF's own issue #522, not fixed here) - fixtures route around it.
- TableSpannedBandRepetitionTests (4 ported, down from 13, tfoot half dropped) -
  BoxFragment.OverflowClip is always null here (no room-reservation/slicing
  mechanism exists), so PeachPDF's strip/confinement assertions have nothing to
  port onto; pins the real, already-documented "only the first band a lone tall
  row overflows onto gets a repeat" limitation instead.
- TableRepeatedGroupConditionsTests (5 ported, down from 16, tfoot half and the
  print-media-only UA-stylesheet tests dropped) - the quarter-of-page-height cap
  (css-tables-3 6.2's second condition) is confirmed unimplemented; pinned as an
  inverted, real test rather than force-fit.
- TableRowBreakValueTests (1 real + 4 Ignored) - CssLayoutEngineTable.cs never
  reads BreakBefore/BreakAfter anywhere; forced row breaks don't exist.
- PageBreakTableIntegrationTests (revised in place) - un-Ignored
  SingleRowTable_CrossingPageBoundary_IsMovedToNextPage with a corrected
  assertion (row preservation shifts the straddling row's cells, never the
  table's own outer Location - the original assertion checked the wrong
  property); corrected the other two Ignore reasons to cite the real, current
  gaps (freely-fragmentable-via-width carve-out; EnforceKeepWithNext reading the
  table's own EffectiveTop, untouched by an internal row-shift) instead of a
  BreakPage() method that no longer exists anywhere in the source.
- PageBreakTableKeepWithNextIntegrationTests (3 real + 2 Ignored, dropped the
  3-way composition test) - PeachPDF's "Gap 1" does not reproduce here at all
  (BlockFragmentation.EnforceKeepWithNext is unconditional, not table-specific);
  "Gap 2"'s shape does, for a different, more fundamental reason (row
  preservation moves cells, never the table's own box - same fact as above).
- BreakValueCascadeTests (9/9 port, adapted to hold break-inside:avoid fixed on
  the source and vary before/after, since the print-media UA default doesn't
  apply under this harness) and StructuralCloneBreakValueBehaviourTests (5/5
  port) - characterize the InheritStyle fix's own real effect: correctly
  carried now, but still inert for both clone sites (detached header clones
  never reached by BlockFragmentation; the block-in-inline split's anonymous
  wrapper still breaks the sibling chain a following box would read).

Full suite: HtmlRenderer.Test 2427 passed/128 skipped/0 failed (was 2426/129 -
one previously-Ignored test now passes), HtmlRenderer.IntegrationTest 323
passed/101 skipped/0 failed (was 275/95), HtmlRenderer.PdfSharp.Test 30
passed/0 skipped/0 failed (unchanged). Clean build across net8.0/
netstandard2.0/net462, 1 pre-existing warning (unrelated nullable-dereference).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants