split toast into alert and toast
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AnyWindowHandle, AppContext as _, InteractiveElement as _, IntoElement, Modifiers,
|
||||
ParentElement as _, Render, Styled as _, TestAppContext, VisualTestContext, Window, div, point,
|
||||
px,
|
||||
};
|
||||
use gpui_native_core::alert::{Alert, AlertSeverity};
|
||||
use gpui_native_theme::Theme;
|
||||
|
||||
struct Harness {
|
||||
severity: AlertSeverity,
|
||||
dismissed: Rc<RefCell<bool>>,
|
||||
}
|
||||
|
||||
impl Render for Harness {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut gpui::Context<Self>) -> impl IntoElement {
|
||||
let dismissed = self.dismissed.clone();
|
||||
div().size_full().p_4().child(
|
||||
div().debug_selector(|| "test-alert".to_string()).child(
|
||||
Alert::new("test-alert")
|
||||
.title("Heads up")
|
||||
.severity(self.severity)
|
||||
.message("Secondary line.")
|
||||
.on_dismiss(move |_| *dismissed.borrow_mut() = true),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The alert stretches across the top of the content area; its close button
|
||||
/// sits in the banner's top-right corner.
|
||||
fn alert_window(
|
||||
severity: AlertSeverity,
|
||||
) -> (VisualTestContext, AnyWindowHandle, Rc<RefCell<bool>>) {
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(Theme::init);
|
||||
let dismissed = Rc::new(RefCell::new(false));
|
||||
let harness_dismissed = dismissed.clone();
|
||||
let window = cx.add_window(move |_, _| Harness {
|
||||
severity,
|
||||
dismissed: harness_dismissed,
|
||||
});
|
||||
let window = window.into();
|
||||
cx.update_window(window, |_, window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
})
|
||||
.unwrap();
|
||||
(
|
||||
VisualTestContext::from_window(window, &cx),
|
||||
window,
|
||||
dismissed,
|
||||
)
|
||||
}
|
||||
|
||||
/// A banner with only a message paints, so the title stays optional.
|
||||
#[test]
|
||||
fn a_message_only_alert_renders() {
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(Theme::init);
|
||||
let window = cx.add_window(|_, _| MessageOnlyHarness);
|
||||
let window = window.into();
|
||||
let mut cx = VisualTestContext::from_window(window, &cx);
|
||||
cx.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
});
|
||||
assert!(cx.debug_bounds("message-only-alert").is_some());
|
||||
}
|
||||
|
||||
struct MessageOnlyHarness;
|
||||
|
||||
impl Render for MessageOnlyHarness {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut gpui::Context<Self>) -> impl IntoElement {
|
||||
div().size_full().p_4().child(
|
||||
div()
|
||||
.debug_selector(|| "message-only-alert".to_string())
|
||||
.child(Alert::new("message-only").message("Only line.")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Clicking the close button is how the user acts on the alert; the banner
|
||||
/// reports the intent instead of hiding itself.
|
||||
#[test]
|
||||
fn closing_the_alert_reports_dismissal() {
|
||||
let (mut cx, _window, dismissed) = alert_window(AlertSeverity::Danger);
|
||||
|
||||
let bounds = cx.debug_bounds("test-alert").expect("alert rendered");
|
||||
cx.simulate_click(
|
||||
point(bounds.right() - px(14.), bounds.top() + px(14.)),
|
||||
Modifiers::default(),
|
||||
);
|
||||
|
||||
assert!(*dismissed.borrow(), "the close button reported dismissal");
|
||||
}
|
||||
|
||||
/// Every severity paints a banner.
|
||||
#[test]
|
||||
fn every_severity_renders_a_banner() {
|
||||
for severity in [
|
||||
AlertSeverity::Info,
|
||||
AlertSeverity::Success,
|
||||
AlertSeverity::Warning,
|
||||
AlertSeverity::Danger,
|
||||
] {
|
||||
let (mut cx, _window, _dismissed) = alert_window(severity);
|
||||
assert!(
|
||||
cx.debug_bounds("test-alert").is_some(),
|
||||
"{severity:?} alert rendered"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{AppContext as _, TestAppContext};
|
||||
use gpui_native_core::alert::{Alert, AlertSeverity, Alerts};
|
||||
use gpui_native_theme::Theme;
|
||||
|
||||
fn alerts_stack() -> (TestAppContext, gpui::Entity<Alerts>) {
|
||||
let cx = TestAppContext::single();
|
||||
cx.update(Theme::init);
|
||||
let alerts = cx.update(|cx| cx.new(|_| Alerts::default()));
|
||||
(cx, alerts)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pushed_alerts_stack_up_and_dismiss_by_id() {
|
||||
let (cx, alerts) = alerts_stack();
|
||||
|
||||
cx.update(|cx| {
|
||||
alerts.update(cx, |alerts, cx| {
|
||||
alerts.push(
|
||||
Alert::new("a").title("First").severity(AlertSeverity::Info),
|
||||
cx,
|
||||
);
|
||||
alerts.push(
|
||||
Alert::new("b")
|
||||
.title("Second")
|
||||
.severity(AlertSeverity::Warning),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
assert_eq!(cx.update(|cx| alerts.read(cx).count()), 2);
|
||||
|
||||
cx.update(|cx| {
|
||||
alerts.update(cx, |alerts, cx| alerts.dismiss(0, cx));
|
||||
});
|
||||
assert_eq!(cx.update(|cx| alerts.read(cx).count()), 1);
|
||||
|
||||
// Duplicate ids are fine: the stack re-keys each entry to its own id.
|
||||
cx.update(|cx| {
|
||||
alerts.update(cx, |alerts, cx| {
|
||||
alerts.push(Alert::new("b").title("Third"), cx);
|
||||
});
|
||||
});
|
||||
assert_eq!(cx.update(|cx| alerts.read(cx).count()), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_close_button_routes_through_the_store_and_runs_the_handler() {
|
||||
let (cx, alerts) = alerts_stack();
|
||||
let dismissed = Rc::new(std::cell::RefCell::new(false));
|
||||
|
||||
cx.update(|cx| {
|
||||
let handler = dismissed.clone();
|
||||
alerts.update(cx, |alerts, cx| {
|
||||
alerts.push(
|
||||
Alert::new("a").title("Sync failed").on_dismiss(move |_| {
|
||||
*handler.borrow_mut() = true;
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// The pushed alert's close button calls the store's dismissal, which
|
||||
// then runs the handler.
|
||||
cx.update(|cx| {
|
||||
alerts.update(cx, |alerts, cx| alerts.dismiss(0, cx));
|
||||
});
|
||||
assert!(*dismissed.borrow());
|
||||
assert_eq!(cx.update(|cx| alerts.read(cx).count()), 0);
|
||||
}
|
||||
+97
-24
@@ -3,7 +3,7 @@ use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{AppContext as _, TestAppContext};
|
||||
use gpui_native_core::toast::{Toast, ToastSeverity, Toasts};
|
||||
use gpui_native_core::toast::{Toast, ToastAction, Toasts};
|
||||
use gpui_native_theme::Theme;
|
||||
|
||||
fn toasts_store() -> (TestAppContext, gpui::Entity<Toasts>) {
|
||||
@@ -14,23 +14,49 @@ fn toasts_store() -> (TestAppContext, gpui::Entity<Toasts>) {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_raises_a_toast_and_the_timer_dismisses_it() {
|
||||
fn push_raises_a_toast_and_the_timer_fades_it_out() {
|
||||
let (cx, toasts) = toasts_store();
|
||||
|
||||
let id = cx.update(|cx| {
|
||||
toasts.update(cx, |toasts, cx| {
|
||||
toasts.push(Toast::new("Saved").severity(ToastSeverity::Success), cx)
|
||||
})
|
||||
});
|
||||
let id = cx.update(|cx| toasts.update(cx, |toasts, cx| toasts.push(Toast::new("Saved"), cx)));
|
||||
assert_eq!(cx.update(|cx| toasts.read(cx).count()), 1);
|
||||
|
||||
// The default lifetime is five seconds.
|
||||
// The default lifetime is five seconds; the fade-out follows it.
|
||||
cx.executor().advance_clock(Toast::DURATION);
|
||||
cx.run_until_parked();
|
||||
assert_eq!(cx.update(|cx| toasts.read(cx).count()), 1, "fading out");
|
||||
cx.executor()
|
||||
.advance_clock(std::time::Duration::from_secs(1));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(cx.update(|cx| toasts.read(cx).count()), 0);
|
||||
let _ = id;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pushing_replaces_the_toast_on_screen() {
|
||||
let (cx, toasts) = toasts_store();
|
||||
|
||||
cx.update(|cx| {
|
||||
toasts.update(cx, |toasts, cx| toasts.push(Toast::new("first"), cx));
|
||||
});
|
||||
cx.update(|cx| {
|
||||
toasts.update(cx, |toasts, cx| toasts.push(Toast::new("second"), cx));
|
||||
});
|
||||
|
||||
// One showing, one fading out: never two current toasts.
|
||||
let (current, total) = cx.update(|cx| {
|
||||
toasts.read_with(cx, |toasts, _| {
|
||||
(toasts.current().map(|entry| entry.id), toasts.count())
|
||||
})
|
||||
});
|
||||
assert_eq!(current, Some(1));
|
||||
assert_eq!(total, 2);
|
||||
|
||||
cx.executor()
|
||||
.advance_clock(std::time::Duration::from_secs(6));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(cx.update(|cx| toasts.read(cx).count()), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sticky_toasts_ignore_the_timer_until_dismissed() {
|
||||
let (cx, toasts) = toasts_store();
|
||||
@@ -48,42 +74,89 @@ fn sticky_toasts_ignore_the_timer_until_dismissed() {
|
||||
cx.update(|cx| {
|
||||
toasts.update(cx, |toasts, cx| toasts.dismiss(id, cx));
|
||||
});
|
||||
assert_eq!(cx.update(|cx| toasts.read(cx).count()), 1, "fading out");
|
||||
cx.executor()
|
||||
.advance_clock(std::time::Duration::from_secs(1));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(cx.update(|cx| toasts.read(cx).count()), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dismiss_runs_the_handler_and_clear_removes_everything() {
|
||||
fn dismiss_runs_the_handler_only_after_the_fade_completes() {
|
||||
let (cx, toasts) = toasts_store();
|
||||
let dismissed = Rc::new(std::cell::RefCell::new(Vec::<&'static str>::new()));
|
||||
|
||||
let ids = cx.update(|cx| {
|
||||
cx.update(|cx| {
|
||||
let first = dismissed.clone();
|
||||
toasts.update(cx, |toasts, cx| {
|
||||
let first = dismissed.clone();
|
||||
let second = dismissed.clone();
|
||||
vec![
|
||||
toasts.push(
|
||||
Toast::new("first").on_dismiss(move |_| record(&first, "first")),
|
||||
cx,
|
||||
),
|
||||
toasts.push(
|
||||
Toast::new("second").on_dismiss(move |_| record(&second, "second")),
|
||||
cx,
|
||||
),
|
||||
]
|
||||
})
|
||||
toasts.push(
|
||||
Toast::new("first").on_dismiss(move |_| record(&first, "first")),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
toasts.update(cx, |toasts, cx| toasts.dismiss(ids[0], cx));
|
||||
toasts.update(cx, |toasts, cx| toasts.dismiss(0, cx));
|
||||
});
|
||||
assert!(
|
||||
dismissed.borrow().is_empty(),
|
||||
"the handler waits out the fade"
|
||||
);
|
||||
cx.executor()
|
||||
.advance_clock(std::time::Duration::from_secs(1));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(*dismissed.borrow(), vec!["first"]);
|
||||
|
||||
cx.update(|cx| {
|
||||
toasts.update(cx, |toasts, cx| toasts.clear(cx));
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(std::time::Duration::from_secs(1));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(cx.update(|cx| toasts.read(cx).count()), 0);
|
||||
|
||||
fn record(sink: &Rc<RefCell<Vec<&'static str>>>, what: &'static str) {
|
||||
sink.borrow_mut().push(what);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismissing by any other path never runs the action: only a click on the
|
||||
/// action itself does (the view wires the two together).
|
||||
#[test]
|
||||
fn dismissal_does_not_run_the_action() {
|
||||
let (cx, toasts) = toasts_store();
|
||||
let ran = Rc::new(std::cell::RefCell::new(false));
|
||||
let dismissed = Rc::new(std::cell::RefCell::new(false));
|
||||
|
||||
cx.update(|cx| {
|
||||
let action_ran = ran.clone();
|
||||
let toast_dismissed = dismissed.clone();
|
||||
toasts.update(cx, |toasts, cx| {
|
||||
toasts.push(
|
||||
Toast::new("Draft deleted")
|
||||
.action(ToastAction::new("Undo", move |_| {
|
||||
*action_ran.borrow_mut() = true;
|
||||
}))
|
||||
.on_dismiss(move |_| *toast_dismissed.borrow_mut() = true),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
toasts.update(cx, |toasts, cx| toasts.dismiss(0, cx));
|
||||
});
|
||||
assert!(!*ran.borrow(), "dismiss is not the action");
|
||||
cx.executor()
|
||||
.advance_clock(std::time::Duration::from_secs(1));
|
||||
cx.run_until_parked();
|
||||
assert!(
|
||||
*dismissed.borrow(),
|
||||
"on_dismiss still reports the dismissal"
|
||||
);
|
||||
assert!(
|
||||
*dismissed.borrow(),
|
||||
"on_dismiss still reports the dismissal"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user