T10b feedback: error views with 404, 403, maintenance and offline, error boundary, drawn results, confirmation card, saving, sync and connection lines, countdown and timer with rolling digits
This commit is contained in:
24 files changed
+1806
-173
No files matched your search
@@ -0,0 +1,177 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use gpui::{
|
||||
Context, Entity, FocusHandle, IntoElement, ParentElement, Pixels, Render, Styled,
|
||||
TestAppContext, VisualTestContext, Window, div, px,
|
||||
};
|
||||
|
||||
use super::{press, settle, setup, wait};
|
||||
use crate::{
|
||||
buttons::Button,
|
||||
feedback::{
|
||||
ConfirmationCard, Countdown, EmptyState, ErrorBoundary, SaveState, SavingIndicator,
|
||||
},
|
||||
primitives::{FocusScope, IconName, Measure},
|
||||
};
|
||||
|
||||
/// A boundary, a confirmation card, a saving line and a countdown, recording what they do.
|
||||
struct Shelf {
|
||||
root: FocusHandle,
|
||||
broken: bool,
|
||||
save: SaveState,
|
||||
until: Instant,
|
||||
log: Vec<String>,
|
||||
}
|
||||
|
||||
impl Shelf {
|
||||
fn note(
|
||||
&self,
|
||||
cx: &mut Context<Self>,
|
||||
what: &'static str,
|
||||
) -> impl Fn(&mut Window, &mut gpui::App) + 'static + use<> {
|
||||
let view = cx.entity();
|
||||
move |_, cx| view.update(cx, |shelf, _| shelf.log.push(what.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Shelf {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let content = if self.broken {
|
||||
Err(anyhow!("the service answered 503"))
|
||||
} else {
|
||||
Ok(div().child("fine"))
|
||||
};
|
||||
FocusScope::new(&self.root)
|
||||
.root()
|
||||
.size_full()
|
||||
.child(ErrorBoundary::new("boundary", content).on_retry(self.note(cx, "retry")))
|
||||
.child(
|
||||
ConfirmationCard::new("card", "Sure?")
|
||||
.on_cancel(self.note(cx, "cancel"))
|
||||
.on_confirm(self.note(cx, "confirm")),
|
||||
)
|
||||
.child(SavingIndicator::new("saving", self.save).on_retry(self.note(cx, "save again")))
|
||||
.child(Countdown::new("countdown", self.until).on_done(self.note(cx, "done")))
|
||||
}
|
||||
}
|
||||
|
||||
fn shelf(
|
||||
broken: bool,
|
||||
save: SaveState,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Shelf>, &mut VisualTestContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|_, cx| Shelf {
|
||||
root: cx.focus_handle(),
|
||||
broken,
|
||||
save,
|
||||
until: cx.background_executor().now() + Duration::from_secs(3),
|
||||
log: Vec::new(),
|
||||
});
|
||||
settle(cx);
|
||||
let root = view.read_with(cx, |shelf, _| shelf.root.clone());
|
||||
cx.update(|window, _| window.focus(&root));
|
||||
(view, cx)
|
||||
}
|
||||
|
||||
fn log(view: &Entity<Shelf>, cx: &mut VisualTestContext) -> Vec<String> {
|
||||
view.read_with(cx, |shelf, _| shelf.log.clone())
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_broken_boundary_offers_to_try_again(cx: &mut TestAppContext) {
|
||||
let (view, cx) = shelf(true, SaveState::Saved, cx);
|
||||
press("tab", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(log(&view, cx), ["retry"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_card_cancels_and_confirms(cx: &mut TestAppContext) {
|
||||
let (view, cx) = shelf(false, SaveState::Saved, cx);
|
||||
press("tab", cx);
|
||||
press("enter", cx);
|
||||
press("tab", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(log(&view, cx), ["cancel", "confirm"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn retry_shows_only_once_saving_failed(cx: &mut TestAppContext) {
|
||||
let (view, cx) = shelf(false, SaveState::Failed, cx);
|
||||
for _ in 0..3 {
|
||||
press("tab", cx);
|
||||
}
|
||||
press("enter", cx);
|
||||
assert_eq!(log(&view, cx), ["save again"]);
|
||||
view.update(cx, |shelf, cx| {
|
||||
shelf.save = SaveState::Saved;
|
||||
cx.notify();
|
||||
});
|
||||
settle(cx);
|
||||
let root = view.read_with(cx, |shelf, _| shelf.root.clone());
|
||||
cx.update(|window, _| window.focus(&root));
|
||||
for _ in 0..3 {
|
||||
press("tab", cx);
|
||||
}
|
||||
press("enter", cx);
|
||||
assert_eq!(
|
||||
log(&view, cx),
|
||||
["save again", "cancel"],
|
||||
"Tab wrapped past a saved line"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_countdown_runs_on_done_once_at_zero(cx: &mut TestAppContext) {
|
||||
let (view, cx) = shelf(false, SaveState::Saved, cx);
|
||||
wait(Duration::from_millis(2_900), cx);
|
||||
assert!(log(&view, cx).is_empty());
|
||||
wait(Duration::from_millis(200), cx);
|
||||
assert_eq!(log(&view, cx), ["done"]);
|
||||
wait(Duration::from_secs(5), cx);
|
||||
assert_eq!(log(&view, cx), ["done"]);
|
||||
}
|
||||
|
||||
/// An empty state in a narrow column, and the height it took.
|
||||
struct Column {
|
||||
body: &'static str,
|
||||
height: Pixels,
|
||||
}
|
||||
|
||||
impl Render for Column {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
div().w(px(420.0)).child(
|
||||
Measure::new("column", move |bounds, _, cx| {
|
||||
view.update(cx, |column, _| column.height = bounds.size.height)
|
||||
})
|
||||
.child(
|
||||
EmptyState::new("empty", IconName::Inbox, "Nothing yet")
|
||||
.body(self.body)
|
||||
.action(Button::new("act", "Act")),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn column_height(body: &'static str, cx: &mut TestAppContext) -> Pixels {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|_, _| Column {
|
||||
body,
|
||||
height: Pixels::ZERO,
|
||||
});
|
||||
settle(cx);
|
||||
view.read_with(cx, |column, _| column.height)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_wrapped_body_pushes_the_actions_down(cx: &mut TestAppContext) {
|
||||
let short = column_height("One line.", cx);
|
||||
let long = column_height(
|
||||
"Many words that cannot fit on one line of a narrow column, so they wrap onto a second, a third and a fourth line.",
|
||||
cx,
|
||||
);
|
||||
assert!(long > short + px(24.0), "{short:?} then {long:?}");
|
||||
}
|
||||
Reference in new issue
Block a user