Files
Ely-GPUI-Components/src/motion/tests.rs
T

405 lines
13 KiB
Rust

use std::{cell::Cell, rc::Rc};
use gpui::{
Context, InteractiveElement, IntoElement, Modifiers, ParentElement, Render, ScrollDelta,
ScrollWheelEvent, StatefulInteractiveElement, Styled, TestAppContext, TouchPhase,
VisualTestContext, Window, div, point, px,
};
use super::{LazyLoad, LoadingOverlay, ProgressBar};
use crate::theme::Theme;
/// A pressable box under a loading veil, and a lazy row far down a scroll.
struct Bench {
loading: bool,
presses: usize,
built: Rc<Cell<usize>>,
}
impl Render for Bench {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (view, built) = (cx.entity(), self.built.clone());
div()
.child(
LoadingOverlay::new("veil", self.loading).child(
div()
.id("under")
.w(px(200.0))
.h(px(100.0))
.on_click(move |_, _, cx| view.update(cx, |bench, _| bench.presses += 1)),
),
)
.child(
div()
.id("scroll")
.h(px(100.0))
.overflow_y_scroll()
.child(div().h(px(1000.0)))
.child(LazyLoad::new("row", px(20.0), move |_, _| {
built.set(built.get() + 1);
div().h(px(20.0)).child("row")
})),
)
}
}
fn bench(loading: bool, cx: &mut TestAppContext) -> (gpui::Entity<Bench>, &mut VisualTestContext) {
cx.update(|cx| {
Theme::init(cx);
Theme::update(cx, |theme| theme.reduced_motion = true);
});
let (view, cx) = cx.add_window_view(|_, _| Bench {
loading,
presses: 0,
built: Rc::new(Cell::new(0)),
});
cx.run_until_parked();
(view, cx)
}
fn press_box(cx: &mut VisualTestContext) {
let at = point(px(100.0), px(50.0));
cx.simulate_mouse_move(at, None, Modifiers::none());
cx.simulate_click(at, Modifiers::none());
cx.run_until_parked();
}
#[gpui::test]
fn the_veil_stops_presses_only_while_loading(cx: &mut TestAppContext) {
let (view, cx) = bench(true, cx);
press_box(cx);
assert_eq!(view.read_with(cx, |bench, _| bench.presses), 0);
view.update(cx, |bench, cx| {
bench.loading = false;
cx.notify();
});
cx.run_until_parked();
cx.update(|window, _| window.refresh());
cx.run_until_parked();
press_box(cx);
assert_eq!(view.read_with(cx, |bench, _| bench.presses), 1);
}
#[gpui::test]
fn a_lazy_row_builds_once_it_scrolls_into_view(cx: &mut TestAppContext) {
let (view, cx) = bench(false, cx);
let built = view.read_with(cx, |bench, _| bench.built.clone());
assert_eq!(built.get(), 0);
cx.simulate_event(ScrollWheelEvent {
position: point(px(50.0), px(150.0)),
delta: ScrollDelta::Pixels(point(px(0.0), px(-2000.0))),
modifiers: Modifiers::none(),
touch_phase: TouchPhase::Moved,
});
for _ in 0..3 {
cx.update(|window, _| window.refresh());
cx.run_until_parked();
}
assert!(built.get() >= 1, "the row was built");
}
#[test]
#[should_panic(expected = "is behind")]
fn a_buffer_behind_the_value_is_refused() {
let _ = ProgressBar::new("bar", 0.6).buffer(0.4);
}
#[test]
#[should_panic(expected = "not 0..=1")]
fn a_value_past_whole_is_refused() {
let _ = ProgressBar::new("bar", 1.2);
}
/// A sweeping bar at the top of a tall column, and where the next child starts.
struct Column {
after: Rc<Cell<f32>>,
}
impl Render for Column {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let after = self.after.clone();
div()
.flex()
.flex_col()
.h(px(100.0))
.child(ProgressBar::indeterminate("sweep"))
.child(
crate::primitives::Measure::new("after", move |bounds, _, _| {
after.set(f32::from(bounds.origin.y))
})
.h(px(10.0)),
)
}
}
#[gpui::test]
fn a_sweeping_bar_keeps_its_thickness_in_a_tall_column(cx: &mut TestAppContext) {
cx.update(Theme::init);
let after = Rc::new(Cell::new(-1.0));
let (_, cx) = cx.add_window_view({
let after = after.clone();
move |_, _| Column { after }
});
cx.run_until_parked();
let thickness = cx.update(|window, cx| {
f32::from(
crate::theme::ActiveTheme::theme(cx)
.progress_thickness()
.to_pixels(window.rem_size()),
)
});
assert_eq!(after.get(), thickness, "the bar took only its thickness");
}
/// A transition, a presence list and a reorderable list, recording what they report.
struct Stage {
shown: bool,
rows: Vec<(&'static str, bool)>,
order: Vec<&'static str>,
/// The top of what sits below the transition.
drawn: Rc<Cell<usize>>,
/// Each reorder row's keyed state, by row.
seen: Rc<std::cell::RefCell<std::collections::HashMap<&'static str, gpui::EntityId>>>,
log: Vec<String>,
}
/// A 20px row that keeps keyed state and says which entity holds it.
#[derive(IntoElement)]
struct Marker {
key: &'static str,
seen: Rc<std::cell::RefCell<std::collections::HashMap<&'static str, gpui::EntityId>>>,
}
impl gpui::RenderOnce for Marker {
fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
let state = window.use_keyed_state(self.key, cx, |_, _| ());
self.seen.borrow_mut().insert(self.key, state.entity_id());
div().h(px(20.0)).child(self.key)
}
}
impl Render for Stage {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let drawn = self.drawn.clone();
let (gone, moved) = (cx.entity(), cx.entity());
let presence = self.rows.iter().fold(
super::AnimatePresence::new("presence").on_gone(move |key, _, cx| {
gone.update(cx, |stage, _| stage.log.push(format!("gone {key}")))
}),
|list, (key, shown)| list.row(*key, *shown, div().h(px(20.0)).child(*key)),
);
let reorder = self.order.iter().fold(
super::Reorder::new("reorder").on_reorder(move |from, to, _, cx| {
moved.update(cx, |stage, cx| {
let row = stage.order.remove(from);
stage.order.insert(to, row);
stage.log.push(format!("moved {from} to {to}"));
cx.notify();
})
}),
|list, key| {
list.row(
*key,
Marker {
key,
seen: self.seen.clone(),
},
)
},
);
div()
.w(px(300.0))
.child(reorder)
.child(super::Transition::new("fade", self.shown).child(div().h(px(10.0))))
.child(
crate::primitives::Measure::new("below", move |bounds, _, _| {
drawn.set(f32::from(bounds.origin.y) as usize)
})
.h(px(1.0)),
)
.child(presence)
}
}
fn stage(cx: &mut TestAppContext) -> (gpui::Entity<Stage>, &mut VisualTestContext) {
cx.update(|cx| {
Theme::init(cx);
Theme::update(cx, |theme| theme.reduced_motion = true);
});
let (view, cx) = cx.add_window_view(|_, _| Stage {
shown: true,
rows: vec![("a", true), ("b", true)],
order: vec!["one", "two", "three", "four", "five"],
drawn: Rc::new(Cell::new(0)),
seen: Rc::default(),
log: Vec::new(),
});
settle(cx);
(view, cx)
}
fn settle(cx: &mut VisualTestContext) {
for _ in 0..3 {
std::thread::sleep(std::time::Duration::from_millis(2));
cx.update(|window, _| window.refresh());
cx.run_until_parked();
}
}
#[gpui::test]
fn a_hidden_transition_leaves_then_stops_drawing(cx: &mut TestAppContext) {
let (view, cx) = stage(cx);
let below = view.read_with(cx, |stage, _| stage.drawn.clone());
assert_eq!(below.get(), 110, "the reorder list, then the shown content");
view.update(cx, |stage, cx| {
stage.shown = false;
cx.notify();
});
settle(cx);
assert_eq!(below.get(), 100, "the content left the layout");
}
#[gpui::test]
fn a_row_marked_gone_is_reported_once(cx: &mut TestAppContext) {
let (view, cx) = stage(cx);
view.update(cx, |stage, cx| {
stage.rows[1].1 = false;
cx.notify();
});
settle(cx);
settle(cx);
let log = view.read_with(cx, |stage, _| stage.log.clone());
assert_eq!(log, ["gone b"]);
}
#[gpui::test]
fn dragging_a_row_down_moves_it_past_the_rows_it_passes(cx: &mut TestAppContext) {
let (view, cx) = stage(cx);
let at = |y: f32| point(px(100.0), px(y));
cx.simulate_mouse_move(at(10.0), None, Modifiers::none());
cx.simulate_mouse_down(at(10.0), gpui::MouseButton::Left, Modifiers::none());
for y in [16.0, 30.0, 45.0, 50.0] {
cx.simulate_mouse_move(at(y), Some(gpui::MouseButton::Left), Modifiers::none());
cx.run_until_parked();
}
cx.simulate_mouse_up(at(50.0), gpui::MouseButton::Left, Modifiers::none());
settle(cx);
let (order, log) = view.read_with(cx, |stage, _| (stage.order.clone(), stage.log.clone()));
assert_eq!(log, ["moved 0 to 2"]);
assert_eq!(order, ["two", "three", "one", "four", "five"]);
}
#[gpui::test]
fn a_moved_row_keeps_its_state(cx: &mut TestAppContext) {
let (view, cx) = stage(cx);
let seen = view.read_with(cx, |stage, _| stage.seen.clone());
let before = seen.borrow()["one"];
view.update(cx, |stage, cx| {
stage.order.reverse();
cx.notify();
});
settle(cx);
assert_eq!(
seen.borrow()["one"],
before,
"the same state after the move"
);
}
#[gpui::test]
fn a_list_that_shrinks_under_a_drag_lets_it_go(cx: &mut TestAppContext) {
let (view, cx) = stage(cx);
let at = |y: f32| point(px(100.0), px(y));
cx.simulate_mouse_move(at(90.0), None, Modifiers::none());
cx.simulate_mouse_down(at(90.0), gpui::MouseButton::Left, Modifiers::none());
for y in [84.0, 60.0] {
cx.simulate_mouse_move(at(y), Some(gpui::MouseButton::Left), Modifiers::none());
cx.run_until_parked();
}
view.update(cx, |stage, cx| {
stage.order.truncate(2);
cx.notify();
});
settle(cx);
cx.simulate_mouse_up(at(20.0), gpui::MouseButton::Left, Modifiers::none());
settle(cx);
let log = view.read_with(cx, |stage, _| stage.log.clone());
assert!(log.is_empty(), "no move for a row that left: {log:?}");
}
#[gpui::test]
fn a_row_hidden_from_the_start_is_gone_at_once(cx: &mut TestAppContext) {
let (view, cx) = stage(cx);
cx.update(|_, cx| Theme::update(cx, |theme| theme.reduced_motion = false));
view.update(cx, |stage, cx| {
stage.rows.push(("c", false));
cx.notify();
});
settle(cx);
let log = view.read_with(cx, |stage, _| stage.log.clone());
assert_eq!(log, ["gone c"], "no fold to wait for");
}
/// A shaken and a flashed box around keyed state, and a press target under a ripple and confetti.
struct Effects {
key: usize,
presses: usize,
seen: Rc<std::cell::RefCell<std::collections::HashMap<&'static str, gpui::EntityId>>>,
}
impl Render for Effects {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let marker = |key| Marker {
key,
seen: self.seen.clone(),
};
div()
.child(super::Shake::new("shake", self.key).child(marker("shaken")))
.child(super::Flash::new("flash", self.key).child(marker("flashed")))
.child(
div()
.relative()
.w(px(200.0))
.h(px(100.0))
.child(super::Ripple::new("ripple").size_full().child(
div().id("target").size_full().on_click(move |_, _, cx| {
view.update(cx, |effects, _| effects.presses += 1)
}),
))
.child(super::Confetti::new("confetti", self.key)),
)
}
}
#[gpui::test]
fn effects_keep_their_content_and_let_presses_through(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::init(cx);
Theme::update(cx, |theme| theme.reduced_motion = true);
});
let (view, cx) = cx.add_window_view(|_, _| Effects {
key: 0,
presses: 0,
seen: Rc::default(),
});
settle(cx);
let seen = view.read_with(cx, |effects, _| effects.seen.clone());
let before = seen.borrow().clone();
view.update(cx, |effects, cx| {
effects.key += 1;
cx.notify();
});
settle(cx);
assert_eq!(
*seen.borrow(),
before,
"a shake and a flash keep what they hold"
);
let at = point(px(100.0), px(90.0));
cx.simulate_mouse_move(at, None, Modifiers::none());
cx.simulate_click(at, Modifiers::none());
settle(cx);
assert_eq!(view.read_with(cx, |effects, _| effects.presses), 1);
}