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" + ); + } +}