Files
Ely-GPUI-Components/src/charts/tests.rs
T
ZacharyZhang-NY a6f9a7b77b Capture · Roots: the roots round 2 missed
A sweep that read every render found 28 roots still on w_full,
reached by 64 components: the field button under Select and every
picker, the checked and mention fields, the slider track,
NumberInput, Steps, the state views, the agent previews, and every
chart and finance frame. All drop it. LivePreview's size_full becomes
h_full, since its card no longer fills.

Every host of those roots was listed and read, 231 in all. Five
tests that mounted a picker as the window root sit in a size_full
box, the index manager gives its method select a flex_1 box, and the
notification center and the gallery's state frames stack their state
views in a column so they stretch.

Each dropped root carries a debug selector, and six chapter tests
mount 43 components in a padded card whose column is measured by
content, through layout::tests::narrow_width: each root must take the
full 240px. With w_full back on the roots, all six fail.

AGENTS.md names the roots that keep w_full and why: picture frames
that keep a ratio, and rows that hand a text field its share.
2026-09-28 19:34:01 -04:00

333 lines
12 KiB
Rust

use std::{
cell::{Cell, RefCell},
rc::Rc,
};
use gpui::{
Context, IntoElement, KeyUpEvent, Keystroke, Modifiers, MouseMoveEvent, ParentElement, Render,
SharedString, Styled, TestAppContext, VisualTestContext, Window, black, div, point, px,
};
use jiff::civil::date;
use super::{
AreaChart, BarChart, BoxPlot, Bullet, BulletChart, CalendarHeatmap, ChartLegend, ChordDiagram,
FunnelChart, GanttChart, HeatmapChart, Histogram, LineChart, NetworkGraph, ParallelCoordinates,
PieChart, Points, ProgressChart, RadarChart, RealtimeChart, SankeyChart, ScatterChart, Series,
Slice, Sunburst, Task, Treemap, ViolinPlot, WaterfallChart,
};
use crate::{layout::tests::narrow_width, theme::Theme};
#[gpui::test]
fn a_chart_exports_as_svg_and_csv(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::init(cx);
let line = LineChart::new("sales", ["Jan", "Feb", "Mar"])
.series(Series::new("Sales, net", [1.0, 3.0, 2.0]))
.smooth();
let svg = line.svg(400.0, 200.0, cx);
assert!(svg.starts_with("<svg") && svg.ends_with("</svg>"));
assert_eq!(svg.matches("<path").count(), 1, "one line, no area");
assert!(svg.contains(" C"), "a smooth line curves");
assert!(svg.contains(">Feb</text>"), "categories label the axis");
assert_eq!(line.csv(), ",Jan,Feb,Mar\n\"Sales, net\",1,3,2");
let bars = BarChart::new("share", ["A", "B"])
.series(Series::new("Now", [2.0, 4.0]))
.series(Series::new("Before", [1.0, 3.0]));
let svg = bars.svg(400.0, 200.0, cx);
assert_eq!(
svg.matches("<rect").count(),
5,
"the page, then a bar per value"
);
});
}
/// Every chart that picks a part under the pointer, stacked 200 tall at 400 across, with many parts or few.
struct Picked(Rc<Cell<bool>>);
const TALL: f32 = 200.0;
impl Render for Picked {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let many = self.0.get();
let count = if many { 6 } else { 3 };
let values = |count: usize| {
(0..count)
.map(|ix| 10.0 + ix as f64 * 3.0)
.collect::<Vec<_>>()
};
let names: Vec<String> = (0..count).map(|ix| format!("n{ix}")).collect();
let tile = |chart: gpui::AnyElement| {
div()
.w(px(400.0))
.h(px(TALL))
.overflow_hidden()
.child(chart)
};
let treemap = names
.iter()
.zip(values(count))
.fold(Treemap::new("treemap"), |map, (name, value)| {
map.tile(name.clone(), value)
});
let heatmap = names
.iter()
.fold(HeatmapChart::new("heatmap", names.clone()), |map, name| {
map.row(name.clone(), values(count))
});
let radar = RadarChart::new("radar", names.clone()).series(Series::new("a", values(count)));
let funnel = names
.iter()
.zip(values(count).into_iter().rev())
.fold(FunnelChart::new("funnel"), |funnel, (name, value)| {
funnel.stage(name.clone(), value)
});
let sankey = (1..count).fold(SankeyChart::new("sankey", names.clone()), |sankey, to| {
sankey.link(0, to, 5.0)
});
let gantt = (0..count).fold(GanttChart::new("gantt"), |gantt, ix| {
gantt.task(Task::new(
format!("t{ix}"),
date(2026, 3, 1 + ix as i8),
date(2026, 3, 9 + ix as i8),
))
});
let network = (1..count).fold(
names
.iter()
.fold(NetworkGraph::new("network"), |graph, name| {
graph.node(name.clone(), 0)
}),
|graph, to| graph.edge(0, to),
);
let matrix: Vec<Vec<f64>> = (0..count)
.map(|row| {
(0..count)
.map(|col| if row == col { 0.0 } else { 1.0 + col as f64 })
.collect()
})
.collect();
let chord = ChordDiagram::new("chord", names.clone(), matrix);
let parallel = (0..count).fold(
ParallelCoordinates::new("parallel", ["a", "b"]),
|chart, ix| chart.record(format!("r{ix}"), [ix as f64, (count - ix) as f64]),
);
let bullets = (0..count).fold(BulletChart::new("bullets"), |chart, ix| {
chart.bullet(Bullet::new(format!("b{ix}"), 5.0 + ix as f64, 8.0))
});
let rings = (0..count).fold(ProgressChart::new("rings"), |chart, ix| {
chart.goal(format!("g{ix}"), 3.0 + ix as f64, 10.0)
});
let days = CalendarHeatmap::new("days", date(2026, 1, 5), values(count * 10));
div()
.flex()
.flex_col()
.child(tile(treemap.h(px(TALL)).into_any_element()))
.child(tile(heatmap.h(px(TALL)).into_any_element()))
.child(tile(radar.h(px(TALL)).into_any_element()))
.child(tile(funnel.h(px(TALL)).into_any_element()))
.child(tile(sankey.h(px(TALL)).into_any_element()))
.child(tile(gantt.h(px(TALL)).into_any_element()))
.child(tile(network.h(px(TALL)).into_any_element()))
.child(tile(chord.h(px(TALL)).into_any_element()))
.child(tile(parallel.h(px(TALL)).into_any_element()))
.child(tile(bullets.h(px(TALL)).into_any_element()))
.child(tile(rings.h(px(TALL)).into_any_element()))
.child(tile(days.h(px(TALL)).into_any_element()))
}
}
fn settle(cx: &mut VisualTestContext) {
for _ in 0..3 {
cx.run_until_parked();
cx.update(|window, _| window.refresh());
}
cx.run_until_parked();
}
#[gpui::test]
fn a_part_under_the_pointer_lets_go_when_its_data_shrinks(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::init(cx);
Theme::update(cx, |theme| theme.reduced_motion = true);
});
let many = Rc::new(Cell::new(true));
let seen = many.clone();
let (view, cx) = cx.add_window_view(|_, _| Picked(seen));
cx.simulate_resize(gpui::size(px(400.0), px(TALL * 12.0)));
settle(cx);
for chart in 0..12 {
for (x, y) in [(360.0, 20.0), (200.0, 100.0), (60.0, 170.0)] {
let at = point(px(x), px(TALL * chart as f32 + y));
cx.simulate_event(MouseMoveEvent {
position: at,
pressed_button: None,
modifiers: Modifiers::none(),
});
settle(cx);
for full in [false, true] {
many.set(full);
view.update(cx, |_, cx| cx.notify());
settle(cx);
}
}
}
}
/// A legend of two series that hears which one is toggled.
struct Keyed(Rc<RefCell<Vec<SharedString>>>);
impl Render for Keyed {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let heard = self.0.clone();
div().child(
ChartLegend::new("legend")
.entry(black(), "Training", false)
.entry(black(), "Held out", true)
.on_toggle(move |name, _, _| heard.borrow_mut().push(name.clone())),
)
}
}
#[gpui::test]
fn tab_reaches_a_legend_entry_and_enter_toggles_it(cx: &mut TestAppContext) {
cx.update(Theme::init);
let heard = Rc::new(RefCell::new(Vec::new()));
let seen = heard.clone();
let (_, cx) = cx.add_window_view(|_, _| Keyed(seen));
cx.update(|window, _| window.activate_window());
settle(cx);
cx.update(|window, _| {
window.focus_next();
window.focus_next();
});
settle(cx);
cx.simulate_keystrokes("enter");
cx.simulate_event(KeyUpEvent {
keystroke: Keystroke::parse("enter").expect("a key"),
});
settle(cx);
assert_eq!(*heard.borrow(), ["Held out"]);
}
#[gpui::test]
fn every_chart_fills_a_column_its_block_measures_by_content(cx: &mut TestAppContext) {
cx.update(Theme::init);
let two = || Series::new("s", [1.0, 2.0]);
let pairs = [vec![0.0, 1.0], vec![1.0, 0.0]];
let widths = [
narrow_width(cx, "chart-root", move |_, _| {
LineChart::new("line", ["a", "b"])
.series(two())
.into_any_element()
}),
narrow_width(cx, "chart-root", move |_, _| {
AreaChart::new("area", ["a", "b"])
.series(two())
.into_any_element()
}),
narrow_width(cx, "chart-root", move |_, _| {
BarChart::new("bar", ["a", "b"])
.series(two())
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
let points = Points::new("p", [(0.0, 1.0), (1.0, 2.0)]);
ScatterChart::new("scatter")
.points(points)
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
Histogram::new("histogram", [1.0, 2.0, 2.0, 3.0]).into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
WaterfallChart::new("waterfall")
.step("a", 2.0)
.step("b", -1.0)
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
BoxPlot::new("box")
.group("a", [1.0, 2.0, 3.0])
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
ViolinPlot::new("violin")
.group("a", [1.0, 2.0, 3.0])
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
CalendarHeatmap::new("days", date(2026, 1, 5), [1.0; 14]).into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
SankeyChart::new("sankey", ["a", "b"])
.link(0, 1, 5.0)
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
FunnelChart::new("funnel")
.stage("a", 2.0)
.stage("b", 1.0)
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
NetworkGraph::new("network")
.node("a", 0)
.node("b", 0)
.edge(0, 1)
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
ParallelCoordinates::new("parallel", ["a", "b"])
.record("r", [1.0, 2.0])
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
PieChart::new("pie")
.slice("a", 1.0)
.slice("b", 2.0)
.into_any_element()
}),
narrow_width(cx, "chart-root", move |_, _| {
RadarChart::new("radar", ["a", "b", "c"])
.series(Series::new("s", [1.0, 2.0, 3.0]))
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
RealtimeChart::new("live", [1.0, 2.0], 2).into_any_element()
}),
narrow_width(cx, "chart-root", move |_, _| {
ChordDiagram::new("chord", ["a", "b"], pairs.to_vec()).into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
GanttChart::new("gantt")
.task(Task::new("t", date(2026, 3, 1), date(2026, 3, 9)))
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
Sunburst::new("sun", [Slice::new("a", 1.0)]).into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
BulletChart::new("bullets")
.bullet(Bullet::new("b", 5.0, 8.0))
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
Treemap::new("treemap")
.tile("a", 1.0)
.tile("b", 2.0)
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
HeatmapChart::new("heatmap", ["a"])
.row("a", [1.0])
.into_any_element()
}),
];
assert_eq!(
widths,
[px(240.0); 22],
"each chart spans the card inside its padding"
);
}