use crate::harness;
use crate::source_scan;
use gpui::{
point, prelude::*, px, Bounds, ElementId, Modifiers, MouseButton, Pixels, TestAppContext,
VisualTestContext,
};
use harness::{events, open_host, press};
use herogpui_components::CloseButton;
const PRESS_SCALE: f32 = 0.93;
const BOX: f32 = 24.;
const ICON: f32 = 16.;
const SVG_MARGIN: f32 = 2.;
fn flush_frame(cx: &mut VisualTestContext) {
cx.update(|window, _| window.refresh());
}
fn pending_frames(cx: &mut VisualTestContext) -> usize {
cx.update(|window, cx| window.simulate_next_frame(cx))
}
fn settle(cx: &mut VisualTestContext) {
for _ in 0..100 {
if pending_frames(cx) == 0 {
return;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
panic!("close button animations never settled");
}
fn probe(name: String) -> &'static str {
Box::leak(name.into_boxed_str())
}
fn root_probe(id: &'static str) -> &'static str {
probe(format!("{:?}", ElementId::from(id)))
}
fn icon_probe(id: &'static str) -> &'static str {
probe(format!("{:?}-icon", ElementId::from(id)))
}
fn near(a: impl Into<Pixels>, b: f32) -> bool {
(f32::from(a.into()) - b).abs() < 0.5
}
fn bounds(cx: &mut VisualTestContext, name: &'static str) -> Bounds<Pixels> {
cx.debug_bounds(name)
.unwrap_or_else(|| panic!("{name} must paint"))
}
fn centre(b: Bounds<Pixels>) -> gpui::Point<Pixels> {
point(
b.origin.x + b.size.width / 2.,
b.origin.y + b.size.height / 2.,
)
}
#[test]
fn default_svg_uses_the_pinned_margins() {
let code: String = source_scan::component_src("close_button.rs")
.split("#[cfg(test)]")
.next()
.expect("the implementation section is always present")
.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
assert!(
code.contains("mx(px(-2.))") && code.contains("my(px(2.))"),
"the default CloseButton glyph must take the pinned -mx-0.5 my-0.5 \
margins, representable as px(-2)/px(2)"
);
}
#[gpui::test]
fn press_scales_without_dimming(cx: &mut TestAppContext) {
harness::still();
let cx = open_host(cx, || {
gpui::div()
.p(px(24.))
.child(CloseButton::new("cb-dim"))
.into_any_element()
});
let resting = harness::painted(cx).solids();
let at = point(px(36.), px(36.));
cx.simulate_mouse_move(at, None, Modifiers::none());
cx.simulate_mouse_down(at, MouseButton::Left, Modifiers::none());
let pressed = harness::painted(cx);
assert!(
!pressed.quads.is_empty()
&& pressed
.quads
.iter()
.filter_map(|q| q.background.as_solid())
.all(|c| c.a < 1e-3 || (c.a - 1.).abs() < 1e-3),
"a pressed close button must not dim, painted {:?} (resting {resting:?})",
pressed.solids()
);
cx.simulate_mouse_up(at, MouseButton::Left, Modifiers::none());
}
#[gpui::test]
fn press_scales_the_box_to_0_93(cx: &mut TestAppContext) {
let presses = events();
let recorded = presses.clone();
let cx = open_host(cx, move || {
let recorded = recorded.clone();
CloseButton::new("cb")
.on_press(move |_, _, _| recorded.borrow_mut().push("press".into()))
.into_any_element()
});
let root = root_probe("cb");
flush_frame(cx);
let at_rest = bounds(cx, root);
assert!(
near(at_rest.size.width, BOX) && near(at_rest.size.height, BOX),
"a resting CloseButton is a 24px square, got {at_rest:?}"
);
let at = centre(at_rest);
cx.simulate_mouse_move(at, None, Modifiers::none());
flush_frame(cx);
let hovered = bounds(cx, root);
assert!(
near(hovered.size.width, BOX) && near(hovered.size.height, BOX),
"hover must not change the box, got {hovered:?}"
);
cx.simulate_mouse_down(at, MouseButton::Left, Modifiers::none());
flush_frame(cx);
let starting = bounds(cx, root);
assert!(
near(starting.size.width, BOX),
"the ramp starts from the painted frame, not the endpoint, got {starting:?}"
);
assert!(
pending_frames(cx) > 0,
"a press must start the transform ramp, not snap"
);
settle(cx);
let pressed = bounds(cx, root);
let scaled = BOX * PRESS_SCALE;
assert!(
near(pressed.size.width, scaled) && near(pressed.size.height, scaled),
"a pressed CloseButton must scale to 0.93 ({scaled}px), got {pressed:?}"
);
let inset = (BOX - scaled) / 2.;
assert!(
near(pressed.origin.x, inset) && near(pressed.origin.y, inset),
"the scale is about the centre, so the leftover is margin, got {pressed:?}"
);
cx.simulate_mouse_up(at, MouseButton::Left, Modifiers::none());
flush_frame(cx);
assert!(
pending_frames(cx) > 0,
"the release must ramp back rather than snap"
);
settle(cx);
let released = bounds(cx, root);
assert!(
near(released.size.width, BOX) && near(released.size.height, BOX),
"the box must spring back after the release, got {released:?}"
);
assert_eq!(
presses.borrow().as_slice(),
["press"],
"the completed click must still report on_press"
);
}
#[gpui::test]
fn reduced_motion_keeps_the_press_scale_instantly(cx: &mut TestAppContext) {
harness::still();
let presses = events();
let recorded = presses.clone();
let cx = open_host(cx, move || {
let recorded = recorded.clone();
CloseButton::new("cb-still")
.on_press(move |_, _, _| recorded.borrow_mut().push("press".into()))
.into_any_element()
});
let root = root_probe("cb-still");
flush_frame(cx);
let at = centre(bounds(cx, root));
cx.simulate_mouse_down(at, MouseButton::Left, Modifiers::none());
flush_frame(cx);
let pressed = bounds(cx, root);
let scaled = BOX * PRESS_SCALE;
assert!(
near(pressed.size.width, scaled) && near(pressed.size.height, scaled),
"reduced motion must apply the press scale immediately ({scaled}px), got {pressed:?}"
);
let inset = (BOX - scaled) / 2.;
assert!(
near(pressed.origin.x, inset) && near(pressed.origin.y, inset),
"reduced-motion press must stay centered, got {pressed:?}"
);
cx.simulate_mouse_up(at, MouseButton::Left, Modifiers::none());
flush_frame(cx);
assert_eq!(
presses.borrow().as_slice(),
["press"],
"reduced motion must not swallow the press"
);
}
#[gpui::test]
fn disabled_press_does_not_scale_or_fire(cx: &mut TestAppContext) {
let presses = events();
let recorded = presses.clone();
let cx = open_host(cx, move || {
let recorded = recorded.clone();
CloseButton::new("cb-off")
.is_disabled(true)
.on_press(move |_, _, _| recorded.borrow_mut().push("press".into()))
.into_any_element()
});
let root = root_probe("cb-off");
flush_frame(cx);
let at = centre(bounds(cx, root));
cx.simulate_mouse_move(at, None, Modifiers::none());
cx.simulate_mouse_down(at, MouseButton::Left, Modifiers::none());
flush_frame(cx);
let pressed = bounds(cx, root);
assert!(
near(pressed.size.width, BOX) && near(pressed.size.height, BOX),
"a disabled CloseButton must not scale, got {pressed:?}"
);
cx.simulate_mouse_up(at, MouseButton::Left, Modifiers::none());
flush_frame(cx);
assert!(
presses.borrow().is_empty(),
"a disabled CloseButton must not report on_press"
);
}
#[gpui::test]
fn focus_visible_keeps_the_24px_box(cx: &mut TestAppContext) {
let cx = open_host(cx, || CloseButton::new("cb-focus").into_any_element());
let root = root_probe("cb-focus");
flush_frame(cx);
press(cx, "tab");
flush_frame(cx);
let focused = bounds(cx, root);
assert!(
near(focused.size.width, BOX) && near(focused.size.height, BOX),
"focus-visible must not change the box, got {focused:?}"
);
}
#[gpui::test]
fn default_glyph_fills_the_padded_content_box(cx: &mut TestAppContext) {
let cx = open_host(cx, || CloseButton::new("cb-icon").into_any_element());
let icon = icon_probe("cb-icon");
flush_frame(cx);
let glyph = bounds(cx, icon);
assert!(
near(glyph.size.width, ICON) && near(glyph.size.height, ICON),
"the default glyph is size-4 (16px), got {glyph:?}"
);
assert!(
near(glyph.origin.x, 4.) && near(glyph.origin.y, 4.),
"items-center + justify-center keep a 16px glyph at the p-1 inset, \
got {glyph:?}"
);
let _ = SVG_MARGIN;
}
#[gpui::test]
fn close_button_reads_the_hover_override_and_the_sx_background(cx: &mut TestAppContext) {
let (rest, over): (gpui::Hsla, gpui::Hsla) =
(gpui::rgb(0x224466).into(), gpui::rgb(0x88aa22).into());
let cx = open_host(cx, move || {
gpui::div()
.p(px(24.))
.child(
CloseButton::new("cb-hover")
.sx(move |s| s.bg(rest))
.hover_bg(over),
)
.into_any_element()
});
let resting = harness::painted(cx).solids();
assert!(
harness::has_color(&resting, rest) && !harness::has_color(&resting, over),
"the resting endpoint must be the sx background, painted {resting:?}"
);
cx.simulate_mouse_move(point(px(36.), px(36.)), None, Modifiers::none());
harness::wait_real(cx, 300);
let hovered = harness::painted(cx).solids();
assert!(
harness::has_color(&hovered, over),
"the hover endpoint must be the named override, painted {hovered:?}"
);
cx.simulate_mouse_move(point(px(600.), px(600.)), None, Modifiers::none());
harness::wait_real(cx, 300);
let left = harness::painted(cx).solids();
let stock = cx.update(|_, cx| {
use herogpui_theme::ActiveTheme;
cx.colors().default.color
});
assert!(
harness::has_color(&left, rest) && !harness::has_color(&left, stock),
"leaving must ease back to the sx background, not `--default`, \
painted {left:?}"
);
}