Browser-derived layout fixtures in Taffy and Yoga (Chrome as reference oracle)
Document status:
verified. Canonical source.
Summary
Both Taffy and Yoga treat Chrome as the reference implementation of CSS Flexbox (and, for Taffy, Block and Grid). Each repository keeps HTML fixtures in which the tree structure and inline style attributes are the test input; a generator loads each fixture in headless Chrome through WebDriver, reads back the DOM geometry with getBoundingClientRect(), and emits unit tests whose expectations are the browser’s numbers. Taffy (Rust, scripts/gentest, fantoccini WebDriver client) downloads a Chrome for Testing build and matching ChromeDriver, measures each fixture under four variants (border-box/content-box × ltr/rtl), records unrounded, naively rounded and “smart” rounded layouts, and writes XML test descriptions that a Rust harness replays with a tolerance of 0.1 px. Yoga (TypeScript, gentest/src/cli.ts, selenium-webdriver) injects each fixture into a template that sets Yoga’s defaults as CSS, measures ltr and rtl trees with edges rounded to integers, and emits C++, Java and JavaScript tests asserting exact equality; generated files are signed and CI regenerates them to detect drift. Neither project uses a numeric tolerance against the browser beyond rounding; known divergences are handled by excluding or flagging fixtures rather than by relaxing assertions. Facts below were verified against the repositories at the commits stated.
Evidence
Taffy at commit b3b387132be1dda0e9d08d5044692236532c166d (2026-08-26, crate version 0.14.0), retrieved 2026-08-29:
- Generator dependencies:
fantoccini = "0.22.0"(WebDriver client), local crategetchrome,tokio,walkdir,xmlwriter,serde_json. Source:scripts/gentest/Cargo.toml. getchromedownloads the latest Stable “Chrome for Testing” browser and matching ChromeDriver fromgooglechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.jsoninto.chrome-for-testing/<version>; the Chrome version is not pinned in the repository (CHANNEL = "Stable"). Source:scripts/getchrome/src/lib.rs, lines 1-30 andscripts/getchrome/Cargo.toml.- Fixture discovery walks
test_fixtures/, skips_scratchdirectories and any file whose name starts withx. Source:scripts/gentest/src/main.rs, lines 37-49. At this commit there are 1533 HTML fixtures (block 235, blockflex 11, blockgrid 14, contain 8, flex 674, float 27, grid 543, gridflex 7, leaf 14) of which 17 arex-prefixed and excluded; the excluded files carry no explanatory comment (titles are the placeholder “Test description”). - Chrome is launched headless with
--headless --no-sandbox --disable-gpuand a per-run profile directory; ChromeDriver is started on a free port with 10 s timeouts. Source:main.rs, lines 197-284. - Before measuring, the generator asserts that scrollbars occupy space (15 px) and aborts otherwise; the stylesheet forces
::-webkit-scrollbar { width: 15px; height: 15px }and the comment states the width must match the test runner. Source:main.rs, lines 484-505;scripts/gentest/test_base_style.css. - Each fixture is loaded from a
file://URL, the load event is awaited, andgetTestData()is executed; it togglesbody.classNamethroughborder-box ltr,content-box ltr,border-box rtl,content-box rtland describes#test-rootunder each. Source:main.rs, lines 507-554;scripts/gentest/test_helper.js,getTestData, lines 1308-1321 of the concatenated listing (function at the end of the file). describeElementreads input styles from the inlinestyleobject (e.style.*), exceptboxSizinganddirection, which are read fromgetComputedStyle; grid template strings are passed through verbatim so that line names survive. Source:test_helper.js,describeElement.- Three geometry records are captured per element:
unroundedLayoutfromgetBoundingClientRect()withx/yrelative to the parent rectangle;naivelyRoundedLayoutfromoffsetWidth/offsetHeight/offsetLeft + parent.clientLeft;smartRoundedLayoutcomputed asMath.round(right) - Math.round(left)andMath.round(x - parent.x). The comment states that Chrome uses a smarter rounding algorithm but does not expose its output, so the script emulates Taffy’s algorithm. Source:test_helper.js,describeElement. - Text measurement: the Ahem font is embedded as a WOFF2 data URI; the
Xglyph is 10 × 10 px; zero-width spaces are used to control min-content and max-content;#test-rootsetsfont-family: ahem; line-height: 1; font-size: 10px. LeaftextContentis captured to drive measure functions. Source:test_base_style.css;test_helper.js. - Opt-out attributes:
data-test-rounding="false"disables rounding for a fixture (5 fixtures);data-test-resolved-track-lists="false"suppresses comparison of resolved grid track lists (26 fixtures), documented in the script as intended for “overlarge grids, where Taffy’s MAX_GRID_TRACKS clamp intentionally differs from Chrome’s track limit”. Source:test_helper.js,describeElement; counts fromgrepovertest_fixtures/. - Expectations use
smartRoundedLayoutwhen rounding is enabled andunroundedLayoutotherwise; scroll sizes are recorded only for scroll containers asscrollWidth - naive clientWidthfloored at zero; resolved grid rows/columns are recorded from computed style. Source:main.rs,generate_assertions, lines 586-628. - Output: one XML file per variant under
tests/xml/<family>/<name>__{border_box,content_box}_{ltr,rtl}.xml(6064 files at this commit, equal to (1533 − 17) × 4) plus a generatedtests/xml/mod.rswith one#[test]per file, gated by#[cfg(feature = "grid")]for grid names. Source:main.rs, lines 95-152. - Comparison: the harness constructs a
TaffyTree, enables or disables rounding from theuse-roundingattribute, sets available space from<viewport>, and comparesx,y,width,heightwithabs() < 0.1; scroll sizes likewise; resolved track lists compare line names exactly and track sizes with< 0.1, with the comment that Chrome and Taffy “format/round subpixel used sizes slightly differently”. Source:tests/xml.rs,impl PartialEq for OutputNodeandtrack_lists_match, lines 60-100 and 178-193. - Freshness gate: CI job “Generated Test Freshness” runs
cargo run -p gentestand fails ifgit status --porcelain -- tests/xmlis non-empty. Source:.github/workflows/ci.yml, lines 287-307. - Policy statements: “Flexbox layouts are tested by validating that layouts written in this crate perform the same as in Chrome”; generated tests must not be edited by hand. Source:
CONTRIBUTING.md, lines 26-31 (the text still namestests/generated, while the current output directory istests/xml). - Changelog entries record behaviour changes made to track Chrome: content alignment “updated to match the latest spec (and Chrome 123+)” and rounding “fixed … to follow latest Chrome”. Source:
CHANGELOG.md, lines 449 and 1137-1140.
Yoga at commit bd8fe0d6d243cc7e0334d4cc68864a994f63beae (2026-08-27), retrieved 2026-08-29:
- Dependencies:
selenium-webdriver ^4.16.0,signedsource ^2.0.0,minimist; scriptsgentest(runssrc/cli.ts) andgentest-validate. Source:gentest/package.json. A serial predecessor,gentest/gentest-driver.ts(single driver, LTR/RTL by textualstart/endsubstitution, expectations read from console logs), is still present but is not referenced bypackage.json;gentest/gentest.jsandgentest/gentest.rbreturn 404 on themainbranch. - Documentation: “Many of Yoga’s tests are automatically generated, using HTML fixtures … rendered in Chrome to generate an expected layout result”. Source:
README.md, lines 19-31. - Fixtures: 25 HTML files in
gentest/fixtures(for exampleYGAlignItemsTest.html,YGRoundingTest.html,YGIntrinsicSizeTest.html,YGBoxSizingTest.html,YGStaticPositionTest.html). Each top-level<div id="...">becomes one test named by its id. - Template:
gentest/test-template.htmlloads Ahem fromgentest/fonts/Ahem.ttf, setsbody { font: 10px/1 Ahem }, and gives everydiv, spanYoga’s defaults as CSS:box-sizing: border-box; position: relative; display: flex; flex-direction: column; align-items: stretch; align-content: flex-start; justify-content: flex-start; flex-shrink: 0; test roots are absolutely positioned. Source:gentest/test-template.html. - Browser: a pool (default 8) of headless Chrome sessions with
--force-device-scale-factor=1 --window-position=0,0 --hide-scrollbars --headless; the generator waits fordocument.fonts.readybefore measuring. Source:gentest/src/ChromePool.ts;gentest/src/cli.ts, lines 93-104. - Measurement:
buildLayoutTreesetsstyle.directiontoltrthenrtlon each test root, walks the DOM, and recordswidth = Math.round(rect.right) - Math.round(rect.left),heightlikewise,left = Math.round(rect.left - parentLeft),toplikewise, the originalstyleattribute string,data-experiments(space separated) anddata-disabled === 'true', andinnerTextfor leaves. Source:gentest/src/buildLayoutTree.ts, lines 40-77. - Style mapping:
CssToYoga.tsparses the inline style string (not computed style), expandsflex: Ntoflex-grow: N; flex-shrink: 1; flex-basis: 0%, and emits setter calls only for values that differ from Yoga defaults. Source:gentest/src/CssToYoga.ts,parseStyleAttribute,expandShorthand,applyStyles. - Emission: each fixture yields
tests/generated/<Name>.cpp(GoogleTest,ASSERT_FLOAT_EQ),java/tests/generated/com/facebook/yoga/<Name>.java(assertEquals(expected, actual, 0.0f)), andjavascript/tests/generated/<Name>.test.ts(expect(...).toBe(...)); each test computes layout withYGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR)and asserts left/top/width/height for every node, then repeats for RTL. Source:gentest/src/emitters/Emitter.ts,generateFixture, lines 126-160;tests/generated/YGAlignItemsTest.cpp, lines 16-54. - Known-bug handling:
data-disabled="true"emitsGTEST_SKIP();in C++ andtest.skipin JavaScript;data-experiments="Foo"emitsYGConfigSetExperimentalFeatureEnabled(config, YGExperimentalFeatureFoo, true). At this commit no fixture uses either attribute. Source:gentest/src/emitters/CppEmitter.ts, lines 91-112;JavascriptEmitter.ts, lines 136-150;grepovergentest/fixtures. - Rounding: Yoga rounds every node’s absolute left/top/right/bottom to the pixel grid with
pointScaleFactor(default 1.0) after layout, so integer expectations derived from rounded browser rectangle edges are comparable. Source:yoga/algorithm/PixelGrid.cpp,roundLayoutResultsToPixelGrid, lines 65-136;yoga/config/Config.h, line 80;yoga/algorithm/CalculateLayout.cpp, line 2938. - Integrity: generated files carry a
@generated SignedSource<<hash>>header;gentest-validate.tsverifies signatures; CI workflowvalidate-tests.ymlrunsyarn gentest-validateandyarn gentest -hand fails when regeneration modifies any test. Source:gentest/scripts/gentest-validate.ts;.github/workflows/validate-tests.yml, lines 23-33.
Mechanism
Common pipeline:
fixture.html (structure + inline styles)
→ headless Chrome via WebDriver (Ahem font, fixed defaults, scale factor 1)
→ per-element {input styles, getBoundingClientRect geometry}
→ emitter → unit tests in the engine's language
→ engine computes layout → assert per-node x, y, width, height
Rounding models differ:
Taffy smart rounding (per element, relative to parent rect):
width = round(right) − round(left)
x = round(x_abs − parent.x_abs)
Taffy comparison: |expected − actual| < 0.1 on x, y, w, h (and scroll sizes, track sizes)
Yoga (browser side): identical edge-rounding formula
Yoga (engine side): roundLayoutResultsToPixelGrid with pointScaleFactor = 1
Yoga comparison: exact equality (ASSERT_FLOAT_EQ / toBe / assertEquals delta 0)
Taffy captures both unrounded and rounded layouts so that a fixture can opt out of rounding (data-test-rounding="false") and be compared against unrounded floating-point Chrome values; Yoga has no unrounded mode.
Divergence handling is structural, not numeric: Taffy excludes fixtures by file-name prefix and suppresses particular assertions by attribute; Yoga skips tests or enables experimental features by attribute. Neither project stores the Chrome version used for generation in the generated artefacts; Taffy downloads the current Stable channel at generation time, and Yoga uses whatever Chrome is installed, so regenerating after a Chrome release can change expectations. Both projects rely on CI regeneration to detect such drift.
NUIF relevance
- Borrow: The fixture format (HTML tree with inline styles plus browser-measured expectations under a controlled font and scale factor) is directly reusable for nuif:experiment:layout-differential; NUIF flex/grid fixtures can be lowered to the same HTML and compared against both Chrome and Taffy without new tooling.
- Adapt: Taffy’s four-variant measurement (box-sizing × direction) is a tested baseline. Its 0.1 px value is only a safety ceiling in NUIF; the executable report derives and stores a smaller value independently for each fixture from the measured Taffy/browser delta.
- Borrow: The signed-artefact plus CI-regeneration pattern (Yoga) and the porcelain-status freshness gate (Taffy) are appropriate for NUIF conformance fixtures that are derived rather than authored.
- Adapt: NUIF resolved-layout snapshots must record the evaluation context fingerprint, including the reference browser version, since neither project pins it and expectations are known to move with Chrome releases (Taffy changelog, Chrome 123+ alignment change).
- Adapt: Rounding must be an explicit part of the NUIF layout family contract; Taffy’s separation of unrounded and edge-rounded geometry should be mirrored so that fixtures can assert either.
- Adapt: Divergence flags (
xprefix,data-test-*,data-disabled) are untyped; NUIF should replace them with the typed fidelity classes of spec/09 (evaluator bug, target semantic difference, schema loss) as required by the layout-differential experiment. - Reject: Treating Chrome as the sole ground truth is not acceptable for a vendor-neutral draft specification; NUIF fixtures should record specification citations and, where browsers disagree (see nuif:research:css-flexbox-grid-algorithm-specs), state which behaviour is normative for the NUIF family.
- Reject: Reading input styles from the inline
styleobject couples the fixtures to CSS syntax; NUIF fixtures should be authored in the NUIF layout vocabulary and lowered to CSS, not the reverse.
NUIF executable verification
cargo xtask gate-c now applies this method without hand-edited generated expectations. The lock file selects Chrome for Testing 152.0.7977.64 revision 1669021 and Taffy is exactly pinned to 0.14.0. The generator revision is the NUIF source revision recorded in the JSON report. One deterministic seed produces the v0 card at 360, 768 and 1,440 px plus 24 stack/flex/Grid cases; the harness retains all three box maps and compares NUIF/Taffy, NUIF/browser and Taffy/browser.
The 2026-08-30 strict run evaluated 27 cases, 81 engine pairs and 1,536 box components. Eight Grid cases cover positive fixed and zero-minimum fr tracks, sparse row/column flow, explicit placement and spanning items. The complete v0, stack, flex and bounded-Grid sets passed with zero classified, blocking or unexplained divergence. Twenty-six fixtures had exact Taffy/browser agreement; one fractional Grid fixture measured 0.015594482421875 px and therefore received the fixture-local 0.02 px assertion bound. Across every case, the maximum NUIF/Taffy delta was 0.00003051757818184342 px and the maximum NUIF/browser delta was 0.015625000000056843 px. The run first exposed fill lowering as auto under non-stretch Grid alignment; explicit justify-self/align-self: stretch fixed the foreign lowering without changing the normative evaluator.
This closes the bounded explicit-Grid implementation criterion. It does not claim the broader CSS Grid surface: intrinsic, percentage, named, repeated, implicit, subgrid and masonry tracks remain capability-reported exclusions. Text-dependent Grid layout is also outside this experiment; Gate D separately pins font and shaping inputs.
Open questions
- What is the empirical divergence between Chrome, Firefox and WebKit on the Taffy fixture corpus? The corpus is Chrome-only; running it through another browser would quantify how much “CSS-compatible” behaviour is Chrome-specific.
- Should NUIF pin a Chrome for Testing version per fixture generation and store it in the resolved snapshot’s context fingerprint?
- Text fixtures rely on Ahem; NUIF text pinning (nuif:experiment:text-pinning) needs an equivalent deterministic font for layout fixtures that include shaped text.
- Taffy’s 17 excluded
xfixtures are undocumented; their content (margins withstart/end, aspect-ratio stretch fills, grid fr spans) indicates areas where Taffy and Chrome disagree and could seed NUIF’s divergence catalogue.