use velox_dom::layout::compute_layout;
use velox_dom::{Props, VNode};
fn el(style: &str, kids: Vec<VNode>) -> VNode {
VNode::Element {
tag: "div".to_string(),
props: Props::from_inline(style),
children: kids,
}
}
fn root_rect(style: &str) -> (i32, i32, i32, i32) {
let lo = compute_layout(&el(style, vec![]), 1024, 768);
let r = lo.rect;
(r.x, r.y, r.w, r.h)
}
#[test]
fn first_occurrence_wins_for_lookup_helpers() {
assert_eq!(
root_rect("width: 10px; width: 99px; height: 10px; height: 99px"),
(0, 0, 10, 10)
);
}
#[test]
fn margin_shorthand_takes_the_last_occurrence() {
assert_eq!(
root_rect("margin: 0 10px; margin: 0 auto; width: 100px; height: 20px"),
(462, 0, 100, 20),
"the later `margin: 0 auto` wins, so the box is centred"
);
assert_eq!(
root_rect("margin: 0 auto; margin: 0 10px; width: 100px; height: 20px"),
(0, 0, 100, 20),
"the later non-auto `margin` wins, so the box is not centred"
);
}
#[test]
fn margin_longhand_value_takes_the_first_occurrence() {
let (x, ..) = root_rect(
"margin-left: 1px; margin-left: 7px; margin-right: 2px; margin-right: 9px; width: 40px; height: 10px",
);
assert_eq!(x, 1, "first margin-left wins for the resolved value");
}
#[test]
fn shorthand_expansion_keeps_all_four_value_forms() {
assert_eq!(root_rect("margin: 1px; width: 100px").0, 1);
assert_eq!(root_rect("margin: 1px 2px; width: 100px").0, 2);
assert_eq!(root_rect("margin: 1px 2px 3px; width: 100px").0, 2);
assert_eq!(root_rect("margin: 1px 2px 3px 4px; width: 100px").0, 4);
assert_eq!(root_rect("margin: 1px 2px 3px 4px 5px; width: 100px").0, 0);
}
#[test]
fn margin_auto_centres() {
let want = (462, 0, 100, 768);
for s in [
"margin: auto; width: 100px",
"margin: 0 auto; width: 100px",
"margin: 0 auto 0 auto; width: 100px",
] {
assert_eq!(root_rect(s), want, "style: {s}");
}
}
#[test]
fn longhand_overrides_shorthand_in_either_order() {
let want = (0, 0, 122, 54);
assert_eq!(
root_rect("padding: 1px 2px 3px 4px; padding-left: 20px; width: 100px; height: 50px"),
want
);
assert_eq!(
root_rect("padding-left: 20px; padding: 1px 2px 3px 4px; width: 100px; height: 50px"),
want
);
}
#[test]
fn all_length_units_resolve_against_their_own_input() {
assert_eq!(
root_rect(
"font-size: 20px; padding: 1em; width: 50%; height: 10vh; min-height: 5dvh; \
max-width: 10dvw; border-width: 2dvw; border-top-width: 1rem"
),
(0, 0, 174, 145)
);
}
#[test]
fn same_style_under_different_containing_blocks_resolves_per_parent() {
let tree = el(
"width: 400px; height: 10px",
vec![
el("width: 50%; height: 5px", vec![]),
el("width: 50%; height: 5px", vec![]),
],
);
let lo = compute_layout(&tree, 1024, 768);
assert_eq!(lo.children[0].rect.w, 200);
assert_eq!(lo.children[1].rect.w, 200);
let tree = el(
"width: 400px; height: 100px",
vec![
el(
"width: 50%; height: 50px",
vec![el("width: 25%; height: 10px", vec![])],
),
el(
"width: 200px; height: 50px",
vec![el("width: 25%; height: 10px", vec![])],
),
],
);
let lo = compute_layout(&tree, 1024, 768);
let a = lo.children[0].children[0].rect.w;
let b = lo.children[1].children[0].rect.w;
assert_eq!(a, 50, "25% of 200");
assert_eq!(b, 50, "25% of 200");
assert_eq!(lo.children[0].rect.w, 200);
assert_eq!(lo.children[1].rect.w, 200);
}
#[test]
fn percentage_follows_its_own_parent_not_an_ancestor() {
let tree = el(
"width: 400px; height: 200px",
vec![
el("width: 50%; height: 20px", vec![]),
el(
"width: 100px; height: 20px",
vec![el("width: 50%; height: 20px", vec![])],
),
],
);
let lo = compute_layout(&tree, 1024, 768);
assert_eq!(lo.children[0].rect.w, 200, "50% of 400");
assert_eq!(
lo.children[1].children[0].rect.w, 50,
"50% of 100, not of 400"
);
}
#[test]
fn em_uses_parent_font_size_not_own() {
let tree = el(
"font-size: 10px; width: 200px; height: 60px",
vec![
el("font-size: 30px; padding: 1em; height: 5px", vec![]),
el("font-size: 40px; padding: 1em; height: 5px", vec![]),
],
);
let lo = compute_layout(&tree, 1024, 768);
assert_eq!(lo.children[0].rect.h, 25, "5 + 2*10 (parent font size)");
assert_eq!(lo.children[1].rect.h, 25, "5 + 2*10 (parent font size)");
}
#[test]
fn em_inherits_from_parent_font_size() {
let tree = el(
"font-size: 10px; width: 200px; height: 200px",
vec![
el(
"font-size: 30px; width: 200px; height: 50px",
vec![el("padding: 1em; width: 50px; height: 5px", vec![])],
),
el(
"font-size: 40px; width: 200px; height: 50px",
vec![el("padding: 1em; width: 50px; height: 5px", vec![])],
),
],
);
let lo = compute_layout(&tree, 1024, 768);
let a = lo.children[0].children[0].rect;
let b = lo.children[1].children[0].rect;
assert_eq!((a.w, a.h), (110, 65), "50 + 2*30, 5 + 2*30");
assert_eq!((b.w, b.h), (130, 85), "50 + 2*40, 5 + 2*40");
assert_ne!(
a.w, b.w,
"the same style string must resolve per parent font size"
);
}
#[test]
fn rem_ignores_parent_font_size() {
let tree = el(
"font-size: 10px; width: 300px; height: 200px",
vec![
el(
"font-size: 30px; width: 300px; height: 20px",
vec![el("padding: 1rem; width: 40px; height: 5px", vec![])],
),
el(
"font-size: 40px; width: 300px; height: 20px",
vec![el("padding: 1rem; width: 40px; height: 5px", vec![])],
),
],
);
let lo = compute_layout(&tree, 1024, 768);
for (i, c) in [0usize, 1].iter().enumerate() {
let g = lo.children[*c].children[0].rect;
assert_eq!((g.w, g.h), (72, 37), "case {i}: 40 + 2*16, 5 + 2*16");
}
}
#[test]
fn viewport_units_use_the_viewport() {
for s in [
"width: 50vw; height: 25vh; width: 200px",
"width: 50dvw; height: 25dvh; width: 200px",
] {
let lo = compute_layout(
&el(s, vec![el("width: 10vw; height: 5px", vec![])]),
1024,
768,
);
assert_eq!((lo.rect.w, lo.rect.h), (512, 192), "style: {s}");
assert_eq!(lo.children[0].rect.w, 102, "10vw of 1024, style: {s}");
}
}
#[test]
fn percentage_memo_is_keyed_on_the_containing_block() {
let tree = el(
"width: 400px; height: 200px",
vec![
el(
"width: 200px; height: 20px",
vec![el("padding: 25%; width: 10px; height: 5px", vec![])],
),
el(
"width: 80px; height: 20px",
vec![el("padding: 25%; width: 10px; height: 5px", vec![])],
),
],
);
let lo = compute_layout(&tree, 1024, 768);
let a = lo.children[0].children[0].rect;
let b = lo.children[1].children[0].rect;
assert_eq!((a.w, a.h), (110, 105), "25% of 200 => 50; 10+2*50, 5+2*50");
assert_eq!((b.w, b.h), (50, 45), "25% of 80 => 20; 10+2*20, 5+2*20");
assert_ne!(
a.w, b.w,
"identical style strings, different containing blocks"
);
}
#[test]
fn border_percentage_memo_is_keyed_on_the_containing_block() {
let tree = el(
"width: 400px; height: 200px",
vec![
el(
"width: 200px; height: 20px",
vec![el("border-width: 5%; width: 20px; height: 10px", vec![])],
),
el(
"width: 100px; height: 20px",
vec![el("border-width: 5%; width: 20px; height: 10px", vec![])],
),
],
);
let lo = compute_layout(&tree, 1024, 768);
let a = lo.children[0].children[0].rect;
let b = lo.children[1].children[0].rect;
assert_eq!((a.w, a.h), (40, 30), "5% of 200 => 10");
assert_eq!((b.w, b.h), (30, 20), "5% of 100 => 5");
}
#[test]
fn border_width_shorthand_and_longhand() {
assert_eq!(
root_rect("border: 3px solid red; width: 100px; height: 30px"),
(0, 0, 106, 36)
);
assert_eq!(
root_rect("border-width: 2px; width: 100px; height: 30px"),
(0, 0, 104, 34)
);
assert_eq!(
root_rect(
"border-left-width: 1px; border-top-width: 2px; border-right-width: 3px; \
border-bottom-width: 4px; width: 100px; height: 30px"
),
(0, 0, 104, 36)
);
assert_eq!(
root_rect("border: 2px solid red; border-left-width: 7px; width: 100px; height: 30px"),
(0, 0, 109, 34)
);
assert_eq!(
root_rect(
"border-left: 5px solid red; border-top: 6px solid red; width: 100px; height: 30px"
),
(0, 0, 105, 36)
);
assert_eq!(
root_rect("border-width: 10%; width: 200px; height: 30px"),
(0, 0, 404, 234)
);
}
#[test]
fn malformed_declarations_are_skipped() {
assert_eq!(
root_rect(";; ; width : 20px ;;height: 12px; ; nonsense ; width: 3px;"),
(0, 0, 20, 12),
"empty decls and colon-less decls are skipped; the key is trimmed"
);
assert_eq!(root_rect("width:; height:; width: 8px"), (0, 0, 1024, 768));
assert_eq!(root_rect(""), (0, 0, 1024, 768));
}
#[test]
fn property_names_are_case_sensitive() {
assert_eq!(
root_rect("Width: 50px; width: 7px; HEIGHT: 9px; height: 8px"),
(0, 0, 7, 8)
);
}
#[test]
fn identical_sibling_styles_share_a_cache_entry() {
let tree = el(
"width: 300px; height: 10px",
vec![
el("padding: 10%; width: 50px", vec![el("height: 4px", vec![])]),
el("padding: 10%; width: 50px", vec![el("height: 4px", vec![])]),
el("padding: 10%; width: 50px", vec![el("height: 4px", vec![])]),
],
);
let lo = compute_layout(&tree, 1024, 768);
let w0 = lo.children[0].rect.w;
for (i, c) in lo.children.iter().enumerate() {
assert_eq!(c.rect.w, w0, "sibling {i} shares the style string");
}
}
fn lay(n: &VNode, vw: i32, vh: i32) -> Vec<(i32, i32, i32, i32)> {
compute_layout(n, vw, vh)
.children
.iter()
.map(|c| (c.rect.x, c.rect.y, c.rect.w, c.rect.h))
.collect()
}
fn lay_root(n: &VNode, vw: i32, vh: i32) -> (i32, i32, i32, i32) {
let r = compute_layout(n, vw, vh).rect;
(r.x, r.y, r.w, r.h)
}
#[test]
fn the_same_string_under_two_parent_font_sizes_resolves_twice() {
let mk = |pfs: &str| {
el(
&format!("font-size: {pfs}; width: 400px; height: 5px"),
vec![el("width: 2em; height: 3px", vec![])],
)
};
assert_eq!(lay(&mk("20px"), 1024, 768), vec![(0, 0, 40, 3)]);
assert_eq!(lay(&mk("40px"), 1024, 768), vec![(0, 0, 80, 3)]);
}
#[test]
fn a_resize_invalidates_viewport_units() {
let t = el("width: 10vw; height: 4px", vec![]);
assert_eq!(lay_root(&t, 1000, 500), (0, 0, 100, 4));
assert_eq!(lay_root(&t, 2000, 500), (0, 0, 200, 4));
assert_eq!(lay_root(&t, 3000, 500), (0, 0, 300, 4));
}
#[test]
fn a_resize_invalidates_dynamic_viewport_units() {
let t = el("width: 10dvw; height: 5dvh", vec![]);
assert_eq!(lay_root(&t, 1024, 768), (0, 0, 102, 38));
assert_eq!(lay_root(&t, 2048, 1536), (0, 0, 205, 77));
}
#[test]
fn a_parent_font_size_change_invalidates_the_border_memo() {
let mk = |pfs: &str| {
el(
&format!("font-size: {pfs}; width: 400px; height: 5px"),
vec![el("border-width: 2em; width: 200px; height: 60px", vec![])],
)
};
assert_eq!(lay(&mk("20px"), 1024, 768), vec![(0, 0, 280, 140)]);
assert_eq!(lay(&mk("40px"), 1024, 768), vec![(0, 0, 360, 220)]);
}
#[test]
fn a_resize_invalidates_viewport_units_in_the_border_memo() {
let t = el("border-width: 2vw; width: 400px; height: 60px", vec![]);
assert_eq!(lay_root(&t, 1024, 768), (0, 0, 440, 100));
assert_eq!(lay_root(&t, 2048, 1536), (0, 0, 482, 142));
}
#[test]
fn a_resize_invalidates_viewport_units_in_the_padding_memo() {
let t = el("padding: 2vw; width: 400px; height: 60px", vec![]);
assert_eq!(lay_root(&t, 1024, 768), (0, 0, 440, 100));
assert_eq!(lay_root(&t, 2048, 1536), (0, 0, 482, 142));
}
#[test]
fn a_parent_font_size_change_invalidates_the_font_size_memo() {
let mk = |pfs: &str| {
el(
&format!("font-size: {pfs}; width: 400px; height: 5px"),
vec![el(
"font-size: 2em; padding: 1em; width: 50px; height: 5px",
vec![],
)],
)
};
assert_eq!(lay(&mk("20px"), 1024, 768), vec![(0, 0, 90, 45)]);
assert_eq!(lay(&mk("40px"), 1024, 768), vec![(0, 0, 130, 85)]);
}
#[test]
fn a_resize_invalidates_viewport_units_in_the_font_size_memo() {
let t = || {
el(
"font-size: 5vw; width: 400px; height: 5px",
vec![el("padding: 1em; width: 50px; height: 5px", vec![])],
)
};
assert_eq!(lay(&t(), 1024, 768), vec![(0, 0, 152, 107)]);
assert_eq!(lay(&t(), 2048, 1536), vec![(0, 0, 254, 209)]);
}
#[test]
fn rem_ignores_the_roots_own_font_size_because_root_font_size_is_constant() {
let a = el(
"font-size: 20px; width: 400px; height: 5px",
vec![el("padding: 1rem; width: 40px; height: 5px", vec![])],
);
let b = el(
"font-size: 40px; width: 400px; height: 5px",
vec![el("padding: 1rem; width: 40px; height: 5px", vec![])],
);
assert_eq!(lay(&a, 1024, 768), vec![(0, 0, 72, 37)]);
assert_eq!(lay(&b, 1024, 768), vec![(0, 0, 72, 37)]);
assert_eq!(lay(&a, 1024, 768), lay(&b, 1024, 768));
}
#[test]
fn rem_in_a_border_is_constant_because_root_font_size_is_constant() {
let mk = |root_fs: &str| {
el(
&format!("font-size: {root_fs}; width: 400px; height: 5px"),
vec![el("border-width: 2rem; width: 200px; height: 60px", vec![])],
)
};
assert_eq!(lay(&mk("20px"), 1024, 768), vec![(0, 0, 264, 124)]);
assert_eq!(lay(&mk("40px"), 1024, 768), vec![(0, 0, 264, 124)]);
}
fn lay_at(n: &VNode, depth: usize, vw: i32, vh: i32) -> (i32, i32, i32, i32) {
let mut lo = compute_layout(n, vw, vh);
for _ in 0..depth {
lo = lo.children[0].clone();
}
let r = lo.rect;
(r.x, r.y, r.w, r.h)
}
fn under_fixed_parent(child: &str) -> VNode {
el("width: 200px; height: 5px", vec![el(child, vec![])])
}
fn three_levels(pfs: &str) -> VNode {
el(
&format!("font-size: {pfs}; width: 400px; height: 5px"),
vec![el(
"font-size: 2em; width: 50px; height: 5px",
vec![el("padding: 1em; width: 10px; height: 5px", vec![])],
)],
)
}
#[test]
fn the_length_memo_is_keyed_on_the_viewport_width() {
let n = under_fixed_parent("width: 10vw; height: 4px");
assert_eq!(lay_at(&n, 1, 1000, 2000), (0, 0, 100, 4));
assert_eq!(lay_at(&n, 1, 2000, 1000), (0, 0, 200, 4));
}
#[test]
fn the_length_memo_is_keyed_on_the_viewport_height() {
let n = under_fixed_parent("width: 10px; height: 10vh");
assert_eq!(lay_at(&n, 1, 500, 1500), (0, 0, 10, 150));
assert_eq!(lay_at(&n, 1, 1500, 500), (0, 0, 10, 50));
}
#[test]
fn the_box_sides_memo_is_keyed_on_the_viewport() {
let n = under_fixed_parent("padding: 2vw; width: 10px; height: 4px");
assert_eq!(lay_at(&n, 1, 1000, 2000), (0, 0, 50, 44));
assert_eq!(lay_at(&n, 1, 2000, 1000), (0, 0, 90, 84));
}
#[test]
fn the_border_memo_is_keyed_on_the_viewport_through_the_shorthand() {
let n = under_fixed_parent("border: 2vw solid red; width: 10px; height: 4px");
assert_eq!(lay_at(&n, 1, 1000, 2000), (0, 0, 50, 44));
assert_eq!(lay_at(&n, 1, 2000, 1000), (0, 0, 90, 84));
}
#[test]
fn the_border_memo_is_keyed_on_the_viewport_through_border_width() {
let n = under_fixed_parent("border-width: 2vw; width: 10px; height: 4px");
assert_eq!(lay_at(&n, 1, 1000, 2000), (0, 0, 50, 44));
assert_eq!(lay_at(&n, 1, 2000, 1000), (0, 0, 90, 84));
}
#[test]
fn the_font_size_memo_is_keyed_on_the_parent_font_size() {
assert_eq!(lay_at(&three_levels("20px"), 2, 1024, 1024), (0, 0, 90, 85));
assert_eq!(
lay_at(&three_levels("40px"), 2, 1024, 1024),
(0, 0, 170, 165)
);
}
#[test]
fn the_font_size_memo_is_keyed_on_the_viewport() {
let n = el(
"width: 200px; height: 5px",
vec![el(
"font-size: 5vw; width: 50px; height: 5px",
vec![el("padding: 1em; width: 10px; height: 5px", vec![])],
)],
);
assert_eq!(lay_at(&n, 2, 1000, 2000), (0, 0, 110, 105));
assert_eq!(lay_at(&n, 2, 2000, 1000), (0, 0, 210, 205));
}
#[test]
fn every_viewport_unit_re_resolves_when_the_viewport_changes() {
let mk = || {
el(
"width: 400px; height: 60px",
vec![el(
"box-sizing: border-box; display: flex; flex-direction: column; \
width: 10vw; height: 10vh; min-width: 3vw; max-width: 50vw; \
min-height: 2vh; max-height: 40vh; \
margin: 1vw; margin-left: 2vw; margin-right: 3vw; \
padding: 1vw; padding-top: 2vh; padding-bottom: 2vh; \
border-width: 1vw; border: 1vw solid red; border-left-width: 2vw; \
border-top: 1vh solid red; gap: 1vw; row-gap: 1vh; \
flex: 1; flex-basis: 5vw; \
line-height: 2vw; \
left: 1vw; right: 2vw; top: 1vh; bottom: 2vh",
vec![el("width: 4px; height: 4px", vec![])],
)],
)
};
let a = lay_at(&mk(), 1, 1000, 2000);
let b = lay_at(&mk(), 1, 2000, 1000);
assert_ne!(
a, b,
"a 2x viewport change must change viewport-relative geometry"
);
assert_eq!(a, (20, 10, 100, 200));
assert_eq!(b, (40, 20, 200, 100));
}
#[test]
fn the_length_memo_is_keyed_on_viewport_width_alone() {
let n = under_fixed_parent("width: 10vw; height: 10vh; min-width: 3vw");
assert_eq!(lay_at(&n, 1, 1000, 2000), (0, 0, 100, 200));
assert_eq!(lay_at(&n, 1, 2000, 2000), (0, 0, 200, 200));
}
#[test]
fn the_length_memo_is_keyed_on_viewport_height_alone() {
let n = under_fixed_parent("width: 10vw; height: 10vh; max-height: 40vh");
assert_eq!(lay_at(&n, 1, 1000, 2000), (0, 0, 100, 200));
assert_eq!(lay_at(&n, 1, 1000, 1000), (0, 0, 100, 100));
}
#[test]
fn the_box_sides_memo_is_keyed_on_viewport_width_alone() {
let n = under_fixed_parent("padding: 2vw; width: 10px; height: 4px");
assert_eq!(lay_at(&n, 1, 1000, 2000), (0, 0, 50, 44));
assert_eq!(lay_at(&n, 1, 2000, 2000), (0, 0, 90, 84));
}
#[test]
fn the_box_sides_memo_is_keyed_on_viewport_height_alone() {
let n = under_fixed_parent("padding: 2vh; width: 10px; height: 4px");
assert_eq!(lay_at(&n, 1, 1000, 2000), (0, 0, 90, 84));
assert_eq!(lay_at(&n, 1, 1000, 1000), (0, 0, 50, 44));
}
#[test]
fn the_border_memo_is_keyed_on_viewport_width_alone() {
let n = under_fixed_parent("border: 2vw solid red; width: 10px; height: 4px");
assert_eq!(lay_at(&n, 1, 1000, 2000), (0, 0, 50, 44));
assert_eq!(lay_at(&n, 1, 2000, 2000), (0, 0, 90, 84));
}
#[test]
fn the_border_memo_is_keyed_on_viewport_height_alone() {
let n = under_fixed_parent("border-width: 2vh; width: 10px; height: 4px");
assert_eq!(lay_at(&n, 1, 1000, 2000), (0, 0, 90, 84));
assert_eq!(lay_at(&n, 1, 1000, 1000), (0, 0, 50, 44));
}
#[test]
fn the_font_size_memo_is_keyed_on_viewport_width_alone() {
let mk = || {
el(
"width: 400px; height: 400px",
vec![el(
"font-size: 5vw; width: 50px; height: 5px",
vec![el("padding: 1em; width: 10px; height: 5px", vec![])],
)],
)
};
assert_eq!(lay_at(&mk(), 2, 1000, 2000), (0, 0, 110, 105));
assert_eq!(lay_at(&mk(), 2, 2000, 2000), (0, 0, 210, 205));
}
#[test]
fn the_font_size_memo_is_keyed_on_viewport_height_alone() {
let mk = || {
el(
"width: 400px; height: 400px",
vec![el(
"font-size: 5vh; width: 50px; height: 5px",
vec![el("padding: 1em; width: 10px; height: 5px", vec![])],
)],
)
};
assert_eq!(lay_at(&mk(), 2, 1000, 2000), (0, 0, 210, 205));
assert_eq!(lay_at(&mk(), 2, 1000, 1000), (0, 0, 110, 105));
}