♻️ (pagetop): Props::unpack() exige &mut Context

This commit is contained in:
Manuel Cillero 2026-09-08 07:28:50 +02:00
parent ee1b48ff3f
commit 8577ca8a59
14 changed files with 280 additions and 190 deletions

View file

@ -49,92 +49,97 @@ async fn without_flex_no_style_attribute_is_added() {
#[pagetop::test]
async fn default_flex_adds_only_display_flex() {
let mut cx = Context::default();
let mut container = Container::new()
.with_flex(Flex::row())
.with_flex(Flex::new())
.with_child(Lc::n("x"));
let html = container
.render(&mut Context::default())
.await
.into_string();
let html = container.render(&mut cx).await.into_string();
let assets = cx.render_assets().into_string();
assert!(html.contains(r#"style="display: flex""#));
assert!(html.contains(r#"class="_flex_""#));
assert!(assets.contains("_flex_{display:flex}"));
}
#[pagetop::test]
async fn column_direction_adds_flex_direction_style() {
let mut cx = Context::default();
let mut container = Container::new()
.with_flex(Flex::column())
.with_flex(Flex::new().with_direction(flex::Direction::Column))
.with_child(Lc::n("x"));
let html = container
.render(&mut Context::default())
.await
.into_string();
let html = container.render(&mut cx).await.into_string();
let assets = cx.render_assets().into_string();
assert!(html.contains("display: flex"));
assert!(html.contains("flex-direction: column"));
assert!(html.contains("_flex_"));
assert!(html.contains("_flex-direction_column_"));
assert!(assets.contains("_flex_{display:flex}"));
assert!(assets.contains("_flex-direction_column_{flex-direction:column}"));
}
#[pagetop::test]
async fn wrap_justify_and_align_add_their_matching_styles() {
let mut cx = Context::default();
let mut container = Container::new()
.with_flex(
Flex::row()
Flex::new()
.with_wrap(flex::Behavior::Wrap)
.with_justify(flex::ContentJustify::Center)
.with_align(flex::Align::Center)
.with_align_content(flex::AlignContent::SpaceBetween),
)
.with_child(Lc::n("x"));
let html = container
.render(&mut Context::default())
.await
.into_string();
let html = container.render(&mut cx).await.into_string();
let assets = cx.render_assets().into_string();
assert!(html.contains("flex-wrap: wrap"));
assert!(html.contains("justify-content: center"));
assert!(html.contains("align-items: center"));
assert!(html.contains("align-content: space-between"));
assert!(html.contains("_flex-wrap_wrap_"));
assert!(html.contains("_flex-justify_center_"));
assert!(html.contains("_flex-align-items_center_"));
assert!(html.contains("_flex-align-content_space-between_"));
assert!(assets.contains("_flex-wrap_wrap_{flex-wrap:wrap}"));
assert!(assets.contains("_flex-justify_center_{justify-content:center}"));
assert!(assets.contains("_flex-align-items_center_{align-items:center}"));
assert!(assets.contains("_flex-align-content_space-between_{align-content:space-between}"));
}
#[pagetop::test]
async fn gap_both_adds_a_single_gap_style() {
let mut cx = Context::default();
let mut container = Container::new()
.with_flex(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
.with_flex(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
.with_child(Lc::n("x"));
let html = container
.render(&mut Context::default())
.await
.into_string();
let html = container.render(&mut cx).await.into_string();
let assets = cx.render_assets().into_string();
assert!(html.contains("gap: 0.5rem"));
assert!(html.contains("_flex-gap_0_5rem_"));
assert!(assets.contains("_flex-gap_0_5rem_{gap:0.5rem}"));
}
#[pagetop::test]
async fn gap_distinct_adds_row_and_column_gap_styles() {
let mut cx = Context::default();
let mut container = Container::new()
.with_flex(Flex::row().with_gap(flex::Gap::Distinct {
.with_flex(Flex::new().with_gap(flex::Gap::Distinct {
row: UnitValue::Px(4),
column: UnitValue::Px(8),
}))
.with_child(Lc::n("x"));
let html = container
.render(&mut Context::default())
.await
.into_string();
let html = container.render(&mut cx).await.into_string();
let assets = cx.render_assets().into_string();
assert!(html.contains("row-gap: 4px"));
assert!(html.contains("column-gap: 8px"));
assert!(html.contains("_flex-row-gap_4px_"));
assert!(html.contains("_flex-column-gap_8px_"));
assert!(assets.contains("_flex-row-gap_4px_{row-gap:4px}"));
assert!(assets.contains("_flex-column-gap_8px_{column-gap:8px}"));
}
#[pagetop::test]
async fn gap_none_adds_no_gap_style() {
let mut cx = Context::default();
let mut container = Container::new()
.with_flex(Flex::row())
.with_flex(Flex::new())
.with_child(Lc::n("x"));
let html = container
.render(&mut Context::default())
.await
.into_string();
let html = container.render(&mut cx).await.into_string();
let assets = cx.render_assets().into_string();
assert!(!html.contains("gap:"));
assert!(!html.contains("gap"));
assert!(!assets.contains("gap"));
}

View file

@ -17,7 +17,7 @@ async fn is_not_rendered_when_empty() {
#[pagetop::test]
async fn nav_root_class_is_unaffected_by_content_flex() {
let mut navbar = Navbar::simple()
.with_flex(Flex::row().with_justify(flex::ContentJustify::End))
.with_flex(Flex::new().with_justify(flex::ContentJustify::End))
.with_item(navbar::Item::nav(one_link_nav()));
let html = navbar.render(&mut Context::default()).await.into_string();
@ -37,32 +37,42 @@ async fn without_flex_content_area_has_no_style_attribute() {
#[pagetop::test]
async fn flex_adds_its_styles_to_the_content_area() {
let mut cx = Context::default();
let mut navbar = Navbar::simple()
.with_flex(Flex::row().with_justify(flex::ContentJustify::End))
.with_flex(Flex::new().with_justify(flex::ContentJustify::End))
.with_item(navbar::Item::nav(one_link_nav()));
let html = navbar.render(&mut Context::default()).await.into_string();
let html = navbar.render(&mut cx).await.into_string();
let assets = cx.render_assets().into_string();
assert!(html.contains(r#"class="navbar-content""#));
assert!(html.contains("display: flex"));
assert!(html.contains("justify-content: flex-end"));
assert!(html.contains("navbar-content"));
assert!(html.contains("_flex_"));
assert!(html.contains("_flex-justify_flex-end_"));
assert!(assets.contains("_flex_{display:flex}"));
assert!(assets.contains("_flex-justify_flex-end_{justify-content:flex-end}"));
}
#[pagetop::test]
async fn flex_gap_adds_a_style_to_the_content_area() {
let mut cx = Context::default();
let mut navbar = Navbar::simple()
.with_flex(Flex::row().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
.with_flex(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
.with_item(navbar::Item::nav(one_link_nav()));
let html = navbar.render(&mut Context::default()).await.into_string();
let html = navbar.render(&mut cx).await.into_string();
let assets = cx.render_assets().into_string();
assert!(html.contains("gap: 0.5rem"));
assert!(html.contains("_flex-gap_0_5rem_"));
assert!(assets.contains("_flex-gap_0_5rem_{gap:0.5rem}"));
}
// **< Navbar + FlexItem::push_end >****************************************************************
#[pagetop::test]
async fn push_end_adds_an_automatic_start_margin() {
let mut nav = one_link_nav().with_prop(FlexItem::push_end());
let html = nav.render(&mut Context::default()).await.into_string();
let mut cx = Context::default();
let mut nav = one_link_nav().with_prop(FlexItem::push_end().into());
let html = nav.render(&mut cx).await.into_string();
let assets = cx.render_assets().into_string();
assert!(html.contains(r#"style="margin-inline-start: auto""#));
assert!(html.contains("_flex-item-offset_auto_"));
assert!(assets.contains("_flex-item-offset_auto_{margin-inline-start:auto}"));
}

View file

@ -4,9 +4,9 @@ use pagetop::prelude::*;
#[pagetop::test]
async fn props_default_renders_nothing() {
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { span (Props::default().unpack(&cx)) {} }.into_string(),
html! { span (Props::default().unpack(&mut cx)) {} }.into_string(),
"<span></span>"
);
}
@ -45,9 +45,9 @@ async fn props_set_replaces_existing_value() {
async fn props_set_does_not_create_duplicate_key() {
// Reassigning the same key must replace the value, not add a duplicate entry.
let p = Props::new("key", "v1").with_prop(PropsOp::set("key", "v2"));
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(),
html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span key="v2"></span>"#
);
}
@ -57,9 +57,9 @@ async fn props_set_preserves_insertion_order() {
let p = Props::new("a", "1")
.with_prop(PropsOp::set("b", "2"))
.with_prop(PropsOp::set("c", "3"));
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(),
html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span a="1" b="2" c="3"></span>"#
);
}
@ -85,9 +85,9 @@ async fn props_remove_nonexistent_key_is_noop() {
#[pagetop::test]
async fn props_renders_nothing_after_removing_last_attr() {
let p = Props::new("only", "one").with_prop(PropsOp::remove("only"));
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(),
html! { span (p.unpack(&mut cx)) {} }.into_string(),
"<span></span>"
);
}
@ -97,9 +97,9 @@ async fn props_renders_nothing_after_removing_last_attr() {
#[pagetop::test]
async fn props_escapes_ampersand_and_angle_brackets_in_value() {
let p = Props::new("data-info", "a&b<c>d");
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(),
html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span data-info="a&amp;b&lt;c&gt;d"></span>"#
);
}
@ -107,9 +107,9 @@ async fn props_escapes_ampersand_and_angle_brackets_in_value() {
#[pagetop::test]
async fn props_escapes_double_quotes_in_value() {
let p = Props::new("data-label", r#"say "hello""#);
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(),
html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span data-label="say &quot;hello&quot;"></span>"#
);
}
@ -120,9 +120,9 @@ async fn props_escapes_double_quotes_in_value() {
async fn props_empty_in_html_macro_produces_no_attributes() {
// An empty Props must not emit even an extra blank space.
let p = Props::default();
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { button (p.unpack(&cx)) { "x" } }.into_string(),
html! { button (p.unpack(&mut cx)) { "x" } }.into_string(),
"<button>x</button>"
);
}
@ -130,9 +130,9 @@ async fn props_empty_in_html_macro_produces_no_attributes() {
#[pagetop::test]
async fn props_single_attr_in_html_macro() {
let p = Props::new("hx-get", "/api");
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { button (p.unpack(&cx)) { "Load" } }.into_string(),
html! { button (p.unpack(&mut cx)) { "Load" } }.into_string(),
r#"<button hx-get="/api">Load</button>"#
);
}
@ -142,9 +142,9 @@ async fn props_multiple_attrs_preserve_order_in_html_macro() {
let p = Props::new("hx-get", "/api")
.with_prop(PropsOp::set("hx-target", "#result"))
.with_prop(PropsOp::set("hx-swap", "outerHTML"));
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { button (p.unpack(&cx)) {} }.into_string(),
html! { button (p.unpack(&mut cx)) {} }.into_string(),
r##"<button hx-get="/api" hx-target="#result" hx-swap="outerHTML"></button>"##
);
}
@ -153,9 +153,9 @@ async fn props_multiple_attrs_preserve_order_in_html_macro() {
async fn props_alongside_class_and_id_in_html_macro() {
// The unpack is always emitted after class and id, regardless of the order they are written in.
let p = Props::new("hx-get", "/api");
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { button #mybtn .btn (p.unpack(&cx)) { "Go" } }.into_string(),
html! { button #mybtn .btn (p.unpack(&mut cx)) { "Go" } }.into_string(),
r#"<button class="btn" id="mybtn" hx-get="/api">Go</button>"#
);
}
@ -163,9 +163,9 @@ async fn props_alongside_class_and_id_in_html_macro() {
#[pagetop::test]
async fn props_alongside_named_attr_renders_after_it() {
let p = Props::new("hx-get", "/api");
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { button type="button" (p.unpack(&cx)) {} }.into_string(),
html! { button type="button" (p.unpack(&mut cx)) {} }.into_string(),
r#"<button type="button" hx-get="/api"></button>"#
);
}
@ -176,31 +176,31 @@ async fn props_combined_via_chaining_instead_of_multiple_splices() {
// is a compile error); values from separate sources are combined by chaining `with_prop()`
// on one `Props`, not by splicing two of them.
let p = Props::new("hx-get", "/api").with_prop(PropsOp::set("hx-swap", "outerHTML"));
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { button (p.unpack(&cx)) {} }.into_string(),
html! { button (p.unpack(&mut cx)) {} }.into_string(),
r#"<button hx-get="/api" hx-swap="outerHTML"></button>"#
);
}
#[pagetop::test]
async fn props_inline_construction_in_html_macro() {
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { button (Props::new("hx-get", "/api").unpack(&cx)) { "Go" } }.into_string(),
html! { button (Props::new("hx-get", "/api").unpack(&mut cx)) { "Go" } }.into_string(),
r#"<button hx-get="/api">Go</button>"#
);
}
#[pagetop::test]
async fn props_conditional_expression_in_html_macro() {
let cx = Context::default();
let mut cx = Context::default();
for (active, expected) in [
(true, r#"<button hx-get="/api">x</button>"#),
(false, "<button>x</button>"),
] {
let markup = html! {
button (if active { Props::new("hx-get", "/api") } else { Props::default() }.unpack(&cx)) { "x" }
button (if active { Props::new("hx-get", "/api") } else { Props::default() }.unpack(&mut cx)) { "x" }
};
assert_eq!(markup.into_string(), expected);
}
@ -219,9 +219,9 @@ async fn props_id_collision_with_literal_omits_props_id() {
// A literal `#id` on the element takes precedence; `Props`'s own id is silently omitted instead
// of producing a duplicate `id` attribute.
let p = Props::default().with_id("from-props");
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { div #fixed (p.unpack(&cx)) {} }.into_string(),
html! { div #fixed (p.unpack(&mut cx)) {} }.into_string(),
r#"<div id="fixed"></div>"#
);
}
@ -229,9 +229,9 @@ async fn props_id_collision_with_literal_omits_props_id() {
#[pagetop::test]
async fn props_class_collision_with_literal_omits_props_classes() {
let p = Props::classes("from-props-a from-props-b");
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { div.fixed (p.unpack(&cx)) {} }.into_string(),
html! { div.fixed (p.unpack(&mut cx)) {} }.into_string(),
r#"<div class="fixed"></div>"#
);
}
@ -241,9 +241,9 @@ async fn props_style_collision_with_literal_omits_props_styles() {
let p = Props::default()
.with_prop(PropsOp::add_style("color", "red"))
.with_prop(PropsOp::add_style("font-weight", "bold"));
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { div style="color: blue" (p.unpack(&cx)) {} }.into_string(),
html! { div style="color: blue" (p.unpack(&mut cx)) {} }.into_string(),
r#"<div style="color: blue"></div>"#
);
}
@ -251,9 +251,9 @@ async fn props_style_collision_with_literal_omits_props_styles() {
#[pagetop::test]
async fn props_named_attr_collision_with_literal_omits_props_value() {
let p = Props::default().with_prop(PropsOp::set("title", "from-props"));
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { span title="literal" (p.unpack(&cx)) {} }.into_string(),
html! { span title="literal" (p.unpack(&mut cx)) {} }.into_string(),
r#"<span title="literal"></span>"#
);
}
@ -321,9 +321,9 @@ async fn get_prop_id_matches_get_id() {
async fn props_hx_target_value_with_hash_renders_correctly() {
// Regression: r#"..."# used to close prematurely when it found `"#list"`.
let p = Props::new("hx-target", "#list");
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { button (p.unpack(&cx)) {} }.into_string(),
html! { button (p.unpack(&mut cx)) {} }.into_string(),
r##"<button hx-target="#list"></button>"##
);
}
@ -331,9 +331,9 @@ async fn props_hx_target_value_with_hash_renders_correctly() {
#[pagetop::test]
async fn props_with_empty_value_renders_attr_with_empty_value() {
let p = Props::new("data-expanded", "");
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(),
html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span data-expanded=""></span>"#
);
}
@ -348,9 +348,9 @@ async fn props_chained_set_and_remove_yields_expected_state() {
assert_eq!(p.get_prop("a"), Some("updated".to_string()));
assert_eq!(p.get_prop("b"), None);
assert_eq!(p.get_prop("c"), Some("3".to_string()));
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(),
html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span a="updated" c="3"></span>"#
);
}
@ -359,9 +359,9 @@ async fn props_chained_set_and_remove_yields_expected_state() {
async fn props_with_empty_attr_name_renders_without_validation() {
// Documented behavior: names are not validated; the resulting HTML is not standard.
let p = Props::new("", "val");
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(),
html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span ="val"></span>"#
);
}

View file

@ -299,9 +299,9 @@ async fn get_prop_class_matches_get_classes() {
#[pagetop::test]
async fn props_classes_renders_class_attribute() {
let p = Props::classes("btn btn-primary");
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { button (p.unpack(&cx)) { "OK" } }.into_string(),
html! { button (p.unpack(&mut cx)) { "OK" } }.into_string(),
r#"<button class="btn btn-primary">OK</button>"#
);
}
@ -309,9 +309,9 @@ async fn props_classes_renders_class_attribute() {
#[pagetop::test]
async fn props_classes_can_be_extended_with_add_classes() {
let p = Props::classes("btn").with_prop(PropsOp::add_classes("active"));
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { button (p.unpack(&cx)) { "OK" } }.into_string(),
html! { button (p.unpack(&mut cx)) { "OK" } }.into_string(),
r#"<button class="btn active">OK</button>"#
);
}

View file

@ -113,9 +113,9 @@ async fn extras_not_emitted_in_html() {
let props = Props::default()
.with_prop(PropsOp::set_extra("ext.flag", true))
.with_prop(PropsOp::add_classes("btn"));
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { button (props.unpack(&cx)) { "OK" } }.into_string(),
html! { button (props.unpack(&mut cx)) { "OK" } }.into_string(),
r#"<button class="btn">OK</button>"#
);
}

View file

@ -2,93 +2,121 @@ use pagetop::prelude::*;
#[pagetop::test]
async fn default_flex_item_adds_nothing() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item(FlexItem::new()));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
assert_eq!(props.get_classes(), None);
assert_eq!(props.get_styles(), None);
assert_eq!(html, "<span></span>");
assert!(cx.render_assets().into_string().is_empty());
}
#[pagetop::test]
async fn grow_adds_flex_grow_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_grow(flex::ItemGrow::Is1),
));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(props.get_styles(), Some("flex-grow: 1".to_string()));
assert!(html.contains(r#"class="_flex-item-grow_1_""#));
assert!(assets.contains("_flex-item-grow_1_{flex-grow:1}"));
}
#[pagetop::test]
async fn shrink_adds_flex_shrink_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_shrink(flex::ItemShrink::Is0),
));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(props.get_styles(), Some("flex-shrink: 0".to_string()));
assert!(html.contains(r#"class="_flex-item-shrink_0_""#));
assert!(assets.contains("_flex-item-shrink_0_{flex-shrink:0}"));
}
#[pagetop::test]
async fn align_self_adds_matching_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_align_self(flex::ItemAlign::Center),
));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(props.get_styles(), Some("align-self: center".to_string()));
assert!(html.contains(r#"class="_flex-item-align_center_""#));
assert!(assets.contains("_flex-item-align_center_{align-self:center}"));
}
#[pagetop::test]
async fn order_adds_matching_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_order(flex::ItemOrder::First),
));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(props.get_styles(), Some("order: -129".to_string()));
assert!(html.contains(r#"class="_flex-item-order_-129_""#));
assert!(assets.contains("_flex-item-order_-129_{order:-129}"));
}
#[pagetop::test]
async fn size_percent_adds_flex_basis_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_size(flex::ItemSize::Percent33),
));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(props.get_styles(), Some("flex-basis: 33.3333%".to_string()));
assert!(html.contains(r#"class="_flex-item-basis_33_3333pct_""#));
assert!(assets.contains("_flex-item-basis_33_3333pct_{flex-basis:33.3333%}"));
}
#[pagetop::test]
async fn size_custom_adds_flex_basis_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_size(flex::ItemSize::Custom(UnitValue::Zero)),
));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(props.get_classes(), None);
assert_eq!(props.get_styles(), Some("flex-basis: 0".to_string()));
assert!(html.contains(r#"class="_flex-item-basis_0_""#));
assert!(assets.contains("_flex-item-basis_0_{flex-basis:0}"));
}
#[pagetop::test]
async fn offset_percent_adds_margin_inline_start_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_offset(flex::ItemOffset::Percent33),
));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(
props.get_styles(),
Some("margin-inline-start: 33.3333%".to_string())
);
assert!(html.contains(r#"class="_flex-item-offset_33_3333pct_""#));
assert!(assets.contains("_flex-item-offset_33_3333pct_{margin-inline-start:33.3333%}"));
}
#[pagetop::test]
async fn offset_custom_adds_margin_inline_start_style() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new().with_offset(flex::ItemOffset::Custom(UnitValue::Px(16))),
));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(
props.get_styles(),
Some("margin-inline-start: 16px".to_string())
);
assert!(html.contains(r#"class="_flex-item-offset_16px_""#));
assert!(assets.contains("_flex-item-offset_16px_{margin-inline-start:16px}"));
}
#[pagetop::test]
async fn combines_several_facets_in_one_call() {
let mut cx = Context::default();
let props = Props::default().with_prop(PropsOp::flex_item(
FlexItem::new()
.with_grow(flex::ItemGrow::Is1)
@ -98,22 +126,31 @@ async fn combines_several_facets_in_one_call() {
.with_size(flex::ItemSize::Custom(UnitValue::Zero))
.with_offset(flex::ItemOffset::Percent10),
));
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(
props.get_styles(),
Some(
"flex-grow: 1; flex-shrink: 0; align-self: flex-start; order: 2; flex-basis: 0; \
margin-inline-start: 10%"
.to_string()
)
);
assert_eq!(props.get_classes(), None);
assert!(html.contains("_flex-item-grow_1_"));
assert!(html.contains("_flex-item-shrink_0_"));
assert!(html.contains("_flex-item-align_flex-start_"));
assert!(html.contains("_flex-item-order_2_"));
assert!(html.contains("_flex-item-basis_0_"));
assert!(html.contains("_flex-item-offset_10pct_"));
assert!(assets.contains("_flex-item-grow_1_{flex-grow:1}"));
assert!(assets.contains("_flex-item-shrink_0_{flex-shrink:0}"));
assert!(assets.contains("_flex-item-align_flex-start_{align-self:flex-start}"));
assert!(assets.contains("_flex-item-order_2_{order:2}"));
assert!(assets.contains("_flex-item-basis_0_{flex-basis:0}"));
assert!(assets.contains("_flex-item-offset_10pct_{margin-inline-start:10%}"));
}
#[pagetop::test]
async fn from_flex_item_for_props_op() {
let mut cx = Context::default();
let item = FlexItem::new().with_grow(flex::ItemGrow::Is1);
let props = Props::default().with_prop(item.into());
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
let assets = cx.render_assets().into_string();
assert_eq!(props.get_styles(), Some("flex-grow: 1".to_string()));
assert!(html.contains(r#"class="_flex-item-grow_1_""#));
assert!(assets.contains("_flex-item-grow_1_{flex-grow:1}"));
}

View file

@ -275,9 +275,9 @@ async fn props_styles_renders_style_attribute() {
let p = Props::default()
.with_prop(PropsOp::add_style("color", "red"))
.with_prop(PropsOp::add_style("font-weight", "bold"));
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { button (p.unpack(&cx)) { "OK" } }.into_string(),
html! { button (p.unpack(&mut cx)) { "OK" } }.into_string(),
r#"<button style="color: red; font-weight: bold">OK</button>"#
);
}
@ -289,9 +289,9 @@ async fn props_styles_render_after_class_and_before_other_attrs() {
.with_prop(PropsOp::add_classes("btn"))
.with_prop(PropsOp::add_style("color", "red"))
.with_prop(PropsOp::set("data-x", "1"));
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { button (p.unpack(&cx)) { "OK" } }.into_string(),
html! { button (p.unpack(&mut cx)) { "OK" } }.into_string(),
r#"<button id="main" class="btn" style="color: red" data-x="1">OK</button>"#
);
}
@ -299,9 +299,9 @@ async fn props_styles_render_after_class_and_before_other_attrs() {
#[pagetop::test]
async fn props_styles_escapes_double_quotes_in_value() {
let p = Props::default().with_prop(PropsOp::add_style("content", r#""hi""#));
let cx = Context::default();
let mut cx = Context::default();
assert_eq!(
html! { span (p.unpack(&cx)) {} }.into_string(),
html! { span (p.unpack(&mut cx)) {} }.into_string(),
r#"<span style="content: &quot;hi&quot;"></span>"#
);
}

View file

@ -256,7 +256,7 @@ async fn render_zero_min_width_breakpoint_has_no_media_query() {
let cx = Context::default();
let mut r = ResponsiveStyles::new();
r.add_style(Breakpoint::Xs, "col", "flex-basis", "100%");
assert_eq!(r.render(&cx).into_string(), ".col { flex-basis: 100% }");
assert_eq!(r.render(&cx).into_string(), ".col{flex-basis:100%}");
}
#[pagetop::test]
@ -264,7 +264,7 @@ async fn render_none_breakpoint_has_no_media_query() {
let cx = Context::default();
let mut r = ResponsiveStyles::new();
r.add_style(None, "col", "flex-basis", "100%");
assert_eq!(r.render(&cx).into_string(), ".col { flex-basis: 100% }");
assert_eq!(r.render(&cx).into_string(), ".col{flex-basis:100%}");
}
#[pagetop::test]
@ -275,7 +275,7 @@ async fn render_none_comes_before_every_breakpoint() {
r.add_style(None, "row", "display", "flex");
assert_eq!(
r.render(&cx).into_string(),
".row { display: flex }@media (min-width: 768px) { .col { flex-basis: 50% } }"
".row{display:flex}@media(min-width:768px){.col{flex-basis:50%}}"
);
}
@ -287,7 +287,7 @@ async fn render_non_zero_breakpoint_wraps_in_media_query() {
r.add_style(Breakpoint::Md, "col", "flex-basis", "50%");
assert_eq!(
r.render(&cx).into_string(),
"@media (min-width: 768px) { .col { flex-basis: 50% } }"
"@media(min-width:768px){.col{flex-basis:50%}}"
);
}
@ -299,7 +299,7 @@ async fn render_groups_multiple_properties_in_the_same_rule() {
r.add_style(Breakpoint::Md, "col", "margin-inline-start", "0");
assert_eq!(
r.render(&cx).into_string(),
"@media (min-width: 768px) { .col { flex-basis: 50%; margin-inline-start: 0 } }"
"@media(min-width:768px){.col{flex-basis:50%;margin-inline-start:0}}"
);
}
@ -311,7 +311,7 @@ async fn render_concatenates_rules_of_different_selectors_in_the_same_breakpoint
r.add_style(Breakpoint::Md, "row", "display", "flex");
assert_eq!(
r.render(&cx).into_string(),
"@media (min-width: 768px) { .col { flex-basis: 50% }.row { display: flex } }"
"@media(min-width:768px){.col{flex-basis:50%}.row{display:flex}}"
);
}
@ -322,7 +322,7 @@ async fn render_converts_multiple_classes_into_a_compound_selector() {
r.add_style(Breakpoint::Md, "foo bar", "color", "red");
assert_eq!(
r.render(&cx).into_string(),
"@media (min-width: 768px) { .foo.bar { color: red } }"
"@media(min-width:768px){.foo.bar{color:red}}"
);
}
@ -335,9 +335,9 @@ async fn render_orders_breakpoints_mobile_first_regardless_of_insertion_order()
r.add_style(Breakpoint::Md, "col", "flex-basis", "50%");
assert_eq!(
r.render(&cx).into_string(),
".col { flex-basis: 100% }\
@media (min-width: 768px) { .col { flex-basis: 50% } }\
@media (min-width: 992px) { .col { flex-basis: 33% } }"
".col{flex-basis:100%}\
@media(min-width:768px){.col{flex-basis:50%}}\
@media(min-width:992px){.col{flex-basis:33%}}"
);
}
@ -357,9 +357,9 @@ async fn render_has_no_line_breaks() {
async fn context_add_responsive_style_feeds_responsives() {
let cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle(
Some(Breakpoint::Md),
"col",
"flex-basis",
"50%",
"col".into(),
"flex-basis".into(),
"50%".into(),
));
assert_eq!(
cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
@ -372,15 +372,15 @@ async fn context_add_responsive_style_accumulates_across_calls() {
let cx = Context::default()
.with_assets(AssetsOp::AddResponsiveStyle(
Some(Breakpoint::Md),
"col",
"flex-basis",
"50%",
"col".into(),
"flex-basis".into(),
"50%".into(),
))
.with_assets(AssetsOp::AddResponsiveStyle(
Some(Breakpoint::Md),
"col",
"margin-inline-start",
"0",
"col".into(),
"margin-inline-start".into(),
"0".into(),
));
assert_eq!(
cx.responsive_styles().get_styles(Breakpoint::Md, "col"),
@ -399,13 +399,13 @@ async fn context_default_has_no_responsive_styles() {
async fn render_assets_includes_style_tag_with_responsive_styles() {
let mut cx = Context::default().with_assets(AssetsOp::AddResponsiveStyle(
Some(Breakpoint::Xs),
"col",
"flex-basis",
"100%",
"col".into(),
"flex-basis".into(),
"100%".into(),
));
assert_eq!(
cx.render_assets().into_string(),
"<style>.col { flex-basis: 100% }</style>"
"<style>.col{flex-basis:100%}</style>"
);
}