mod harness;
use gpui::{prelude::*, px, TestAppContext, VisualTestContext};
use herogpui_components::Popover;
use harness::open_host;
const PANEL_WIDTH: f32 = 260.;
const OFFSET: f32 = 8.;
fn near(value: f32, expected: f32) -> bool {
(value - expected).abs() < 0.5
}
fn panel_probe(name: &'static str) -> gpui::AnyElement {
gpui::div()
.h(px(20.))
.debug_selector(move || name.to_owned())
.into_any_element()
}
fn trigger_probe(name: &'static str) -> gpui::AnyElement {
gpui::div()
.w(px(40.))
.h(px(36.))
.debug_selector(move || name.to_owned())
.into_any_element()
}
fn popover(id: &'static str, wide: bool) -> Popover {
let popover = Popover::new(trigger_probe(Box::leak(
format!("trig-{id}").into_boxed_str(),
)))
.id(id)
.is_open(true)
.should_flip(false);
let popover = if wide {
popover.padding(px(24.))
} else {
popover
};
popover.child(panel_probe(Box::leak(
format!("probe-{id}").into_boxed_str(),
)))
}
fn assert_resting_insets(cx: &mut VisualTestContext, id: &str, padding_x: f32, padding_y: f32) {
let trigger = cx
.debug_bounds(Box::leak(format!("trig-{id}").into_boxed_str()))
.unwrap_or_else(|| panic!("the {id} trigger must paint"));
let probe = cx
.debug_bounds(Box::leak(format!("probe-{id}").into_boxed_str()))
.unwrap_or_else(|| panic!("the open {id} panel's content must paint"));
assert!(
near(f32::from(probe.size.width), PANEL_WIDTH - 2. * padding_x),
"{id}: the stretched probe must span the pinned panel width minus \
both x insets, got {} (probe {probe:?})",
f32::from(probe.size.width)
);
let panel_top = f32::from(trigger.origin.y) + f32::from(trigger.size.height) + OFFSET;
let y_inset = f32::from(probe.origin.y) - panel_top;
assert!(
near(y_inset, padding_y),
"{id}: the resting panel's y inset must be the resolved padding, got \
{y_inset} (probe {probe:?}, trigger {trigger:?})"
);
}
fn assert_both_popovers(cx: &mut VisualTestContext) {
assert_resting_insets(cx, "plain", 16., 16.);
assert_resting_insets(cx, "wide", 24., 24.);
}
#[gpui::test]
fn popover_padding_reaches_the_resting_panel_with_reduced_motion(cx: &mut TestAppContext) {
harness::still();
let cx = open_host(cx, || {
gpui::div()
.flex()
.flex_col()
.child(popover("plain", false))
.child(popover("wide", true))
.into_any_element()
});
cx.update(|window, _| window.refresh());
assert_both_popovers(cx);
}
#[test]
fn popover_panel_chain_consumes_the_padding_the_zoom_interpolates() {
let popover = include_str!("../src/popover.rs");
let panel_at = popover
.find("let mut panel = gpui::div()")
.expect("popover.rs: the panel construction must stay");
let chain_end_at = popover[panel_at..]
.find(".shadow(layout.overlay_shadow.clone());")
.map(|at| panel_at + at)
.expect("popover.rs: the panel chain must keep its shadow end");
let zoom_at = popover[panel_at..]
.find("crate::anim::ZoomBox::panel(panel_padding_y, radius)")
.map(|at| panel_at + at)
.expect("popover.rs: the panel zoom must interpolate the hoisted pair");
for binding in [
"let panel_padding_y = self.padding.unwrap_or(px(16.));",
"let panel_padding_x = self.padding.unwrap_or(px(16.));",
] {
assert_eq!(
popover.matches(binding).count(),
1,
"popover.rs: {binding} must be resolved exactly once"
);
let at = popover
.find(binding)
.expect("popover.rs: the binding must exist");
assert!(
at < panel_at,
"popover.rs: {binding} must be hoisted above the panel chain so \
the resting panel and its zoom consume one pair"
);
}
for consumer in [".px(panel_padding_x)", ".py(panel_padding_y)"] {
let at = popover[panel_at..chain_end_at].find(consumer).map_or_else(
|| panic!("popover.rs: the resting panel chain must consume {consumer}"),
|at| panel_at + at,
);
assert!(
at < zoom_at,
"popover.rs: the panel chain's {consumer} must come from the \
hoisted binding, not a second resolution"
);
}
assert!(
popover[panel_at..].contains(".padding_x(panel_padding_x)"),
"popover.rs: the zoom must interpolate the same x binding the panel \
paints"
);
}
#[test]
fn toast_card_chain_consumes_the_padding_the_zoom_interpolates() {
let toast = include_str!("../src/toast.rs");
let card_at = toast
.find("let mut card = gpui::div()")
.expect("toast.rs: the card construction must stay");
let chain_end_at = toast[card_at..]
.find(".overflow_hidden();")
.map(|at| card_at + at)
.expect("toast.rs: the card chain must keep its overflow_hidden end");
let zoom_at = toast[card_at..]
.find("crate::anim::ZoomBox::panel(panel_padding_y, radius)")
.map(|at| card_at + at)
.expect("toast.rs: the card zoom must interpolate the hoisted pair");
for binding in [
"let panel_padding_y = self.t.padding.unwrap_or(px(12.));",
"let panel_padding_x = self.t.padding.unwrap_or(px(16.));",
] {
assert_eq!(
toast.matches(binding).count(),
1,
"toast.rs: {binding} must be resolved exactly once"
);
let at = toast
.find(binding)
.expect("toast.rs: the binding must exist");
assert!(
at < card_at,
"toast.rs: {binding} must be hoisted above the card chain so the \
resting card and its zoom consume one pair"
);
}
for consumer in [".px(panel_padding_x)", ".py(panel_padding_y)"] {
let at = toast[card_at..chain_end_at].find(consumer).map_or_else(
|| panic!("toast.rs: the resting card chain must consume {consumer}"),
|at| card_at + at,
);
assert!(
at < zoom_at,
"toast.rs: the card chain's {consumer} must come from the hoisted \
binding, not a second resolution"
);
}
assert!(
toast[card_at..].contains(".padding_x(panel_padding_x)"),
"toast.rs: the zoom must interpolate the same x binding the card paints"
);
}