diff --git a/crates/layout/src/bin/logical_properties_check.rs b/crates/layout/src/bin/logical_properties_check.rs new file mode 100644 index 00000000..6fe97389 --- /dev/null +++ b/crates/layout/src/bin/logical_properties_check.rs @@ -0,0 +1,1009 @@ +//! Logical properties (`margin-inline-start`, `padding-block`, `inline-size`, and the rest +//! of `css/css-logical`) measured against the three browsers (`specs/logical-properties.md`). +//! Stylo maps a logical property onto the physical property for the box's `writing-mode` +//! and `direction` at cascade time, and `crates/dom/src/computed.rs` reads only the +//! physical ones (`width`, `margin_left`, `padding_top`, `left`, `border_left_width`), so +//! the engine should already lay these out. Nothing in the repository measured it before +//! this binary, and the direction-dependent mapping is the part a mistake would hide in: +//! `margin-inline-start` is the left margin under `direction: ltr` and the right one under +//! `rtl`. +//! +//! This binary adds the measurement and nothing else. If the engine agrees with the +//! browsers, no engine code changes; if it does not, the disagreements are the finding and +//! fixing them belongs to a separate spec, since a fix would edit +//! `crates/dom/src/computed.rs` or `crates/layout/src/fragment.rs`, which the queued +//! generated-function specs edit and this binary's `touches` leaves alone. +//! +//! # Scope +//! +//! In: block-level, non-replaced boxes without text, in normal flow or absolutely +//! positioned, under `writing-mode: horizontal-tb` and both `direction: ltr` and `rtl` +//! (`crates/layout/src/width.rs` reads `direction`; the engine lays out no other writing +//! mode, so vertical modes would be a change to the layout substrate, not to this +//! feature). The longhands `margin-{inline,block}-{start,end}`, +//! `padding-{inline,block}-{start,end}`, `border-{inline,block}-{start,end}-width` (with a +//! `solid` style so the width applies), `inset-{inline,block}-{start,end}`, `inline-size` +//! and `block-size`; the two-value shorthands `margin-inline`, `margin-block`, +//! `padding-inline`, `padding-block`, `inset-inline`, `inset-block`; declarations that mix +//! logical and physical properties for the same side, where the later one wins. +//! +//! Out: `min-*`/`max-*-size` (the accessors for the block axis do not exist yet), +//! `border-*-color` and `border-*-style` (paint only), `float` and `clear` with +//! `inline-start`/`inline-end`, `text-align: start`, `overflow-inline`, `caption-side`, +//! `resize`, `contain-intrinsic-*` and the `border-*-radius` corner mappings. The 92 WPT +//! files under `css/css-logical` cover all of those, so the count says how large the +//! feature is, not how much of it this spec exercises. +//! +//! # Result +//! +//! Cases written: 150, three under `ltr` and three under `rtl` for each of the 25 groups +//! (`margin-inline-start`, `margin-inline-end`, `margin-block-start`, `margin-block-end`, +//! `padding-inline-start`, `padding-inline-end`, `padding-block-start`, `padding-block-end`, +//! `border-inline-start-width`, `border-inline-end-width`, `border-block-start-width`, +//! `border-block-end-width`, `inset-inline-start`, `inset-inline-end`, `inset-block-start`, +//! `inset-block-end`, `inline-size`, `block-size`, the six shorthands `margin-inline`, +//! `margin-block`, `padding-inline`, `padding-block`, `inset-inline` and `inset-block`, and +//! `mixed`). Every group's `rtl` cases include the inline-axis properties whose physical +//! twin is the opposite side, so the swap is in the corpus rather than only in the raw +//! values. +//! +//! All 75 `rtl` cases fail on one engine bug, and the `ltr` runs of the same documents pass: +//! it is the normal-flow block width equation, not the logical mapping. Stylo computes the +//! physical property correctly (`computed::margin_left` is `0` and `computed::margin_right` +//! is `7px` for `margin-inline-start: 7px` under `rtl`), but +//! `crates/layout/src/generated/width.rs` treats a box as over-constrained only when +//! `margin-left + width + margin-right` exceeds the containing block. CSS 2.1 10.3.3 makes +//! every all-non-auto width/margin combination over-constrained regardless of the sum, so +//! under `rtl` the browsers ignore `margin-left` and solve it (`#p` of `lp-mis-rtl-1` sits at +//! x=498, `#a` at x=674), while the engine keeps `margin-left: 0` (`#p` at x=0, `#a` at +//! x=1). +//! +//! The three `inset-block` `ltr` cases fail on a second bug, again outside `computed.rs`: +//! for an absolutely positioned box with a definite `top`, `height` and `bottom`, +//! `crates/layout/src/generated/absolute.rs` ignores `top` and solves it from `bottom`, so +//! `inset-block: 10px 20px` puts `#a` at y=130; the browsers ignore `bottom` and keep +//! `top`, y=10. +//! +//! The block below is this binary's stdout from a real lane run, pasted unchanged; +//! [`recorded_result_adds_up`] re-reads it from this module doc so the recorded numbers +//! cannot drift from each other, and the run needs no browser, so the test stage catches a +//! wrong record before review does. +//! +//! ```text +//! engines: chromium, firefox, webkit +//! cases written: 150 +//! +//! == summary == +//! dropped 0, compared 150, passed 72 +//! +//! == per-group and direction == +//! margin-inline-start/ltr 3/3 passed, 3 written +//! margin-inline-start/rtl 0/3 passed, 3 written +//! margin-inline-end/ltr 3/3 passed, 3 written +//! margin-inline-end/rtl 0/3 passed, 3 written +//! margin-block-start/ltr 3/3 passed, 3 written +//! margin-block-start/rtl 0/3 passed, 3 written +//! margin-block-end/ltr 3/3 passed, 3 written +//! margin-block-end/rtl 0/3 passed, 3 written +//! padding-inline-start/ltr 3/3 passed, 3 written +//! padding-inline-start/rtl 0/3 passed, 3 written +//! padding-inline-end/ltr 3/3 passed, 3 written +//! padding-inline-end/rtl 0/3 passed, 3 written +//! padding-block-start/ltr 3/3 passed, 3 written +//! padding-block-start/rtl 0/3 passed, 3 written +//! padding-block-end/ltr 3/3 passed, 3 written +//! padding-block-end/rtl 0/3 passed, 3 written +//! border-inline-start-width/ltr 3/3 passed, 3 written +//! border-inline-start-width/rtl 0/3 passed, 3 written +//! border-inline-end-width/ltr 3/3 passed, 3 written +//! border-inline-end-width/rtl 0/3 passed, 3 written +//! border-block-start-width/ltr 3/3 passed, 3 written +//! border-block-start-width/rtl 0/3 passed, 3 written +//! border-block-end-width/ltr 3/3 passed, 3 written +//! border-block-end-width/rtl 0/3 passed, 3 written +//! inset-inline-start/ltr 3/3 passed, 3 written +//! inset-inline-start/rtl 0/3 passed, 3 written +//! inset-inline-end/ltr 3/3 passed, 3 written +//! inset-inline-end/rtl 0/3 passed, 3 written +//! inset-block-start/ltr 3/3 passed, 3 written +//! inset-block-start/rtl 0/3 passed, 3 written +//! inset-block-end/ltr 3/3 passed, 3 written +//! inset-block-end/rtl 0/3 passed, 3 written +//! inline-size/ltr 3/3 passed, 3 written +//! inline-size/rtl 0/3 passed, 3 written +//! block-size/ltr 3/3 passed, 3 written +//! block-size/rtl 0/3 passed, 3 written +//! margin-inline/ltr 3/3 passed, 3 written +//! margin-inline/rtl 0/3 passed, 3 written +//! margin-block/ltr 3/3 passed, 3 written +//! margin-block/rtl 0/3 passed, 3 written +//! padding-inline/ltr 3/3 passed, 3 written +//! padding-inline/rtl 0/3 passed, 3 written +//! padding-block/ltr 3/3 passed, 3 written +//! padding-block/rtl 0/3 passed, 3 written +//! inset-inline/ltr 3/3 passed, 3 written +//! inset-inline/rtl 0/3 passed, 3 written +//! inset-block/ltr 0/3 passed, 3 written +//! inset-block/rtl 0/3 passed, 3 written +//! mixed/ltr 3/3 passed, 3 written +//! mixed/rtl 0/3 passed, 3 written +//! +//! == dropped == +//! none +//! +//! == failures == +//! lp-mis-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=673.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=673.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=673.0000px +//! lp-mis-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=658.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=658.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=658.0000px +//! lp-mis-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=691.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=691.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=691.0000px +//! lp-mie-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=673.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=673.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=673.0000px +//! lp-mie-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=658.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=658.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=658.0000px +//! lp-mie-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=691.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=691.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=691.0000px +//! lp-mbs-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px +//! lp-mbs-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px +//! lp-mbs-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px +//! lp-mbe-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px +//! lp-mbe-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px +//! lp-mbe-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px +//! lp-pis-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=470.0000px, sum |delta|=720.0000px | firefox: fail: 2 node(s) differ, worst |delta|=470.0000px, sum |delta|=720.0000px | webkit: fail: 2 node(s) differ, worst |delta|=470.0000px, sum |delta|=720.0000px +//! lp-pis-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=455.0000px, sum |delta|=705.0000px | firefox: fail: 2 node(s) differ, worst |delta|=455.0000px, sum |delta|=705.0000px | webkit: fail: 2 node(s) differ, worst |delta|=455.0000px, sum |delta|=705.0000px +//! lp-pis-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=490.0000px, sum |delta|=740.0000px | firefox: fail: 2 node(s) differ, worst |delta|=490.0000px, sum |delta|=740.0000px | webkit: fail: 2 node(s) differ, worst |delta|=490.0000px, sum |delta|=740.0000px +//! lp-pie-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=470.0000px, sum |delta|=720.0000px | firefox: fail: 2 node(s) differ, worst |delta|=470.0000px, sum |delta|=720.0000px | webkit: fail: 2 node(s) differ, worst |delta|=470.0000px, sum |delta|=720.0000px +//! lp-pie-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=455.0000px, sum |delta|=705.0000px | firefox: fail: 2 node(s) differ, worst |delta|=455.0000px, sum |delta|=705.0000px | webkit: fail: 2 node(s) differ, worst |delta|=455.0000px, sum |delta|=705.0000px +//! lp-pie-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=490.0000px, sum |delta|=740.0000px | firefox: fail: 2 node(s) differ, worst |delta|=490.0000px, sum |delta|=740.0000px | webkit: fail: 2 node(s) differ, worst |delta|=490.0000px, sum |delta|=740.0000px +//! lp-pbs-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-pbs-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-pbs-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-pbe-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-pbe-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-pbe-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-bis-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=490.0000px, sum |delta|=740.0000px | firefox: fail: 2 node(s) differ, worst |delta|=490.0000px, sum |delta|=740.0000px | webkit: fail: 2 node(s) differ, worst |delta|=490.0000px, sum |delta|=740.0000px +//! lp-bis-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=485.0000px, sum |delta|=735.0000px | firefox: fail: 2 node(s) differ, worst |delta|=485.0000px, sum |delta|=735.0000px | webkit: fail: 2 node(s) differ, worst |delta|=485.0000px, sum |delta|=735.0000px +//! lp-bis-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=495.0000px, sum |delta|=745.0000px | firefox: fail: 2 node(s) differ, worst |delta|=495.0000px, sum |delta|=745.0000px | webkit: fail: 2 node(s) differ, worst |delta|=495.0000px, sum |delta|=745.0000px +//! lp-bie-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=490.0000px, sum |delta|=740.0000px | firefox: fail: 2 node(s) differ, worst |delta|=490.0000px, sum |delta|=740.0000px | webkit: fail: 2 node(s) differ, worst |delta|=490.0000px, sum |delta|=740.0000px +//! lp-bie-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=485.0000px, sum |delta|=735.0000px | firefox: fail: 2 node(s) differ, worst |delta|=485.0000px, sum |delta|=735.0000px | webkit: fail: 2 node(s) differ, worst |delta|=485.0000px, sum |delta|=735.0000px +//! lp-bie-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=495.0000px, sum |delta|=745.0000px | firefox: fail: 2 node(s) differ, worst |delta|=495.0000px, sum |delta|=745.0000px | webkit: fail: 2 node(s) differ, worst |delta|=495.0000px, sum |delta|=745.0000px +//! lp-bbs-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-bbs-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-bbs-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-bbe-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-bbe-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-bbe-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-iis-rtl-1: chromium: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | firefox: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | webkit: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px +//! lp-iis-rtl-2: chromium: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | firefox: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | webkit: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px +//! lp-iis-rtl-3: chromium: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | firefox: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | webkit: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px +//! lp-iie-rtl-1: chromium: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | firefox: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | webkit: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px +//! lp-iie-rtl-2: chromium: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | firefox: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | webkit: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px +//! lp-iie-rtl-3: chromium: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | firefox: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | webkit: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px +//! lp-ibs-rtl-1: chromium: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | firefox: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | webkit: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px +//! lp-ibs-rtl-2: chromium: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | firefox: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | webkit: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px +//! lp-ibs-rtl-3: chromium: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | firefox: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | webkit: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px +//! lp-ibe-rtl-1: chromium: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | firefox: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | webkit: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px +//! lp-ibe-rtl-2: chromium: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | firefox: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | webkit: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px +//! lp-ibe-rtl-3: chromium: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | firefox: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px | webkit: fail: 1 node(s) differ, worst |delta|=500.0000px, sum |delta|=500.0000px +//! lp-is-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=398.0000px, sum |delta|=648.0000px | firefox: fail: 2 node(s) differ, worst |delta|=398.0000px, sum |delta|=648.0000px | webkit: fail: 2 node(s) differ, worst |delta|=398.0000px, sum |delta|=648.0000px +//! lp-is-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=398.0000px, sum |delta|=578.0000px | firefox: fail: 2 node(s) differ, worst |delta|=398.0000px, sum |delta|=578.0000px | webkit: fail: 2 node(s) differ, worst |delta|=398.0000px, sum |delta|=578.0000px +//! lp-is-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=398.0000px, sum |delta|=718.0000px | firefox: fail: 2 node(s) differ, worst |delta|=398.0000px, sum |delta|=718.0000px | webkit: fail: 2 node(s) differ, worst |delta|=398.0000px, sum |delta|=718.0000px +//! lp-bs-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=678.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=678.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=678.0000px +//! lp-bs-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=678.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=678.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=678.0000px +//! lp-bs-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=678.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=678.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=678.0000px +//! lp-mi-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=648.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=648.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=648.0000px +//! lp-mi-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=638.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=638.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=638.0000px +//! lp-mi-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=679.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=679.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=679.0000px +//! lp-mb-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px +//! lp-mb-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px +//! lp-mb-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px +//! lp-pi-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=460.0000px, sum |delta|=710.0000px | firefox: fail: 2 node(s) differ, worst |delta|=460.0000px, sum |delta|=710.0000px | webkit: fail: 2 node(s) differ, worst |delta|=460.0000px, sum |delta|=710.0000px +//! lp-pi-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=465.0000px, sum |delta|=715.0000px | firefox: fail: 2 node(s) differ, worst |delta|=465.0000px, sum |delta|=715.0000px | webkit: fail: 2 node(s) differ, worst |delta|=465.0000px, sum |delta|=715.0000px +//! lp-pi-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=472.0000px, sum |delta|=722.0000px | firefox: fail: 2 node(s) differ, worst |delta|=472.0000px, sum |delta|=722.0000px | webkit: fail: 2 node(s) differ, worst |delta|=472.0000px, sum |delta|=722.0000px +//! lp-pb-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-pb-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-pb-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=750.0000px +//! lp-ii-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=690.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=690.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=690.0000px +//! lp-ii-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=680.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=680.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=680.0000px +//! lp-ii-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=721.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=721.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=721.0000px +//! lp-ib-ltr-1: chromium: fail: 1 node(s) differ, worst |delta|=120.0000px, sum |delta|=120.0000px | firefox: fail: 1 node(s) differ, worst |delta|=120.0000px, sum |delta|=120.0000px | webkit: fail: 1 node(s) differ, worst |delta|=120.0000px, sum |delta|=120.0000px +//! lp-ib-ltr-2: chromium: fail: 1 node(s) differ, worst |delta|=120.0000px, sum |delta|=120.0000px | firefox: fail: 1 node(s) differ, worst |delta|=120.0000px, sum |delta|=120.0000px | webkit: fail: 1 node(s) differ, worst |delta|=120.0000px, sum |delta|=120.0000px +//! lp-ib-ltr-3: chromium: fail: 1 node(s) differ, worst |delta|=126.0000px, sum |delta|=126.0000px | firefox: fail: 1 node(s) differ, worst |delta|=126.0000px, sum |delta|=126.0000px | webkit: fail: 1 node(s) differ, worst |delta|=126.0000px, sum |delta|=126.0000px +//! lp-ib-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=620.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=620.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=620.0000px +//! lp-ib-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=620.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=620.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=620.0000px +//! lp-ib-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=626.0000px | firefox: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=626.0000px | webkit: fail: 2 node(s) differ, worst |delta|=500.0000px, sum |delta|=626.0000px +//! lp-mx-rtl-1: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=668.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=668.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=668.0000px +//! lp-mx-rtl-2: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=688.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=688.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=688.0000px +//! lp-mx-rtl-3: chromium: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | firefox: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px | webkit: fail: 2 node(s) differ, worst |delta|=498.0000px, sum |delta|=698.0000px +//! ``` + +use std::collections::{BTreeMap, HashMap}; +use std::path::PathBuf; + +use anyhow::{bail, Result}; +use dom::node::NodeId; +use dom::Document; +use harness::compare::compare; +use harness::dump::{Dump, Node as DumpNode, Rect, Viewport}; +use harness::oracle::Oracle; +use layout::boxtree::BoxTree; +use layout::fragment; + +/// Builds one case document from a stylesheet body and a body markup fragment. `concat!` +/// keeps the result a literal `&'static str` usable in a `const`, so every entry in +/// [`CASES`] is still a literal document while the boilerplate appears once. The reset is +/// spelled out here rather than pasted from [`RESET`] because `concat!` takes literals +/// only; [`every_document_opens_with_the_reset`] is what keeps the two spellings equal. +macro_rules! doc { + ($css:expr, $body:expr) => { + concat!( + "\n\n\n\n\n\n\n", + $body, + "\n\n\n" + ) + }; +} + +/// One case tuple: the direction is written into the `html` rule so that +/// [`direction_of`] can read it back off the literal document, which keeps the tuple the +/// three fields the spec asks for. +macro_rules! lp { + ($id:expr, $cat:expr, $dir:literal, $css:expr) => { + ( + $id, + $cat, + doc!( + concat!("html { direction: ", $dir, "; }\n", $css), + "
" + ), + ) + }; +} + +// The family macros below build one case document each, so the 150 literal documents in +// `CASES` stay one line apiece and the wrapper shape stays identical across a family. + +macro_rules! margin_inline { + ($id:expr, $cat:expr, $dir:literal, $prop:literal, $val:literal) => { + lp!($id, $cat, $dir, + concat!("#p { width: 300px; height: 100px; padding: 1px; }\n#a { ", $prop, ": ", $val, "; width: 100px; height: 30px; }")) + }; +} + +macro_rules! margin_block { + ($id:expr, $cat:expr, $dir:literal, $prop:literal, $val:literal) => { + lp!($id, $cat, $dir, + concat!("#p { width: 300px; height: 200px; padding: 1px; }\n#a { ", $prop, ": ", $val, "; width: 100px; height: 30px; }")) + }; +} + +macro_rules! padding_inline { + ($id:expr, $cat:expr, $dir:literal, $prop:literal, $val:literal) => { + lp!($id, $cat, $dir, + concat!("#p { width: 300px; height: 100px; ", $prop, ": ", $val, "; }\n#a { width: 50px; height: 30px; }")) + }; +} + +macro_rules! padding_block { + ($id:expr, $cat:expr, $dir:literal, $prop:literal, $val:literal) => { + lp!($id, $cat, $dir, + concat!("#p { width: 300px; height: 200px; ", $prop, ": ", $val, "; }\n#a { width: 50px; height: 30px; }")) + }; +} + +macro_rules! border_inline { + ($id:expr, $cat:expr, $dir:literal, $prop:literal, $val:literal) => { + lp!($id, $cat, $dir, + concat!("#p { width: 300px; height: 100px; border: 0 solid; ", $prop, ": ", $val, "; }\n#a { width: 50px; height: 30px; }")) + }; +} + +macro_rules! border_block { + ($id:expr, $cat:expr, $dir:literal, $prop:literal, $val:literal) => { + lp!($id, $cat, $dir, + concat!("#p { width: 300px; height: 200px; border: 0 solid; ", $prop, ": ", $val, "; }\n#a { width: 50px; height: 30px; }")) + }; +} + +macro_rules! inset_inline { + ($id:expr, $cat:expr, $dir:literal, $prop:literal, $val:literal) => { + lp!($id, $cat, $dir, + concat!("#p { position: relative; width: 300px; height: 200px; }\n#a { position: absolute; top: 0; ", $prop, ": ", $val, "; width: 60px; height: 50px; }")) + }; +} + +macro_rules! inset_block { + ($id:expr, $cat:expr, $dir:literal, $prop:literal, $val:literal) => { + lp!($id, $cat, $dir, + concat!("#p { position: relative; width: 300px; height: 200px; }\n#a { position: absolute; left: 0; ", $prop, ": ", $val, "; width: 60px; height: 50px; }")) + }; +} + +macro_rules! size_inline { + ($id:expr, $cat:expr, $dir:literal, $val:literal) => { + lp!($id, $cat, $dir, + concat!("#p { width: 400px; height: 200px; padding: 1px; }\n#a { inline-size: ", $val, "; block-size: 40px; }")) + }; +} + +macro_rules! size_block { + ($id:expr, $cat:expr, $dir:literal, $val:literal) => { + lp!($id, $cat, $dir, + concat!("#p { width: 300px; height: 300px; padding: 1px; }\n#a { inline-size: 120px; block-size: ", $val, "; }")) + }; +} + +/// The zeroing rule every document opens with, character for character the recipe corpus's +/// `RESET` (`crates/harness/src/cases.rs`), so a browser's UA stylesheet cannot leak body +/// margin into the comparison. Duplicated rather than imported because +/// `harness::cases::RESET` is `pub(crate)`. +const RESET: &str = + "* { margin: 0; padding: 0; border: 0; box-sizing: content-box; }\nhtml, body { display: block; }\n"; + +/// Every group [`CASES`] draws from, which is the spec's in-scope list. The test module +/// checks each one is used at least three times under each direction and that no case +/// names a group outside this list. +const CATEGORIES: &[&str] = &[ + "margin-inline-start", + "margin-inline-end", + "margin-block-start", + "margin-block-end", + "padding-inline-start", + "padding-inline-end", + "padding-block-start", + "padding-block-end", + "border-inline-start-width", + "border-inline-end-width", + "border-block-start-width", + "border-block-end-width", + "inset-inline-start", + "inset-inline-end", + "inset-block-start", + "inset-block-end", + "inline-size", + "block-size", + "margin-inline", + "margin-block", + "padding-inline", + "padding-block", + "inset-inline", + "inset-block", + "mixed", +]; + +/// (id, category, html). Literal documents rather than `harness::cases::Elem`, which is +/// `pub(crate)`. +/// +/// Every group gets three `ltr` cases and three `rtl` cases, so the direction dimension is +/// measured for every property rather than assumed. `margin-inline-start` under `rtl` +/// lands on `margin-right`, `margin-inline-end` on `margin-left`, `padding-inline-start` +/// on `padding-right`, `border-inline-start-width` on `border-right-width`, +/// `inset-inline-start` on `right` and `inset-inline-end` on `left`; the block-axis +/// properties keep their physical side under both directions, which the `rtl` cases make +/// visible beside the `ltr` ones. +const CASES: &[(&str, &str, &str)] = &[ + // -- margin-inline-start -- + margin_inline!("lp-mis-ltr-1", "margin-inline-start", "ltr", "margin-inline-start", "25px"), + margin_inline!("lp-mis-ltr-2", "margin-inline-start", "ltr", "margin-inline-start", "40px"), + margin_inline!("lp-mis-ltr-3", "margin-inline-start", "ltr", "margin-inline-start", "7px"), + margin_inline!("lp-mis-rtl-1", "margin-inline-start", "rtl", "margin-inline-start", "25px"), + margin_inline!("lp-mis-rtl-2", "margin-inline-start", "rtl", "margin-inline-start", "40px"), + margin_inline!("lp-mis-rtl-3", "margin-inline-start", "rtl", "margin-inline-start", "7px"), + // -- margin-inline-end -- + margin_inline!("lp-mie-ltr-1", "margin-inline-end", "ltr", "margin-inline-end", "25px"), + margin_inline!("lp-mie-ltr-2", "margin-inline-end", "ltr", "margin-inline-end", "40px"), + margin_inline!("lp-mie-ltr-3", "margin-inline-end", "ltr", "margin-inline-end", "7px"), + margin_inline!("lp-mie-rtl-1", "margin-inline-end", "rtl", "margin-inline-end", "25px"), + margin_inline!("lp-mie-rtl-2", "margin-inline-end", "rtl", "margin-inline-end", "40px"), + margin_inline!("lp-mie-rtl-3", "margin-inline-end", "rtl", "margin-inline-end", "7px"), + // -- margin-block-start -- + margin_block!("lp-mbs-ltr-1", "margin-block-start", "ltr", "margin-block-start", "25px"), + margin_block!("lp-mbs-ltr-2", "margin-block-start", "ltr", "margin-block-start", "40px"), + margin_block!("lp-mbs-ltr-3", "margin-block-start", "ltr", "margin-block-start", "7px"), + margin_block!("lp-mbs-rtl-1", "margin-block-start", "rtl", "margin-block-start", "25px"), + margin_block!("lp-mbs-rtl-2", "margin-block-start", "rtl", "margin-block-start", "40px"), + margin_block!("lp-mbs-rtl-3", "margin-block-start", "rtl", "margin-block-start", "7px"), + // -- margin-block-end -- + margin_block!("lp-mbe-ltr-1", "margin-block-end", "ltr", "margin-block-end", "25px"), + margin_block!("lp-mbe-ltr-2", "margin-block-end", "ltr", "margin-block-end", "40px"), + margin_block!("lp-mbe-ltr-3", "margin-block-end", "ltr", "margin-block-end", "7px"), + margin_block!("lp-mbe-rtl-1", "margin-block-end", "rtl", "margin-block-end", "25px"), + margin_block!("lp-mbe-rtl-2", "margin-block-end", "rtl", "margin-block-end", "40px"), + margin_block!("lp-mbe-rtl-3", "margin-block-end", "rtl", "margin-block-end", "7px"), + // -- padding-inline-start -- + padding_inline!("lp-pis-ltr-1", "padding-inline-start", "ltr", "padding-inline-start", "30px"), + padding_inline!("lp-pis-ltr-2", "padding-inline-start", "ltr", "padding-inline-start", "45px"), + padding_inline!("lp-pis-ltr-3", "padding-inline-start", "ltr", "padding-inline-start", "10px"), + padding_inline!("lp-pis-rtl-1", "padding-inline-start", "rtl", "padding-inline-start", "30px"), + padding_inline!("lp-pis-rtl-2", "padding-inline-start", "rtl", "padding-inline-start", "45px"), + padding_inline!("lp-pis-rtl-3", "padding-inline-start", "rtl", "padding-inline-start", "10px"), + // -- padding-inline-end -- + padding_inline!("lp-pie-ltr-1", "padding-inline-end", "ltr", "padding-inline-end", "30px"), + padding_inline!("lp-pie-ltr-2", "padding-inline-end", "ltr", "padding-inline-end", "45px"), + padding_inline!("lp-pie-ltr-3", "padding-inline-end", "ltr", "padding-inline-end", "10px"), + padding_inline!("lp-pie-rtl-1", "padding-inline-end", "rtl", "padding-inline-end", "30px"), + padding_inline!("lp-pie-rtl-2", "padding-inline-end", "rtl", "padding-inline-end", "45px"), + padding_inline!("lp-pie-rtl-3", "padding-inline-end", "rtl", "padding-inline-end", "10px"), + // -- padding-block-start -- + padding_block!("lp-pbs-ltr-1", "padding-block-start", "ltr", "padding-block-start", "30px"), + padding_block!("lp-pbs-ltr-2", "padding-block-start", "ltr", "padding-block-start", "45px"), + padding_block!("lp-pbs-ltr-3", "padding-block-start", "ltr", "padding-block-start", "10px"), + padding_block!("lp-pbs-rtl-1", "padding-block-start", "rtl", "padding-block-start", "30px"), + padding_block!("lp-pbs-rtl-2", "padding-block-start", "rtl", "padding-block-start", "45px"), + padding_block!("lp-pbs-rtl-3", "padding-block-start", "rtl", "padding-block-start", "10px"), + // -- padding-block-end -- + padding_block!("lp-pbe-ltr-1", "padding-block-end", "ltr", "padding-block-end", "30px"), + padding_block!("lp-pbe-ltr-2", "padding-block-end", "ltr", "padding-block-end", "45px"), + padding_block!("lp-pbe-ltr-3", "padding-block-end", "ltr", "padding-block-end", "10px"), + padding_block!("lp-pbe-rtl-1", "padding-block-end", "rtl", "padding-block-end", "30px"), + padding_block!("lp-pbe-rtl-2", "padding-block-end", "rtl", "padding-block-end", "45px"), + padding_block!("lp-pbe-rtl-3", "padding-block-end", "rtl", "padding-block-end", "10px"), + // -- border-inline-start-width -- + border_inline!("lp-bis-ltr-1", "border-inline-start-width", "ltr", "border-inline-start-width", "10px"), + border_inline!("lp-bis-ltr-2", "border-inline-start-width", "ltr", "border-inline-start-width", "15px"), + border_inline!("lp-bis-ltr-3", "border-inline-start-width", "ltr", "border-inline-start-width", "5px"), + border_inline!("lp-bis-rtl-1", "border-inline-start-width", "rtl", "border-inline-start-width", "10px"), + border_inline!("lp-bis-rtl-2", "border-inline-start-width", "rtl", "border-inline-start-width", "15px"), + border_inline!("lp-bis-rtl-3", "border-inline-start-width", "rtl", "border-inline-start-width", "5px"), + // -- border-inline-end-width -- + border_inline!("lp-bie-ltr-1", "border-inline-end-width", "ltr", "border-inline-end-width", "10px"), + border_inline!("lp-bie-ltr-2", "border-inline-end-width", "ltr", "border-inline-end-width", "15px"), + border_inline!("lp-bie-ltr-3", "border-inline-end-width", "ltr", "border-inline-end-width", "5px"), + border_inline!("lp-bie-rtl-1", "border-inline-end-width", "rtl", "border-inline-end-width", "10px"), + border_inline!("lp-bie-rtl-2", "border-inline-end-width", "rtl", "border-inline-end-width", "15px"), + border_inline!("lp-bie-rtl-3", "border-inline-end-width", "rtl", "border-inline-end-width", "5px"), + // -- border-block-start-width -- + border_block!("lp-bbs-ltr-1", "border-block-start-width", "ltr", "border-block-start-width", "10px"), + border_block!("lp-bbs-ltr-2", "border-block-start-width", "ltr", "border-block-start-width", "15px"), + border_block!("lp-bbs-ltr-3", "border-block-start-width", "ltr", "border-block-start-width", "5px"), + border_block!("lp-bbs-rtl-1", "border-block-start-width", "rtl", "border-block-start-width", "10px"), + border_block!("lp-bbs-rtl-2", "border-block-start-width", "rtl", "border-block-start-width", "15px"), + border_block!("lp-bbs-rtl-3", "border-block-start-width", "rtl", "border-block-start-width", "5px"), + // -- border-block-end-width -- + border_block!("lp-bbe-ltr-1", "border-block-end-width", "ltr", "border-block-end-width", "10px"), + border_block!("lp-bbe-ltr-2", "border-block-end-width", "ltr", "border-block-end-width", "15px"), + border_block!("lp-bbe-ltr-3", "border-block-end-width", "ltr", "border-block-end-width", "5px"), + border_block!("lp-bbe-rtl-1", "border-block-end-width", "rtl", "border-block-end-width", "10px"), + border_block!("lp-bbe-rtl-2", "border-block-end-width", "rtl", "border-block-end-width", "15px"), + border_block!("lp-bbe-rtl-3", "border-block-end-width", "rtl", "border-block-end-width", "5px"), + // -- inset-inline-start -- + inset_inline!("lp-iis-ltr-1", "inset-inline-start", "ltr", "inset-inline-start", "40px"), + inset_inline!("lp-iis-ltr-2", "inset-inline-start", "ltr", "inset-inline-start", "70px"), + inset_inline!("lp-iis-ltr-3", "inset-inline-start", "ltr", "inset-inline-start", "15px"), + inset_inline!("lp-iis-rtl-1", "inset-inline-start", "rtl", "inset-inline-start", "40px"), + inset_inline!("lp-iis-rtl-2", "inset-inline-start", "rtl", "inset-inline-start", "70px"), + inset_inline!("lp-iis-rtl-3", "inset-inline-start", "rtl", "inset-inline-start", "15px"), + // -- inset-inline-end -- + inset_inline!("lp-iie-ltr-1", "inset-inline-end", "ltr", "inset-inline-end", "40px"), + inset_inline!("lp-iie-ltr-2", "inset-inline-end", "ltr", "inset-inline-end", "70px"), + inset_inline!("lp-iie-ltr-3", "inset-inline-end", "ltr", "inset-inline-end", "15px"), + inset_inline!("lp-iie-rtl-1", "inset-inline-end", "rtl", "inset-inline-end", "40px"), + inset_inline!("lp-iie-rtl-2", "inset-inline-end", "rtl", "inset-inline-end", "70px"), + inset_inline!("lp-iie-rtl-3", "inset-inline-end", "rtl", "inset-inline-end", "15px"), + // -- inset-block-start -- + inset_block!("lp-ibs-ltr-1", "inset-block-start", "ltr", "inset-block-start", "30px"), + inset_block!("lp-ibs-ltr-2", "inset-block-start", "ltr", "inset-block-start", "50px"), + inset_block!("lp-ibs-ltr-3", "inset-block-start", "ltr", "inset-block-start", "10px"), + inset_block!("lp-ibs-rtl-1", "inset-block-start", "rtl", "inset-block-start", "30px"), + inset_block!("lp-ibs-rtl-2", "inset-block-start", "rtl", "inset-block-start", "50px"), + inset_block!("lp-ibs-rtl-3", "inset-block-start", "rtl", "inset-block-start", "10px"), + // -- inset-block-end -- + inset_block!("lp-ibe-ltr-1", "inset-block-end", "ltr", "inset-block-end", "30px"), + inset_block!("lp-ibe-ltr-2", "inset-block-end", "ltr", "inset-block-end", "50px"), + inset_block!("lp-ibe-ltr-3", "inset-block-end", "ltr", "inset-block-end", "10px"), + inset_block!("lp-ibe-rtl-1", "inset-block-end", "rtl", "inset-block-end", "30px"), + inset_block!("lp-ibe-rtl-2", "inset-block-end", "rtl", "inset-block-end", "50px"), + inset_block!("lp-ibe-rtl-3", "inset-block-end", "rtl", "inset-block-end", "10px"), + // -- inline-size -- + size_inline!("lp-is-ltr-1", "inline-size", "ltr", "150px"), + size_inline!("lp-is-ltr-2", "inline-size", "ltr", "220px"), + size_inline!("lp-is-ltr-3", "inline-size", "ltr", "80px"), + size_inline!("lp-is-rtl-1", "inline-size", "rtl", "150px"), + size_inline!("lp-is-rtl-2", "inline-size", "rtl", "220px"), + size_inline!("lp-is-rtl-3", "inline-size", "rtl", "80px"), + // -- block-size -- + size_block!("lp-bs-ltr-1", "block-size", "ltr", "120px"), + size_block!("lp-bs-ltr-2", "block-size", "ltr", "90px"), + size_block!("lp-bs-ltr-3", "block-size", "ltr", "40px"), + size_block!("lp-bs-rtl-1", "block-size", "rtl", "120px"), + size_block!("lp-bs-rtl-2", "block-size", "rtl", "90px"), + size_block!("lp-bs-rtl-3", "block-size", "rtl", "40px"), + // -- margin-inline shorthand -- + margin_inline!("lp-mi-ltr-1", "margin-inline", "ltr", "margin-inline", "20px 30px"), + margin_inline!("lp-mi-ltr-2", "margin-inline", "ltr", "margin-inline", "15px 45px"), + margin_inline!("lp-mi-ltr-3", "margin-inline", "ltr", "margin-inline", "7px 12px"), + margin_inline!("lp-mi-rtl-1", "margin-inline", "rtl", "margin-inline", "20px 30px"), + margin_inline!("lp-mi-rtl-2", "margin-inline", "rtl", "margin-inline", "15px 45px"), + margin_inline!("lp-mi-rtl-3", "margin-inline", "rtl", "margin-inline", "7px 12px"), + // -- margin-block shorthand -- + margin_block!("lp-mb-ltr-1", "margin-block", "ltr", "margin-block", "15px 25px"), + margin_block!("lp-mb-ltr-2", "margin-block", "ltr", "margin-block", "30px 5px"), + margin_block!("lp-mb-ltr-3", "margin-block", "ltr", "margin-block", "10px 18px"), + margin_block!("lp-mb-rtl-1", "margin-block", "rtl", "margin-block", "15px 25px"), + margin_block!("lp-mb-rtl-2", "margin-block", "rtl", "margin-block", "30px 5px"), + margin_block!("lp-mb-rtl-3", "margin-block", "rtl", "margin-block", "10px 18px"), + // -- padding-inline shorthand -- + padding_inline!("lp-pi-ltr-1", "padding-inline", "ltr", "padding-inline", "15px 25px"), + padding_inline!("lp-pi-ltr-2", "padding-inline", "ltr", "padding-inline", "30px 5px"), + padding_inline!("lp-pi-ltr-3", "padding-inline", "ltr", "padding-inline", "10px 18px"), + padding_inline!("lp-pi-rtl-1", "padding-inline", "rtl", "padding-inline", "15px 25px"), + padding_inline!("lp-pi-rtl-2", "padding-inline", "rtl", "padding-inline", "30px 5px"), + padding_inline!("lp-pi-rtl-3", "padding-inline", "rtl", "padding-inline", "10px 18px"), + // -- padding-block shorthand -- + padding_block!("lp-pb-ltr-1", "padding-block", "ltr", "padding-block", "12px 18px"), + padding_block!("lp-pb-ltr-2", "padding-block", "ltr", "padding-block", "8px 22px"), + padding_block!("lp-pb-ltr-3", "padding-block", "ltr", "padding-block", "5px 14px"), + padding_block!("lp-pb-rtl-1", "padding-block", "rtl", "padding-block", "12px 18px"), + padding_block!("lp-pb-rtl-2", "padding-block", "rtl", "padding-block", "8px 22px"), + padding_block!("lp-pb-rtl-3", "padding-block", "rtl", "padding-block", "5px 14px"), + // -- inset-inline shorthand -- + inset_inline!("lp-ii-ltr-1", "inset-inline", "ltr", "inset-inline", "20px 30px"), + inset_inline!("lp-ii-ltr-2", "inset-inline", "ltr", "inset-inline", "15px 45px"), + inset_inline!("lp-ii-ltr-3", "inset-inline", "ltr", "inset-inline", "7px 12px"), + inset_inline!("lp-ii-rtl-1", "inset-inline", "rtl", "inset-inline", "20px 30px"), + inset_inline!("lp-ii-rtl-2", "inset-inline", "rtl", "inset-inline", "15px 45px"), + inset_inline!("lp-ii-rtl-3", "inset-inline", "rtl", "inset-inline", "7px 12px"), + // -- inset-block shorthand -- + inset_block!("lp-ib-ltr-1", "inset-block", "ltr", "inset-block", "10px 20px"), + inset_block!("lp-ib-ltr-2", "inset-block", "ltr", "inset-block", "25px 5px"), + inset_block!("lp-ib-ltr-3", "inset-block", "ltr", "inset-block", "8px 16px"), + inset_block!("lp-ib-rtl-1", "inset-block", "rtl", "inset-block", "10px 20px"), + inset_block!("lp-ib-rtl-2", "inset-block", "rtl", "inset-block", "25px 5px"), + inset_block!("lp-ib-rtl-3", "inset-block", "rtl", "inset-block", "8px 16px"), + // -- mixed logical and physical declarations for the same side -- + lp!( + "lp-mx-ltr-1", + "mixed", + "ltr", + "#p { width: 300px; height: 100px; padding: 1px; }\n#a { margin-left: 10px; margin-inline-start: 30px; width: 100px; height: 30px; }" + ), + lp!( + "lp-mx-ltr-2", + "mixed", + "ltr", + "#p { width: 300px; height: 100px; padding: 1px; }\n#a { margin-inline-start: 30px; margin-left: 10px; width: 100px; height: 30px; }" + ), + lp!( + "lp-mx-ltr-3", + "mixed", + "ltr", + "#p { width: 300px; height: 100px; padding: 1px; }\n#a { width: 100px; inline-size: 150px; height: 30px; }" + ), + lp!( + "lp-mx-rtl-1", + "mixed", + "rtl", + "#p { width: 300px; height: 100px; padding: 1px; }\n#a { margin-right: 10px; margin-inline-start: 30px; width: 100px; height: 30px; }" + ), + lp!( + "lp-mx-rtl-2", + "mixed", + "rtl", + "#p { width: 300px; height: 100px; padding: 1px; }\n#a { margin-inline-start: 30px; margin-right: 10px; width: 100px; height: 30px; }" + ), + lp!( + "lp-mx-rtl-3", + "mixed", + "rtl", + "#p { width: 300px; height: 100px; padding: 1px; }\n#a { inline-size: 150px; width: 100px; height: 30px; }" + ), +]; + +/// The harness's fixed viewport (`docs/dump-format.md`), a literal here the same way +/// `fragment_check.rs`, `custom_properties_check.rs` and `box_check.rs` each keep their +/// own. +const VIEWPORT_WIDTH: f64 = 800.0; +const VIEWPORT_HEIGHT: f64 = 600.0; + +/// Fewer comparisons than this and the run fails, so an oracle that silently produced +/// nothing cannot pass. +const MINIMUM_COMPARED: usize = 30; + +/// `fragment_check.rs` `engine_dump` copied rather than shared: two binaries in the same +/// crate cannot import each other, and the spec's `touches` is this file alone. +fn engine_dump(html: &str) -> Dump { + let document = Document::parse(html); + let elements = document.elements_in_document_order(); + let tree = BoxTree::build(&document); + let root_fragment = fragment::layout(&document, &tree, (VIEWPORT_WIDTH, VIEWPORT_HEIGHT)); + + let mut rects: HashMap = HashMap::new(); + root_fragment.walk_absolute(0.0, 0.0, &mut |node, x, y, w, h| { + rects.insert(node, Rect { x, y, w, h }); + }); + + let mut index_of: HashMap = HashMap::new(); + for (i, &el) in elements.iter().enumerate() { + index_of.insert(el, i); + } + + let nodes = elements + .iter() + .enumerate() + .map(|(index, el)| { + let parent = el.dom_parent().map(|p| index_of[&p] as i64).unwrap_or(-1); + let mut depth = 0u32; + let mut cur = *el; + while let Some(p) = cur.dom_parent() { + depth += 1; + cur = p; + } + // No box (`display: none`, or pruned with it) means no fragment and an empty + // `rects` list, zeroed the way a browser zeroes `bounding`. + let rect = rects.get(el).copied(); + DumpNode { + index, + parent, + tag: el.tag(), + depth, + bounding: rect.unwrap_or(Rect { x: 0.0, y: 0.0, w: 0.0, h: 0.0 }), + rects: rect.map(|r| vec![r]).unwrap_or_default(), + scroll: None, + style: Default::default(), + } + }) + .collect(); + + Dump { + engine: "nev-layout".to_string(), + engine_version: env!("CARGO_PKG_VERSION").to_string(), + viewport: Viewport { width: 800, height: 600 }, + overflowed: false, + nodes, + } +} + +/// Passed/compared for one group within one direction. +#[derive(Debug, Default, Clone, Copy)] +struct CategoryTally { + passed: usize, + compared: usize, +} + +/// The direction a case is measured under. Every document names it on `html`, so the +/// literal text is the source of truth rather than a second field beside the document. +fn direction_of(html: &str) -> &'static str { + if html.contains("direction: rtl") { + "rtl" + } else { + "ltr" + } +} + +/// Why one case was dropped before the engine was compared against it: either a browser +/// failed to dump, or the browsers that did dump disagree with each other. Both make the +/// case unusable as a reference, and neither is a statement about the engine. +fn browser_verdict( + engine_names: &[String], + dumps: &HashMap, + errors: &HashMap, +) -> Result<(), String> { + let mut failed: Vec = errors.iter().map(|(engine, msg)| format!("{engine}: {msg}")).collect(); + failed.sort(); + for engine in engine_names { + if !dumps.contains_key(engine) && !errors.contains_key(engine) { + failed.push(format!("{engine}: no dump returned")); + } + } + if !failed.is_empty() { + return Err(format!("browser failed to dump ({})", failed.join("; "))); + } + if dumps.is_empty() { + return Err("no browser dump available".to_string()); + } + + // Every pair, not only neighbours: agreement within a tolerance is not transitive, so + // chromium and webkit can each agree with firefox and still disagree with each other. + let mut names: Vec<&String> = dumps.keys().collect(); + names.sort(); + for (i, left) in names.iter().enumerate() { + for right in &names[i + 1..] { + let outcome = compare(&dumps[left.as_str()], &dumps[right.as_str()]); + if !outcome.passed() { + return Err(format!("browsers disagree ({left} vs {right}: {})", outcome.report())); + } + } + } + Ok(()) +} + +fn main() -> Result<()> { + let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(|p| p.parent()) + .expect("crate sits two levels below the repository root") + .to_path_buf(); + + let mut oracle = Oracle::start(&repo_root, None, None)?; + let engine_names = oracle.engines.clone(); + println!("engines: {}", engine_names.join(", ")); + println!("cases written: {}", CASES.len()); + + let mut compared = 0usize; + let mut passed = 0usize; + let mut dropped: Vec<(&str, String)> = Vec::new(); + let mut failures: Vec<(&str, String)> = Vec::new(); + let mut per_group: BTreeMap<(&str, &str), CategoryTally> = BTreeMap::new(); + + for &(id, category, html) in CASES { + let direction = direction_of(html); + + let (browser_dumps, errors) = oracle.dump(html)?; + if let Err(reason) = browser_verdict(&engine_names, &browser_dumps, &errors) { + dropped.push((id, reason)); + continue; + } + + let engine = engine_dump(html); + // The browsers already agree with each other, but tolerance is not transitive, so + // the engine is held against every dump that counted rather than one reference. + let mut disagreements: Vec = Vec::new(); + for (name, dump) in &browser_dumps { + let outcome = compare(&engine, dump); + if !outcome.passed() { + disagreements.push(format!("{name}: {}", outcome.report())); + } + } + disagreements.sort(); + + compared += 1; + let tally = per_group.entry((category, direction)).or_default(); + tally.compared += 1; + if disagreements.is_empty() { + passed += 1; + tally.passed += 1; + } else { + failures.push((id, disagreements.join(" | "))); + } + } + + println!("\n== summary =="); + println!("dropped {}, compared {compared}, passed {passed}", dropped.len()); + + println!("\n== per-group and direction =="); + for category in CATEGORIES { + for direction in ["ltr", "rtl"] { + let tally = per_group.get(&(category, direction)).copied().unwrap_or_default(); + let written = CASES + .iter() + .filter(|(_, c, html)| c == category && direction_of(html) == direction) + .count(); + println!("{category}/{direction} {}/{} passed, {written} written", tally.passed, tally.compared); + } + } + + println!("\n== dropped =="); + if dropped.is_empty() { + println!("none"); + } else { + for (id, reason) in &dropped { + println!("{id}: {reason}"); + } + } + + println!("\n== failures =="); + if failures.is_empty() { + println!("none"); + } else { + for (id, reason) in &failures { + println!("{id}: {reason}"); + } + } + + oracle.shutdown()?; + + // A case disagreeing with the engine is the result, not a failure of the run; too few + // comparisons to stand on means the run measured nothing. + if compared < MINIMUM_COMPARED { + bail!("only {compared} case(s) compared, fewer than the {MINIMUM_COMPARED} this check needs to mean anything"); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use dom::computed::{self, LengthPercentageOrAuto}; + use std::collections::BTreeSet; + + /// The element with `id`, found the same way the check binaries correlate nodes: + /// walking `elements_in_document_order` and reading the parsed attribute back off + /// `Document::nodes()` (`crates/dom/src/node.rs` `ElementData::attr`). + fn find(document: &Document, id: &str) -> NodeId { + document + .elements_in_document_order() + .into_iter() + .find(|&e| document.nodes()[e.index()].as_element().unwrap().attr("id") == Some(id)) + .unwrap_or_else(|| panic!("no element with id={id:?}")) + } + + #[test] + fn ids_are_unique() { + let mut seen: BTreeSet<&str> = BTreeSet::new(); + for &(id, _, _) in CASES { + assert!(seen.insert(id), "duplicate case id {id}"); + } + } + + #[test] + fn categories_are_non_empty_and_known() { + for &(id, category, _) in CASES { + assert!(!category.is_empty(), "case {id} has an empty category"); + assert!(CATEGORIES.contains(&category), "case {id} names unknown category {category}"); + } + } + + #[test] + fn every_category_has_three_cases_per_direction() { + for category in CATEGORIES { + for direction in ["ltr", "rtl"] { + let count = CASES + .iter() + .filter(|(_, c, html)| c == category && direction_of(html) == direction) + .count(); + assert!(count >= 3, "category {category} has only {count} {direction} case(s)"); + } + } + } + + #[test] + fn the_corpus_is_at_least_sixty_cases() { + assert!(CASES.len() >= 60, "only {} case(s) written", CASES.len()); + } + + #[test] + fn every_document_opens_with_the_reset() { + for &(id, _, html) in CASES { + assert!(html.contains(RESET), "case {id} is missing the reset"); + } + } + + #[test] + fn every_document_parses_and_yields_a_box_tree() { + for &(id, _, html) in CASES { + let document = Document::parse(html); + let elements = document.elements_in_document_order(); + assert!(!elements.is_empty(), "case {id} parsed to no elements"); + let dump = engine_dump(html); + assert_eq!(dump.nodes.len(), elements.len(), "case {id} dumped a different element count"); + assert!( + dump.nodes.iter().any(|node| !node.rects.is_empty()), + "case {id} produced no fragment, so no box tree" + ); + } + } + + /// The accessor itself swaps the physical side for `margin-inline-start`: `7px` is + /// `margin-left` under `ltr` and `margin-right` under `rtl`. + #[test] + fn logical_inline_start_margin_swaps_sides_under_rtl() { + let ltr = + Document::parse("
"); + let a = find(<r, "a"); + assert_eq!(computed::margin_left(<r, a), LengthPercentageOrAuto::Length(7.0)); + assert_eq!(computed::margin_right(<r, a), LengthPercentageOrAuto::Length(0.0)); + + let rtl = Document::parse( + "
", + ); + let a = find(&rtl, "a"); + assert_eq!(computed::margin_left(&rtl, a), LengthPercentageOrAuto::Length(0.0)); + assert_eq!(computed::margin_right(&rtl, a), LengthPercentageOrAuto::Length(7.0)); + } + + /// The run's record, with the `//!` comment markers stripped. Only the one fenced + /// block in this file holds it, so the reader toggles on a fence line. + fn recorded_block() -> String { + let source = include_str!("logical_properties_check.rs"); + let mut block = String::new(); + let mut inside = false; + for line in source.lines() { + let line = line.strip_prefix("//!").unwrap_or(line); + let line = line.strip_prefix(' ').unwrap_or(line); + if line.starts_with("```") { + inside = !inside; + continue; + } + if inside { + block.push_str(line); + block.push('\n'); + } + } + block + } + + /// `dropped N, compared N, passed N` from the summary section. + fn recorded_summary(block: &str) -> (usize, usize, usize) { + for line in block.lines() { + let line = line.trim(); + if let Some(rest) = line.strip_prefix("dropped ") { + let mut parts = rest.split_whitespace(); + let dropped: usize = parts.next().unwrap().trim_end_matches(',').parse().unwrap(); + let compared: usize = parts.nth(1).unwrap().trim_end_matches(',').parse().unwrap(); + let passed: usize = parts.nth(1).unwrap().parse().unwrap(); + return (dropped, compared, passed); + } + } + panic!("the module doc records no summary line"); + } + + /// `(group, direction, passed, compared, written)` from the per-group table. + fn recorded_rows(block: &str) -> Vec<(String, String, usize, usize, usize)> { + let mut rows = Vec::new(); + let mut inside = false; + for line in block.lines() { + let line = line.trim(); + if line == "== per-group and direction ==" { + inside = true; + continue; + } + if inside && line.starts_with("==") { + break; + } + if !inside || line.is_empty() { + continue; + } + let mut parts = line.split_whitespace(); + let Some(head) = parts.next() else { continue }; + let Some((group, direction)) = head.rsplit_once('/') else { continue }; + if direction != "ltr" && direction != "rtl" { + continue; + } + let Some(fraction) = parts.next() else { continue }; + let Some((p, c)) = fraction.split_once('/') else { continue }; + let passed: usize = p.parse().unwrap(); + let compared: usize = c.parse().unwrap(); + parts.next(); // "passed," + let written: usize = parts.next().unwrap().parse().unwrap(); + rows.push((group.to_string(), direction.to_string(), passed, compared, written)); + } + rows + } + + /// The ids under the `== failures ==` section, each read as the token before its + /// cause. + fn recorded_failures(block: &str) -> Vec { + let mut ids = Vec::new(); + let mut inside = false; + for line in block.lines() { + let line = line.trim(); + if line == "== failures ==" { + inside = true; + continue; + } + if !inside || line.is_empty() || line == "none" { + continue; + } + if let Some(id) = line.split([':', ' ']).next() { + if !id.is_empty() { + ids.push(id.to_string()); + } + } + } + ids + } + + /// The recorded table must add up to the summary, and every failing id must be a real + /// case. This catches a record that was typed or recomputed by hand. + #[test] + fn recorded_result_adds_up() { + let block = recorded_block(); + let (dropped, compared, passed) = recorded_summary(&block); + let rows = recorded_rows(&block); + + assert_eq!(rows.len(), CATEGORIES.len() * 2, "the per-group table has the wrong number of rows"); + let sum_passed: usize = rows.iter().map(|r| r.2).sum(); + let sum_compared: usize = rows.iter().map(|r| r.3).sum(); + let sum_written: usize = rows.iter().map(|r| r.4).sum(); + assert_eq!(sum_passed, passed, "per-group passed cells disagree with the summary"); + assert_eq!(sum_compared, compared, "per-group compared cells disagree with the summary"); + assert_eq!(sum_written, CASES.len(), "per-group written cells disagree with CASES"); + assert_eq!(dropped + compared, CASES.len(), "dropped plus compared disagrees with CASES"); + + let known: BTreeSet<&str> = CASES.iter().map(|(id, _, _)| *id).collect(); + let failures = recorded_failures(&block); + assert_eq!(compared - passed, failures.len(), "compared minus passed disagrees with the listed failing ids"); + for id in &failures { + assert!(known.contains(id.as_str()), "recorded failing id {id} is not in CASES"); + } + } +} +