diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted.rs
index 965c007181..add3a0b6b7 100644
--- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted.rs
+++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted.rs
@@ -10,5 +10,6 @@ mod events_and_focus;
mod frame_geometry_and_input;
mod frame_navigation_and_history;
mod geometry_and_scrolling;
+mod inline_layout;
mod ranges_and_selection;
mod window_and_frame_surfaces;
diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_geometry_and_input.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_geometry_and_input.rs
index cd5b783949..a6ad90de10 100644
--- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_geometry_and_input.rs
+++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_geometry_and_input.rs
@@ -341,995 +341,6 @@ fn iframe_line_height_uses_its_own_document_quirks_mode() {
}
}
-#[test]
-fn quirks_block_line_height_preserves_text_and_atomic_alignment() {
- for (doctype, quirks) in [
- ("", true),
- (
- r#""#,
- true,
- ),
- ("", false),
- ] {
- let mut vm = new_parsed_test_vm(
- "https://quirks-block-line-height.test/",
- &format!(
- r#"{doctype}
-
- x
- x
-
-
"#
- ),
- );
- let query = r#"JSON.stringify(['only','text','smaller','aligned','spaced'].map(id => document.getElementById(id).getBoundingClientRect().height))"#;
- let expected = if quirks {
- "[20,80,10,20,20]"
- } else {
- "[80,80,80,80,80]"
- };
- assert_eq!(vm.eval(query).unwrap(), expected, "{doctype}");
- publish_layout_for_test(&mut vm);
- assert_eq!(vm.eval(query).unwrap(), expected, "paint: {doctype}");
- }
-}
-
-fn check_inline_line_height_modes(cases: &[(&str, &str, f64, f64)]) {
- for (doctype, quirks) in [
- ("", true),
- (
- r#""#,
- true,
- ),
- ("", false),
- ] {
- let mut markup = format!(
- r#"{doctype}"#
- );
- for (id, content, _, _) in cases {
- markup.push_str(&format!(r#"{content}
"#));
- }
- let mut vm = new_parsed_test_vm("https://quirks-empty-inline.test/", &markup);
- let query = "JSON.stringify(Array.from(document.querySelectorAll('.line'), e=>e.getBoundingClientRect().height))";
- let result = vm.eval(query).unwrap();
- let heights: Vec = serde_json::from_str(&result).unwrap();
- for ((id, _, quirks_height, standards_height), actual) in cases.iter().zip(heights) {
- assert_eq!(
- actual,
- if quirks {
- *quirks_height
- } else {
- *standards_height
- },
- "{doctype} / {id}: {result}"
- );
- }
- publish_layout_for_test(&mut vm);
- assert_eq!(
- vm.eval(query).unwrap(),
- result,
- "publication uses the same line height rules"
- );
- }
-}
-
-#[test]
-fn quirks_empty_inline_line_height_uses_text_and_fragment_edges() {
- check_inline_line_height_modes(&[
- ("direct", r#""#, 20.0, 80.0),
- (
- "wrapped",
- r#""#,
- 20.0,
- 80.0,
- ),
- (
- "nested",
- r#""#,
- 20.0,
- 80.0,
- ),
- (
- "collapsed",
- " \n \n ",
- 20.0,
- 80.0,
- ),
- (
- "small_descendant_text",
- r#"x"#,
- 10.0,
- 80.0,
- ),
- (
- "own_text",
- r#"x"#,
- 80.0,
- 80.0,
- ),
- (
- "between_atoms",
- r#" "#,
- 80.0,
- 80.0,
- ),
- (
- "preserved_space",
- r#" "#,
- 80.0,
- 80.0,
- ),
- (
- "preserved_break",
- r#"
-"#,
- 80.0,
- 80.0,
- ),
- ("br", r#"
"#, 80.0, 80.0),
- (
- "atomic_text",
- r#"x"#,
- 10.0,
- 80.0,
- ),
- (
- "empty_next_to_atom",
- r#""#,
- 20.0,
- 80.0,
- ),
- (
- "bidi",
- r#""#,
- 20.0,
- 80.0,
- ),
- (
- "nested_small_atom",
- r#""#,
- 20.0,
- 160.0,
- ),
- ]);
-}
-
-#[test]
-fn quirks_empty_inline_line_height_distinguishes_border_padding_and_margin() {
- check_inline_line_height_modes(&[
- (
- "margin_only",
- r#""#,
- 0.0,
- 80.0,
- ),
- (
- "padding_block",
- r#""#,
- 20.0,
- 80.0,
- ),
- (
- "border_block",
- r#""#,
- 20.0,
- 80.0,
- ),
- (
- "padding_left",
- r#""#,
- 80.0,
- 80.0,
- ),
- (
- "padding_right",
- r#""#,
- 80.0,
- 80.0,
- ),
- (
- "border_left",
- r#""#,
- 80.0,
- 80.0,
- ),
- (
- "border_right",
- r#""#,
- 80.0,
- 80.0,
- ),
- (
- "hidden_border",
- r#""#,
- 20.0,
- 80.0,
- ),
- (
- "none_border",
- r#""#,
- 20.0,
- 80.0,
- ),
- (
- "cancelled_edge",
- r#""#,
- 80.0,
- 80.0,
- ),
- (
- "padding_zero_percent",
- r#""#,
- 20.0,
- 80.0,
- ),
- (
- "padding_percent",
- r#""#,
- 80.0,
- 80.0,
- ),
- (
- "margin_next_to_atom",
- r#""#,
- 20.0,
- 80.0,
- ),
- ]);
-}
-
-#[test]
-fn quirks_empty_inline_line_height_is_resolved_per_line() {
- check_inline_line_height_modes(&[
- (
- "multiple_lines",
- r#"
"#,
- 40.0,
- 160.0,
- ),
- (
- "text_first_line",
- r#"x
"#,
- 100.0,
- 160.0,
- ),
- (
- "text_last_line",
- r#"
x"#,
- 100.0,
- 160.0,
- ),
- (
- "border_multiple_lines",
- r#"
"#,
- 100.0,
- 160.0,
- ),
- (
- "space_at_break",
- r#"
"#,
- 40.0,
- 160.0,
- ),
- (
- "text_elsewhere",
- r#"x
"#,
- 100.0,
- 160.0,
- ),
- (
- "middle_atom",
- r#""#,
- 20.0,
- 80.0,
- ),
- (
- "top_wrapper",
- r#""#,
- 20.0,
- 80.0,
- ),
- (
- "bottom_wrapper",
- r#""#,
- 20.0,
- 80.0,
- ),
- ]);
-}
-
-#[test]
-fn quirks_empty_inline_line_height_preserves_forced_empty_lines() {
- check_inline_line_height_modes(&[
- (
- "large_preserved_break",
- r#"
-"#,
- 100.0,
- 100.0,
- ),
- (
- "large_br",
- r#"
"#,
- 100.0,
- 100.0,
- ),
- ("root_br", r#"
"#, 80.0, 80.0),
- (
- "large_nested_br",
- r#"
"#,
- 10.0,
- 160.0,
- ),
- (
- "br_own_font",
- r#"
"#,
- 100.0,
- 80.0,
- ),
- (
- "atom_large_br",
- r#"
"#,
- 20.0,
- 100.0,
- ),
- (
- "atom_large_preserved_break",
- r#"
-"#,
- 20.0,
- 100.0,
- ),
- (
- "atom_small_br",
- r#"
"#,
- 20.0,
- 80.0,
- ),
- (
- "margin_then_br",
- r#"
"#,
- 100.0,
- 80.0,
- ),
- (
- "padding_then_br",
- r#"
"#,
- 80.0,
- 80.0,
- ),
- (
- "padding_with_br",
- r#"
"#,
- 80.0,
- 80.0,
- ),
- (
- "wrapped_auto_break",
- r#" "#,
- 40.0,
- 160.0,
- ),
- (
- "collapsed_large_trailing_space",
- r#" "#,
- 20.0,
- 160.0,
- ),
- (
- "border_empty",
- r#""#,
- 80.0,
- 80.0,
- ),
- ]);
-}
-
-#[test]
-fn quirks_empty_inline_line_height_collapses_spaces_after_wrapping() {
- check_inline_line_height_modes(&[
- (
- "wrapped_auto_break",
- r#" "#,
- 40.0,
- 160.0,
- ),
- (
- "wrapped_auto_break_preserved",
- r#" "#,
- 100.0,
- 160.0,
- ),
- (
- "wrapped_auto_break_rtl",
- r#" "#,
- 40.0,
- 160.0,
- ),
- (
- "wrapped_auto_break_space_font",
- r#" "#,
- 40.0,
- 240.0,
- ),
- (
- "wrapped_space_font_then_text",
- r#" x"#,
- 100.0,
- 240.0,
- ),
- (
- "wrapped_space_font_then_zero_text",
- r#" x"#,
- 20.0,
- 240.0,
- ),
- ]);
-}
-
-#[test]
-fn quirks_nested_break_line_height_uses_its_own_inline_box() {
- check_inline_line_height_modes(&[
- (
- "nested_br_padding",
- r#"
"#,
- 80.0,
- 80.0,
- ),
- (
- "nested_br_border",
- r#"
"#,
- 80.0,
- 80.0,
- ),
- (
- "nested_br_text",
- r#"x
"#,
- 80.0,
- 80.0,
- ),
- (
- "nested_br_atom_sibling",
- r#"
"#,
- 80.0,
- 80.0,
- ),
- (
- "nested_br_atom_in_parent",
- r#"
"#,
- 80.0,
- 80.0,
- ),
- (
- "nested_br_atom_in_same_box",
- r#"
"#,
- 20.0,
- 80.0,
- ),
- (
- "nested_br_descendant_text",
- r#"x
"#,
- 10.0,
- 160.0,
- ),
- (
- "nested_br_own_small_text",
- r#"x
"#,
- 10.0,
- 80.0,
- ),
- (
- "nested_preserved_break",
- r#"
-"#,
- 80.0,
- 80.0,
- ),
- ]);
-}
-
-#[test]
-fn quirks_wrapped_whitespace_line_height_counts_only_surviving_spaces() {
- check_inline_line_height_modes(&[
- (
- "wrapped_space",
- r#" "#,
- 80.0,
- 80.0,
- ),
- (
- "nested_wrapped_space",
- r#" "#,
- 80.0,
- 80.0,
- ),
- (
- "wrapped_space_large_font",
- r#" "#,
- 160.0,
- 160.0,
- ),
- (
- "wrapped_space_small_font",
- r#" "#,
- 22.0,
- 80.0,
- ),
- (
- "wrapped_space_rtl",
- r#" "#,
- 80.0,
- 80.0,
- ),
- (
- "wrapped_space_preserved",
- r#" "#,
- 80.0,
- 80.0,
- ),
- (
- "wrapped_space_leading",
- r#" "#,
- 20.0,
- 80.0,
- ),
- (
- "wrapped_space_trailing",
- r#" "#,
- 20.0,
- 80.0,
- ),
- (
- "wrapped_space_at_break",
- r#"
"#,
- 40.0,
- 160.0,
- ),
- (
- "wrapped_space_after_wrap",
- r#" "#,
- 40.0,
- 160.0,
- ),
- ]);
-}
-
-#[test]
-fn inline_wrapped_whitespace_preserves_sibling_spacing() {
- for doctype in [
- "",
- r#""#,
- "",
- ] {
- let mut vm = new_parsed_test_vm(
- "https://inline-wrapped-whitespace.test/",
- &format!(
- r#"{doctype}
-
-
-
-
"#
- ),
- );
- let query = r#"JSON.stringify([
- ...['plain','wrapped','nested'].map(id => {
- const atoms = document.getElementById(id).querySelectorAll('.atom');
- return atoms[1].getBoundingClientRect().left - atoms[0].getBoundingClientRect().right;
- }),
- edges.querySelector('.atom').getBoundingClientRect().left - edges.getBoundingClientRect().left
- ])"#;
- let result = vm.eval(query).unwrap();
- let gaps: Vec = serde_json::from_str(&result).unwrap();
- assert!(gaps[0] > 0.0, "{doctype}: {result}");
- assert_eq!(gaps[1], gaps[0], "a wrapper must retain the space");
- assert_eq!(gaps[2], gaps[0], "nested wrappers must retain the space");
- assert_eq!(gaps[3], 0.0, "a leading wrapped space still collapses");
- publish_layout_for_test(&mut vm);
- assert_eq!(vm.eval(query).unwrap(), result, "paint: {doctype}");
- }
-}
-
-#[test]
-fn inline_closing_edges_after_forced_breaks_contribute_to_their_own_fragment() {
- let cases = [
- (
- "left_border",
- r#"
"#,
- 160.0,
- 160.0,
- true,
- ),
- (
- "right_border",
- r#"
"#,
- 160.0,
- 160.0,
- true,
- ),
- (
- "right_padding",
- r#"
"#,
- 160.0,
- 160.0,
- true,
- ),
- (
- "terminal_break",
- r#"
"#,
- 80.0,
- 80.0,
- true,
- ),
- (
- "preserved_break",
- "\n
",
- 160.0,
- 160.0,
- true,
- ),
- (
- "nested_closing_edges",
- r#"
"#,
- 160.0,
- 160.0,
- true,
- ),
- (
- "nested_bidi_closing_edges",
- r#"
"#,
- 100.0,
- 160.0,
- false,
- ),
- (
- "nested_bidi_terminal_padding",
- r#"
"#,
- 100.0,
- 160.0,
- false,
- ),
- (
- "nested_bidi_terminal_border",
- r#"
"#,
- 100.0,
- 160.0,
- false,
- ),
- (
- "nested_bidi_terminal_margin",
- r#"
"#,
- 20.0,
- 160.0,
- false,
- ),
- (
- "multiple_fragments",
- r#"
"#,
- 180.0,
- 240.0,
- false,
- ),
- (
- "opening_edge_after_break",
- r#"
"#,
- 100.0,
- 160.0,
- false,
- ),
- (
- "undecorated",
- r#"
"#,
- 100.0,
- 160.0,
- false,
- ),
- (
- "right_margin",
- r#"
"#,
- 100.0,
- 160.0,
- false,
- ),
- ];
- for (doctype, quirks) in [
- ("", true),
- (
- r#""#,
- true,
- ),
- ("", false),
- ] {
- let mut markup = format!(
- r#"{doctype}"#
- );
- for (id, content, ..) in &cases {
- markup.push_str(&format!(r#"{content}
"#));
- }
- let mut vm = new_parsed_test_vm("https://inline-closing-edge.test/", &markup);
- let query = r#"JSON.stringify(Array.from(document.querySelectorAll('.line'), e => {
- const line = e.getBoundingClientRect();
- const atom = e.querySelector('.atom').getBoundingClientRect();
- return [line.height, atom.top - line.top];
- }))"#;
- let result = vm.eval(query).unwrap();
- let geometries: Vec<[f64; 2]> = serde_json::from_str(&result).unwrap();
- let decorated_atom_top = geometries[0][1];
- for ((id, _, quirks_height, standards_height, decorated), actual) in
- cases.iter().zip(geometries)
- {
- assert_eq!(
- actual[0],
- if quirks {
- *quirks_height
- } else {
- *standards_height
- },
- "height: {doctype} / {id}: {result}"
- );
- assert_eq!(
- actual[1],
- if !quirks || *decorated {
- decorated_atom_top
- } else {
- 0.0
- },
- "atom top: {doctype} / {id}"
- );
- }
- for (id, fragments) in [
- ("right_border", 1),
- ("right_padding", 1),
- ("terminal_break", 1),
- ("nested_bidi_closing_edges", 2),
- ("nested_bidi_terminal_padding", 2),
- ("nested_bidi_terminal_border", 2),
- ("nested_bidi_terminal_margin", 2),
- ] {
- assert_eq!(
- vm.eval(&format!(
- "document.getElementById({id:?}).querySelector('span').getClientRects().length"
- ))
- .unwrap(),
- fragments.to_string(),
- "closing edge fragment count: {doctype} / {id}"
- );
- }
- let margin_fragment_query = r#"(() => {
- const rects = document.getElementById('nested_bidi_terminal_margin').querySelector('span').getClientRects();
- return rects[1].height / rects[0].height;
- })()"#;
- let margin_fragment_ratio = vm.eval(margin_fragment_query).unwrap();
- assert_eq!(margin_fragment_ratio, if quirks { "0" } else { "1" });
- publish_layout_for_test(&mut vm);
- assert_eq!(vm.eval(query).unwrap(), result, "paint: {doctype}");
- assert_eq!(
- vm.eval(margin_fragment_query).unwrap(),
- margin_fragment_ratio,
- "margin fragment paint: {doctype}"
- );
- }
-}
-
-#[test]
-fn inline_used_closing_edges_preserve_standards_struts_and_ancestors() {
- check_inline_closing_fragment_modes(&[
- (
- "margin_tall",
- r#"
"#,
- 20.0,
- 320.0,
- 0.0,
- ),
- (
- "padding_tall",
- r#"
"#,
- 180.0,
- 320.0,
- 1.0,
- ),
- (
- "border_tall",
- r#"
"#,
- 180.0,
- 320.0,
- 1.0,
- ),
- (
- "margin_nested",
- r#"
"#,
- 20.0,
- 400.0,
- 0.0,
- ),
- ]);
-}
-
-#[test]
-fn standards_closing_inline_struts_do_not_depend_on_edge_sizes() {
- check_inline_line_height_modes(&[
- (
- "plain_tall_text",
- r#"
x"#,
- 100.0,
- 320.0,
- ),
- (
- "plain_tall_br",
- r#"
"#,
- 100.0,
- 320.0,
- ),
- (
- "plain_tall_terminal",
- r#"
"#,
- 20.0,
- 160.0,
- ),
- (
- "margin_tall_text",
- r#"
x"#,
- 100.0,
- 320.0,
- ),
- (
- "plain_nested_text",
- r#"
x"#,
- 100.0,
- 400.0,
- ),
- (
- "plain_nested_br",
- r#"
"#,
- 100.0,
- 400.0,
- ),
- (
- "plain_nested_terminal",
- r#"
"#,
- 20.0,
- 200.0,
- ),
- (
- "plain_tall_atom",
- r#"
"#,
- 40.0,
- 320.0,
- ),
- (
- "plain_tall_zero_atom",
- r#"
"#,
- 20.0,
- 320.0,
- ),
- (
- "plain_tall_zero_text",
- r#"
x"#,
- 20.0,
- 320.0,
- ),
- (
- "plain_tall_zero_br",
- r#"
"#,
- 20.0,
- 320.0,
- ),
- ]);
-}
-
-#[test]
-fn quirks_closing_fragment_font_boxes_follow_line_content() {
- check_inline_closing_fragment_modes(&[
- (
- "margin_br",
- r#"
"#,
- 100.0,
- 160.0,
- 1.0,
- ),
- (
- "margin_text",
- r#"
x"#,
- 100.0,
- 160.0,
- 1.0,
- ),
- (
- "margin_atom",
- r#"
"#,
- 40.0,
- 160.0,
- 1.0,
- ),
- (
- "margin_zero_atom",
- r#"
"#,
- 20.0,
- 160.0,
- 1.0,
- ),
- (
- "margin_zero_text",
- r#"
x"#,
- 20.0,
- 160.0,
- 1.0,
- ),
- (
- "margin_zero_br",
- r#"
"#,
- 20.0,
- 160.0,
- 1.0,
- ),
- ]);
-}
-
-fn check_inline_closing_fragment_modes(cases: &[(&str, &str, f64, f64, f64)]) {
- for (doctype, quirks) in [
- ("", true),
- (
- r#""#,
- true,
- ),
- ("", false),
- ] {
- let mut markup = format!(
- r#"{doctype}"#
- );
- for (id, content, ..) in cases {
- markup.push_str(&format!(r#"{content}
"#));
- }
- let mut vm = new_parsed_test_vm("https://inline-closing-fragment.test/", &markup);
- let query = r#"JSON.stringify(Array.from(document.querySelectorAll('.line'), e => {
- const rects = e.querySelector('span').getClientRects();
- return [e.getBoundingClientRect().height, rects.length, rects[1].height / rects[0].height];
- }))"#;
- let result = vm.eval(query).unwrap();
- let geometries: Vec<[f64; 3]> = serde_json::from_str(&result).unwrap();
- for ((id, _, quirks_height, standards_height, quirks_ratio), actual) in
- cases.iter().zip(geometries)
- {
- assert_eq!(
- actual,
- [
- if quirks {
- *quirks_height
- } else {
- *standards_height
- },
- 2.0,
- if quirks { *quirks_ratio } else { 1.0 },
- ],
- "closing fragment: {doctype} / {id}: {result}"
- );
- }
- publish_layout_for_test(&mut vm);
- assert_eq!(vm.eval(query).unwrap(), result, "paint: {doctype}");
- }
-}
-
-#[test]
-fn inline_end_border_preserves_tall_atomic_content() {
- for doctype in ["", ""] {
- let mut vm = new_parsed_test_vm(
- "https://inline-end-border-height.test/",
- &format!(
- r#"{doctype}
"#
- ),
- );
- let result = vm.eval("JSON.stringify(Array.from(document.querySelectorAll('.line'), e=>e.getBoundingClientRect().height))").unwrap();
- let heights: Vec = serde_json::from_str(&result).unwrap();
- assert!(heights[0] >= 200.0, "{result}");
- assert_eq!(
- heights[0], heights[1],
- "an end edge must retain earlier content bounds"
- );
- }
-}
-
#[test]
fn iframe_scrolling_attribute_suppresses_only_viewport_scrollbars() {
for (scrolling, suppressed) in [
diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/inline_layout.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/inline_layout.rs
new file mode 100644
index 0000000000..cb0e70acae
--- /dev/null
+++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/inline_layout.rs
@@ -0,0 +1,990 @@
+use super::*;
+
+#[test]
+fn quirks_block_line_height_preserves_text_and_atomic_alignment() {
+ for (doctype, quirks) in [
+ ("", true),
+ (
+ r#""#,
+ true,
+ ),
+ ("", false),
+ ] {
+ let mut vm = new_parsed_test_vm(
+ "https://quirks-block-line-height.test/",
+ &format!(
+ r#"{doctype}
+
+ x
+ x
+
+
"#
+ ),
+ );
+ let query = r#"JSON.stringify(['only','text','smaller','aligned','spaced'].map(id => document.getElementById(id).getBoundingClientRect().height))"#;
+ let expected = if quirks {
+ "[20,80,10,20,20]"
+ } else {
+ "[80,80,80,80,80]"
+ };
+ assert_eq!(vm.eval(query).unwrap(), expected, "{doctype}");
+ publish_layout_for_test(&mut vm);
+ assert_eq!(vm.eval(query).unwrap(), expected, "paint: {doctype}");
+ }
+}
+
+fn check_inline_line_height_modes(cases: &[(&str, &str, f64, f64)]) {
+ for (doctype, quirks) in [
+ ("", true),
+ (
+ r#""#,
+ true,
+ ),
+ ("", false),
+ ] {
+ let mut markup = format!(
+ r#"{doctype}"#
+ );
+ for (id, content, _, _) in cases {
+ markup.push_str(&format!(r#"{content}
"#));
+ }
+ let mut vm = new_parsed_test_vm("https://quirks-empty-inline.test/", &markup);
+ let query = "JSON.stringify(Array.from(document.querySelectorAll('.line'), e=>e.getBoundingClientRect().height))";
+ let result = vm.eval(query).unwrap();
+ let heights: Vec = serde_json::from_str(&result).unwrap();
+ for ((id, _, quirks_height, standards_height), actual) in cases.iter().zip(heights) {
+ assert_eq!(
+ actual,
+ if quirks {
+ *quirks_height
+ } else {
+ *standards_height
+ },
+ "{doctype} / {id}: {result}"
+ );
+ }
+ publish_layout_for_test(&mut vm);
+ assert_eq!(
+ vm.eval(query).unwrap(),
+ result,
+ "publication uses the same line height rules"
+ );
+ }
+}
+
+#[test]
+fn quirks_empty_inline_line_height_uses_text_and_fragment_edges() {
+ check_inline_line_height_modes(&[
+ ("direct", r#""#, 20.0, 80.0),
+ (
+ "wrapped",
+ r#""#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "nested",
+ r#""#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "collapsed",
+ " \n \n ",
+ 20.0,
+ 80.0,
+ ),
+ (
+ "small_descendant_text",
+ r#"x"#,
+ 10.0,
+ 80.0,
+ ),
+ (
+ "own_text",
+ r#"x"#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "between_atoms",
+ r#" "#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "preserved_space",
+ r#" "#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "preserved_break",
+ r#"
+"#,
+ 80.0,
+ 80.0,
+ ),
+ ("br", r#"
"#, 80.0, 80.0),
+ (
+ "atomic_text",
+ r#"x"#,
+ 10.0,
+ 80.0,
+ ),
+ (
+ "empty_next_to_atom",
+ r#""#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "bidi",
+ r#""#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "nested_small_atom",
+ r#""#,
+ 20.0,
+ 160.0,
+ ),
+ ]);
+}
+
+#[test]
+fn quirks_empty_inline_line_height_distinguishes_border_padding_and_margin() {
+ check_inline_line_height_modes(&[
+ (
+ "margin_only",
+ r#""#,
+ 0.0,
+ 80.0,
+ ),
+ (
+ "padding_block",
+ r#""#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "border_block",
+ r#""#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "padding_left",
+ r#""#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "padding_right",
+ r#""#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "border_left",
+ r#""#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "border_right",
+ r#""#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "hidden_border",
+ r#""#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "none_border",
+ r#""#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "cancelled_edge",
+ r#""#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "padding_zero_percent",
+ r#""#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "padding_percent",
+ r#""#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "margin_next_to_atom",
+ r#""#,
+ 20.0,
+ 80.0,
+ ),
+ ]);
+}
+
+#[test]
+fn quirks_empty_inline_line_height_is_resolved_per_line() {
+ check_inline_line_height_modes(&[
+ (
+ "multiple_lines",
+ r#"
"#,
+ 40.0,
+ 160.0,
+ ),
+ (
+ "text_first_line",
+ r#"x
"#,
+ 100.0,
+ 160.0,
+ ),
+ (
+ "text_last_line",
+ r#"
x"#,
+ 100.0,
+ 160.0,
+ ),
+ (
+ "border_multiple_lines",
+ r#"
"#,
+ 100.0,
+ 160.0,
+ ),
+ (
+ "space_at_break",
+ r#"
"#,
+ 40.0,
+ 160.0,
+ ),
+ (
+ "text_elsewhere",
+ r#"x
"#,
+ 100.0,
+ 160.0,
+ ),
+ (
+ "middle_atom",
+ r#""#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "top_wrapper",
+ r#""#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "bottom_wrapper",
+ r#""#,
+ 20.0,
+ 80.0,
+ ),
+ ]);
+}
+
+#[test]
+fn quirks_empty_inline_line_height_preserves_forced_empty_lines() {
+ check_inline_line_height_modes(&[
+ (
+ "large_preserved_break",
+ r#"
+"#,
+ 100.0,
+ 100.0,
+ ),
+ (
+ "large_br",
+ r#"
"#,
+ 100.0,
+ 100.0,
+ ),
+ ("root_br", r#"
"#, 80.0, 80.0),
+ (
+ "large_nested_br",
+ r#"
"#,
+ 10.0,
+ 160.0,
+ ),
+ (
+ "br_own_font",
+ r#"
"#,
+ 100.0,
+ 80.0,
+ ),
+ (
+ "atom_large_br",
+ r#"
"#,
+ 20.0,
+ 100.0,
+ ),
+ (
+ "atom_large_preserved_break",
+ r#"
+"#,
+ 20.0,
+ 100.0,
+ ),
+ (
+ "atom_small_br",
+ r#"
"#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "margin_then_br",
+ r#"
"#,
+ 100.0,
+ 80.0,
+ ),
+ (
+ "padding_then_br",
+ r#"
"#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "padding_with_br",
+ r#"
"#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "wrapped_auto_break",
+ r#" "#,
+ 40.0,
+ 160.0,
+ ),
+ (
+ "collapsed_large_trailing_space",
+ r#" "#,
+ 20.0,
+ 160.0,
+ ),
+ (
+ "border_empty",
+ r#""#,
+ 80.0,
+ 80.0,
+ ),
+ ]);
+}
+
+#[test]
+fn quirks_empty_inline_line_height_collapses_spaces_after_wrapping() {
+ check_inline_line_height_modes(&[
+ (
+ "wrapped_auto_break",
+ r#" "#,
+ 40.0,
+ 160.0,
+ ),
+ (
+ "wrapped_auto_break_preserved",
+ r#" "#,
+ 100.0,
+ 160.0,
+ ),
+ (
+ "wrapped_auto_break_rtl",
+ r#" "#,
+ 40.0,
+ 160.0,
+ ),
+ (
+ "wrapped_auto_break_space_font",
+ r#" "#,
+ 40.0,
+ 240.0,
+ ),
+ (
+ "wrapped_space_font_then_text",
+ r#" x"#,
+ 100.0,
+ 240.0,
+ ),
+ (
+ "wrapped_space_font_then_zero_text",
+ r#" x"#,
+ 20.0,
+ 240.0,
+ ),
+ ]);
+}
+
+#[test]
+fn quirks_nested_break_line_height_uses_its_own_inline_box() {
+ check_inline_line_height_modes(&[
+ (
+ "nested_br_padding",
+ r#"
"#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "nested_br_border",
+ r#"
"#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "nested_br_text",
+ r#"x
"#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "nested_br_atom_sibling",
+ r#"
"#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "nested_br_atom_in_parent",
+ r#"
"#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "nested_br_atom_in_same_box",
+ r#"
"#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "nested_br_descendant_text",
+ r#"x
"#,
+ 10.0,
+ 160.0,
+ ),
+ (
+ "nested_br_own_small_text",
+ r#"x
"#,
+ 10.0,
+ 80.0,
+ ),
+ (
+ "nested_preserved_break",
+ r#"
+"#,
+ 80.0,
+ 80.0,
+ ),
+ ]);
+}
+
+#[test]
+fn quirks_wrapped_whitespace_line_height_counts_only_surviving_spaces() {
+ check_inline_line_height_modes(&[
+ (
+ "wrapped_space",
+ r#" "#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "nested_wrapped_space",
+ r#" "#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "wrapped_space_large_font",
+ r#" "#,
+ 160.0,
+ 160.0,
+ ),
+ (
+ "wrapped_space_small_font",
+ r#" "#,
+ 22.0,
+ 80.0,
+ ),
+ (
+ "wrapped_space_rtl",
+ r#" "#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "wrapped_space_preserved",
+ r#" "#,
+ 80.0,
+ 80.0,
+ ),
+ (
+ "wrapped_space_leading",
+ r#" "#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "wrapped_space_trailing",
+ r#" "#,
+ 20.0,
+ 80.0,
+ ),
+ (
+ "wrapped_space_at_break",
+ r#"
"#,
+ 40.0,
+ 160.0,
+ ),
+ (
+ "wrapped_space_after_wrap",
+ r#" "#,
+ 40.0,
+ 160.0,
+ ),
+ ]);
+}
+
+#[test]
+fn inline_wrapped_whitespace_preserves_sibling_spacing() {
+ for doctype in [
+ "",
+ r#""#,
+ "",
+ ] {
+ let mut vm = new_parsed_test_vm(
+ "https://inline-wrapped-whitespace.test/",
+ &format!(
+ r#"{doctype}
+
+
+
+
"#
+ ),
+ );
+ let query = r#"JSON.stringify([
+ ...['plain','wrapped','nested'].map(id => {
+ const atoms = document.getElementById(id).querySelectorAll('.atom');
+ return atoms[1].getBoundingClientRect().left - atoms[0].getBoundingClientRect().right;
+ }),
+ edges.querySelector('.atom').getBoundingClientRect().left - edges.getBoundingClientRect().left
+ ])"#;
+ let result = vm.eval(query).unwrap();
+ let gaps: Vec = serde_json::from_str(&result).unwrap();
+ assert!(gaps[0] > 0.0, "{doctype}: {result}");
+ assert_eq!(gaps[1], gaps[0], "a wrapper must retain the space");
+ assert_eq!(gaps[2], gaps[0], "nested wrappers must retain the space");
+ assert_eq!(gaps[3], 0.0, "a leading wrapped space still collapses");
+ publish_layout_for_test(&mut vm);
+ assert_eq!(vm.eval(query).unwrap(), result, "paint: {doctype}");
+ }
+}
+
+#[test]
+fn inline_closing_edges_after_forced_breaks_contribute_to_their_own_fragment() {
+ let cases = [
+ (
+ "left_border",
+ r#"
"#,
+ 160.0,
+ 160.0,
+ true,
+ ),
+ (
+ "right_border",
+ r#"
"#,
+ 160.0,
+ 160.0,
+ true,
+ ),
+ (
+ "right_padding",
+ r#"
"#,
+ 160.0,
+ 160.0,
+ true,
+ ),
+ (
+ "terminal_break",
+ r#"
"#,
+ 80.0,
+ 80.0,
+ true,
+ ),
+ (
+ "preserved_break",
+ "\n
",
+ 160.0,
+ 160.0,
+ true,
+ ),
+ (
+ "nested_closing_edges",
+ r#"
"#,
+ 160.0,
+ 160.0,
+ true,
+ ),
+ (
+ "nested_bidi_closing_edges",
+ r#"
"#,
+ 100.0,
+ 160.0,
+ false,
+ ),
+ (
+ "nested_bidi_terminal_padding",
+ r#"
"#,
+ 100.0,
+ 160.0,
+ false,
+ ),
+ (
+ "nested_bidi_terminal_border",
+ r#"
"#,
+ 100.0,
+ 160.0,
+ false,
+ ),
+ (
+ "nested_bidi_terminal_margin",
+ r#"
"#,
+ 20.0,
+ 160.0,
+ false,
+ ),
+ (
+ "multiple_fragments",
+ r#"
"#,
+ 180.0,
+ 240.0,
+ false,
+ ),
+ (
+ "opening_edge_after_break",
+ r#"
"#,
+ 100.0,
+ 160.0,
+ false,
+ ),
+ (
+ "undecorated",
+ r#"
"#,
+ 100.0,
+ 160.0,
+ false,
+ ),
+ (
+ "right_margin",
+ r#"
"#,
+ 100.0,
+ 160.0,
+ false,
+ ),
+ ];
+ for (doctype, quirks) in [
+ ("", true),
+ (
+ r#""#,
+ true,
+ ),
+ ("", false),
+ ] {
+ let mut markup = format!(
+ r#"{doctype}"#
+ );
+ for (id, content, ..) in &cases {
+ markup.push_str(&format!(r#"{content}
"#));
+ }
+ let mut vm = new_parsed_test_vm("https://inline-closing-edge.test/", &markup);
+ let query = r#"JSON.stringify(Array.from(document.querySelectorAll('.line'), e => {
+ const line = e.getBoundingClientRect();
+ const atom = e.querySelector('.atom').getBoundingClientRect();
+ return [line.height, atom.top - line.top];
+ }))"#;
+ let result = vm.eval(query).unwrap();
+ let geometries: Vec<[f64; 2]> = serde_json::from_str(&result).unwrap();
+ let decorated_atom_top = geometries[0][1];
+ for ((id, _, quirks_height, standards_height, decorated), actual) in
+ cases.iter().zip(geometries)
+ {
+ assert_eq!(
+ actual[0],
+ if quirks {
+ *quirks_height
+ } else {
+ *standards_height
+ },
+ "height: {doctype} / {id}: {result}"
+ );
+ assert_eq!(
+ actual[1],
+ if !quirks || *decorated {
+ decorated_atom_top
+ } else {
+ 0.0
+ },
+ "atom top: {doctype} / {id}"
+ );
+ }
+ for (id, fragments) in [
+ ("right_border", 1),
+ ("right_padding", 1),
+ ("terminal_break", 1),
+ ("nested_bidi_closing_edges", 2),
+ ("nested_bidi_terminal_padding", 2),
+ ("nested_bidi_terminal_border", 2),
+ ("nested_bidi_terminal_margin", 2),
+ ] {
+ assert_eq!(
+ vm.eval(&format!(
+ "document.getElementById({id:?}).querySelector('span').getClientRects().length"
+ ))
+ .unwrap(),
+ fragments.to_string(),
+ "closing edge fragment count: {doctype} / {id}"
+ );
+ }
+ let margin_fragment_query = r#"(() => {
+ const rects = document.getElementById('nested_bidi_terminal_margin').querySelector('span').getClientRects();
+ return rects[1].height / rects[0].height;
+ })()"#;
+ let margin_fragment_ratio = vm.eval(margin_fragment_query).unwrap();
+ assert_eq!(margin_fragment_ratio, if quirks { "0" } else { "1" });
+ publish_layout_for_test(&mut vm);
+ assert_eq!(vm.eval(query).unwrap(), result, "paint: {doctype}");
+ assert_eq!(
+ vm.eval(margin_fragment_query).unwrap(),
+ margin_fragment_ratio,
+ "margin fragment paint: {doctype}"
+ );
+ }
+}
+
+#[test]
+fn inline_used_closing_edges_preserve_standards_struts_and_ancestors() {
+ check_inline_closing_fragment_modes(&[
+ (
+ "margin_tall",
+ r#"
"#,
+ 20.0,
+ 320.0,
+ 0.0,
+ ),
+ (
+ "padding_tall",
+ r#"
"#,
+ 180.0,
+ 320.0,
+ 1.0,
+ ),
+ (
+ "border_tall",
+ r#"
"#,
+ 180.0,
+ 320.0,
+ 1.0,
+ ),
+ (
+ "margin_nested",
+ r#"
"#,
+ 20.0,
+ 400.0,
+ 0.0,
+ ),
+ ]);
+}
+
+#[test]
+fn standards_closing_inline_struts_do_not_depend_on_edge_sizes() {
+ check_inline_line_height_modes(&[
+ (
+ "plain_tall_text",
+ r#"
x"#,
+ 100.0,
+ 320.0,
+ ),
+ (
+ "plain_tall_br",
+ r#"
"#,
+ 100.0,
+ 320.0,
+ ),
+ (
+ "plain_tall_terminal",
+ r#"
"#,
+ 20.0,
+ 160.0,
+ ),
+ (
+ "margin_tall_text",
+ r#"
x"#,
+ 100.0,
+ 320.0,
+ ),
+ (
+ "plain_nested_text",
+ r#"
x"#,
+ 100.0,
+ 400.0,
+ ),
+ (
+ "plain_nested_br",
+ r#"
"#,
+ 100.0,
+ 400.0,
+ ),
+ (
+ "plain_nested_terminal",
+ r#"
"#,
+ 20.0,
+ 200.0,
+ ),
+ (
+ "plain_tall_atom",
+ r#"
"#,
+ 40.0,
+ 320.0,
+ ),
+ (
+ "plain_tall_zero_atom",
+ r#"
"#,
+ 20.0,
+ 320.0,
+ ),
+ (
+ "plain_tall_zero_text",
+ r#"
x"#,
+ 20.0,
+ 320.0,
+ ),
+ (
+ "plain_tall_zero_br",
+ r#"
"#,
+ 20.0,
+ 320.0,
+ ),
+ ]);
+}
+
+#[test]
+fn quirks_closing_fragment_font_boxes_follow_line_content() {
+ check_inline_closing_fragment_modes(&[
+ (
+ "margin_br",
+ r#"
"#,
+ 100.0,
+ 160.0,
+ 1.0,
+ ),
+ (
+ "margin_text",
+ r#"
x"#,
+ 100.0,
+ 160.0,
+ 1.0,
+ ),
+ (
+ "margin_atom",
+ r#"
"#,
+ 40.0,
+ 160.0,
+ 1.0,
+ ),
+ (
+ "margin_zero_atom",
+ r#"
"#,
+ 20.0,
+ 160.0,
+ 1.0,
+ ),
+ (
+ "margin_zero_text",
+ r#"
x"#,
+ 20.0,
+ 160.0,
+ 1.0,
+ ),
+ (
+ "margin_zero_br",
+ r#"
"#,
+ 20.0,
+ 160.0,
+ 1.0,
+ ),
+ ]);
+}
+
+fn check_inline_closing_fragment_modes(cases: &[(&str, &str, f64, f64, f64)]) {
+ for (doctype, quirks) in [
+ ("", true),
+ (
+ r#""#,
+ true,
+ ),
+ ("", false),
+ ] {
+ let mut markup = format!(
+ r#"{doctype}"#
+ );
+ for (id, content, ..) in cases {
+ markup.push_str(&format!(r#"{content}
"#));
+ }
+ let mut vm = new_parsed_test_vm("https://inline-closing-fragment.test/", &markup);
+ let query = r#"JSON.stringify(Array.from(document.querySelectorAll('.line'), e => {
+ const rects = e.querySelector('span').getClientRects();
+ return [e.getBoundingClientRect().height, rects.length, rects[1].height / rects[0].height];
+ }))"#;
+ let result = vm.eval(query).unwrap();
+ let geometries: Vec<[f64; 3]> = serde_json::from_str(&result).unwrap();
+ for ((id, _, quirks_height, standards_height, quirks_ratio), actual) in
+ cases.iter().zip(geometries)
+ {
+ assert_eq!(
+ actual,
+ [
+ if quirks {
+ *quirks_height
+ } else {
+ *standards_height
+ },
+ 2.0,
+ if quirks { *quirks_ratio } else { 1.0 },
+ ],
+ "closing fragment: {doctype} / {id}: {result}"
+ );
+ }
+ publish_layout_for_test(&mut vm);
+ assert_eq!(vm.eval(query).unwrap(), result, "paint: {doctype}");
+ }
+}
+
+#[test]
+fn inline_end_border_preserves_tall_atomic_content() {
+ for doctype in ["", ""] {
+ let mut vm = new_parsed_test_vm(
+ "https://inline-end-border-height.test/",
+ &format!(
+ r#"{doctype}
"#
+ ),
+ );
+ let result = vm.eval("JSON.stringify(Array.from(document.querySelectorAll('.line'), e=>e.getBoundingClientRect().height))").unwrap();
+ let heights: Vec = serde_json::from_str(&result).unwrap();
+ assert!(heights[0] >= 200.0, "{result}");
+ assert_eq!(
+ heights[0], heights[1],
+ "an end edge must retain earlier content bounds"
+ );
+ }
+}