use gpui::prelude::*;
use gpui::{div, Context, Entity, Modifiers, MouseButton, TestAppContext, Window};
use crate::ai::{AIChatView, AIComposer, AIComposerEvent, AITurn};
use crate::devtools::{
DevTools, DevToolsEvent, DevToolsState, DevToolsTab, LogLevel, NetworkRecord, Probed,
RequestState, SourceRef, StorageDomain, StorageEntry,
};
use crate::input::{Date, DatePicker, LineEditor as _, Select, TextInput};
use crate::reactive::{validators, Form, Signal};
use crate::theme::{theme, Color, Theme};
use crate::update::{
is_installing, Release, UpdateNotice, UpdateNoticeEvent, UpdateOutcome, UpdatePrompt,
UpdatePromptEvent, UpdateStage, Updater,
};
use crate::{Carousel, CarouselEvent};
#[gpui::test]
fn signal_binding_and_lens_round_trip(cx: &mut TestAppContext) {
let count = cx.update(|cx| Signal::new(cx, 5_i32));
let binding = count.binding();
cx.update(|cx| {
assert_eq!(binding.get(cx), 5);
binding.set(cx, 9);
assert_eq!(count.get(cx), 9);
});
#[derive(Clone, PartialEq)]
struct Settings {
muted: bool,
}
let settings = cx.update(|cx| Signal::new(cx, Settings { muted: false }));
let muted = settings.lens(|s| s.muted, |s, v| s.muted = v);
cx.update(|cx| {
muted.set(cx, true);
assert!(settings.read(cx).muted);
let as_text = muted.map(|b| b.to_string(), |s: String| s == "true");
assert_eq!(as_text.get(cx), "true");
as_text.set(cx, "false".to_string());
assert!(!settings.read(cx).muted);
});
}
#[gpui::test]
fn form_validates_and_revalidates_live(cx: &mut TestAppContext) {
let form = cx.update(|cx| {
Form::new(cx)
.field(cx, "email", "")
.rule("email", validators::required())
.rule("email", validators::email())
.field(cx, "confirm", "")
.rule_form("confirm", validators::equals_field("email", "Must match"))
});
cx.update(|cx| {
assert!(!form.validate(cx));
assert!(form.error(cx, "email").is_some());
assert!(!form.is_valid(cx));
});
cx.update(|cx| form.set(cx, "email", "a@b.com"));
cx.update(|cx| {
assert_eq!(form.error(cx, "email"), None);
assert!(form.touched("email"));
});
cx.update(|cx| form.set(cx, "confirm", "a@b.com"));
cx.update(|cx| {
let values = form.submit(cx).expect("form should validate");
assert_eq!(values["email"], "a@b.com");
});
cx.update(|cx| form.set(cx, "email", "other@b.com"));
cx.update(|cx| assert!(!form.validate(cx)));
}
#[gpui::test]
fn select_bind_follows_the_signal_both_ways(cx: &mut TestAppContext) {
let choice = cx.update(|cx| Signal::new(cx, 2_usize));
let select = cx.update(|cx| cx.new(|cx| Select::new(cx).data(["a", "b", "c"])));
cx.update(|cx| Select::bind(&select, &choice, cx));
cx.update(|cx| assert_eq!(select.read(cx).selected_index(), Some(2)));
cx.update(|cx| choice.set(cx, 0));
cx.update(|cx| assert_eq!(select.read(cx).selected_index(), Some(0)));
}
#[gpui::test]
fn datepicker_bind_adopts_signal_writes(cx: &mut TestAppContext) {
let date = Date::new(2026, 7, 14).unwrap();
let picked = cx.update(|cx| Signal::new(cx, None::<Date>));
let picker = cx.update(|cx| cx.new(DatePicker::new));
cx.update(|cx| DatePicker::bind(&picker, &picked, cx));
cx.update(|cx| assert_eq!(picker.read(cx).selected_date(), None));
cx.update(|cx| picked.set(cx, Some(date)));
cx.update(|cx| assert_eq!(picker.read(cx).selected_date(), Some(date)));
}
#[gpui::test]
fn carousel_navigates_and_emits(cx: &mut TestAppContext) {
use std::cell::RefCell;
use std::rc::Rc;
let deck = cx.update(|cx| {
cx.new(|cx| {
Carousel::new(cx)
.slide(|_, _| gpui::Empty)
.slide(|_, _| gpui::Empty)
.slide(|_, _| gpui::Empty)
})
});
let seen: Rc<RefCell<Vec<usize>>> = Rc::default();
let log = seen.clone();
cx.update(|cx| {
cx.subscribe(&deck, move |_deck, event: &CarouselEvent, _cx| {
log.borrow_mut().push(event.0);
})
.detach();
});
deck.update(cx, |deck, cx| {
deck.next(cx);
deck.next(cx);
deck.next(cx); deck.prev(cx); deck.go_to(1, cx);
deck.go_to(1, cx); });
assert_eq!(*seen.borrow(), vec![1, 2, 0, 2, 1]);
cx.update(|cx| assert_eq!(deck.read(cx).current(), 1));
}
fn prompt_event_name(event: &UpdatePromptEvent) -> &'static str {
match event {
UpdatePromptEvent::Started => "started",
UpdatePromptEvent::Stage(_) => "stage",
UpdatePromptEvent::Installed(_) => "installed",
UpdatePromptEvent::Failed(_) => "failed",
UpdatePromptEvent::Dismissed => "dismissed",
}
}
fn offered_release() -> Release {
Release {
version: "2.0.0".to_string(),
url: "https://example.com/releases/2.0.0".to_string(),
assets: Vec::new(),
}
}
#[gpui::test]
fn update_prompt_offers_the_page_when_it_cannot_install_in_place(cx: &mut TestAppContext) {
use std::cell::RefCell;
use std::rc::Rc;
let updater = Updater::github("Acme", "1.0.0", "acme/acme");
let prompt = cx.update(|cx| cx.new(|cx| UpdatePrompt::new(updater, offered_release(), cx)));
let seen: Rc<RefCell<Vec<&'static str>>> = Rc::default();
let log = seen.clone();
cx.update(|cx| {
cx.subscribe(&prompt, move |_prompt, event: &UpdatePromptEvent, _cx| {
log.borrow_mut().push(prompt_event_name(event));
})
.detach();
});
prompt.update(cx, |prompt, cx| prompt.accept(cx));
assert_eq!(*seen.borrow(), vec!["dismissed"]);
cx.update(|cx| {
assert!(!prompt.read(cx).busy());
assert!(!is_installing(cx));
});
}
#[gpui::test]
fn update_prompt_tracks_the_stages_a_host_drives(cx: &mut TestAppContext) {
use std::cell::RefCell;
use std::rc::Rc;
let updater = Updater::github("Acme", "1.0.0", "acme/acme");
let prompt = cx.update(|cx| cx.new(|cx| UpdatePrompt::new(updater, offered_release(), cx)));
let seen: Rc<RefCell<Vec<&'static str>>> = Rc::default();
let log = seen.clone();
cx.update(|cx| {
cx.subscribe(&prompt, move |_prompt, event: &UpdatePromptEvent, _cx| {
log.borrow_mut().push(prompt_event_name(event));
})
.detach();
});
prompt.update(cx, |prompt, cx| {
prompt.set_stage(UpdateStage::Preparing, cx);
prompt.dismiss(cx);
});
cx.update(|cx| {
let prompt = prompt.read(cx);
assert!(prompt.busy());
assert_eq!(prompt.stage(), Some(&UpdateStage::Preparing));
assert_eq!(prompt.error(), None);
});
assert!(seen.borrow().is_empty());
prompt.update(cx, |prompt, cx| prompt.set_failed("no disk space", cx));
cx.update(|cx| {
let prompt = prompt.read(cx);
assert!(!prompt.busy());
assert_eq!(prompt.error(), Some("no disk space"));
});
prompt.update(cx, |prompt, cx| {
prompt.reset(cx);
prompt.dismiss(cx);
});
cx.update(|cx| assert_eq!(prompt.read(cx).error(), None));
assert_eq!(*seen.borrow(), vec!["dismissed"]);
}
#[gpui::test]
fn update_notice_answers_a_check_and_dismisses(cx: &mut TestAppContext) {
use std::cell::RefCell;
use std::rc::Rc;
let updater = Updater::github("Acme", "1.31.0", "acme/acme");
let notice = cx.update(|cx| {
cx.new(|cx| UpdateNotice::new(updater, UpdateOutcome::Pending("1.32.0".into()), cx))
});
let dismissed: Rc<RefCell<usize>> = Rc::default();
let count = dismissed.clone();
cx.update(|cx| {
cx.subscribe(¬ice, move |_notice, event: &UpdateNoticeEvent, _cx| {
let UpdateNoticeEvent::Dismissed = event;
*count.borrow_mut() += 1;
})
.detach();
});
cx.update(|cx| {
assert_eq!(
notice.read(cx).outcome(),
&UpdateOutcome::Pending("1.32.0".to_string())
);
});
notice.update(cx, |notice, cx| notice.dismiss(cx));
assert_eq!(*dismissed.borrow(), 1);
}
#[gpui::test]
fn theme_presets_install_and_resolve(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::catppuccin().init(cx);
let t = theme(cx);
assert!(t.scheme.is_dark());
assert_eq!(t.primary(), Color::hex("#89b4fa"));
assert_eq!(t.body(), Color::hex("#1e1e2e"));
Theme::solarized_light().init(cx);
let t = theme(cx);
assert!(!t.scheme.is_dark());
assert_eq!(t.primary(), Color::hex("#268bd2"));
});
}
struct Pair {
first: Entity<TextInput>,
second: Entity<TextInput>,
}
impl Render for Pair {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.size_full()
.child(self.first.clone())
.child(self.second.clone())
.probe("Pair")
}
}
fn pair(cx: &mut TestAppContext) -> (Entity<Pair>, &mut gpui::VisualTestContext) {
cx.update(|cx| Theme::light().init(cx));
cx.add_window_view(|_window, cx| Pair {
first: cx.new(|cx| TextInput::new(cx).placeholder("first")),
second: cx.new(|cx| TextInput::new(cx).placeholder("second")),
})
}
fn focus(field: &Entity<TextInput>, cx: &mut gpui::VisualTestContext) {
let handle = field.read_with(cx, |field, _| field.focus_handle());
cx.update(|window, _| window.focus(&handle));
cx.run_until_parked();
}
#[gpui::test]
fn text_input_types_through_the_platform_input_handler(cx: &mut TestAppContext) {
let (view, cx) = pair(cx);
let field = view.read_with(cx, |view, _| view.first.clone());
focus(&field, cx);
cx.simulate_input("hello");
assert_eq!(field.read_with(cx, |field, _| field.text()), "hello");
cx.simulate_keystrokes("backspace left left");
cx.simulate_input("L");
assert_eq!(field.read_with(cx, |field, _| field.text()), "heLll");
}
#[gpui::test]
fn text_input_tab_moves_focus_instead_of_typing(cx: &mut TestAppContext) {
let (view, cx) = pair(cx);
let (first, second) = view.read_with(cx, |view, _| (view.first.clone(), view.second.clone()));
focus(&first, cx);
cx.simulate_input("one");
cx.simulate_keystrokes("tab");
assert_eq!(first.read_with(cx, |field, _| field.text()), "one");
let second_handle = second.read_with(cx, |field, _| field.focus_handle());
assert!(cx.update(|window, _| second_handle.is_focused(window)));
cx.simulate_input("two");
assert_eq!(second.read_with(cx, |field, _| field.text()), "two");
cx.simulate_keystrokes("shift-tab");
let first_handle = first.read_with(cx, |field, _| field.focus_handle());
assert!(cx.update(|window, _| first_handle.is_focused(window)));
assert_eq!(second.read_with(cx, |field, _| field.text()), "two");
}
#[gpui::test]
fn text_input_cuts_copies_and_pastes(cx: &mut TestAppContext) {
let (view, cx) = pair(cx);
let (first, second) = view.read_with(cx, |view, _| (view.first.clone(), view.second.clone()));
focus(&first, cx);
cx.simulate_input("copy me");
cx.simulate_keystrokes("cmd-a cmd-c");
focus(&second, cx);
cx.simulate_keystrokes("cmd-v cmd-v");
assert_eq!(
second.read_with(cx, |field, _| field.text()),
"copy mecopy me"
);
focus(&first, cx);
cx.simulate_keystrokes("cmd-a cmd-x");
assert_eq!(first.read_with(cx, |field, _| field.text()), "");
cx.simulate_keystrokes("cmd-v");
assert_eq!(first.read_with(cx, |field, _| field.text()), "copy me");
}
#[gpui::test]
fn text_input_pasting_flattens_line_breaks(cx: &mut TestAppContext) {
let (view, cx) = pair(cx);
let field = view.read_with(cx, |view, _| view.first.clone());
focus(&field, cx);
cx.write_to_clipboard(gpui::ClipboardItem::new_string("one\ntwo\r\nthree".into()));
cx.simulate_keystrokes("cmd-v");
assert_eq!(
field.read_with(cx, |field, _| field.text()),
"one two three"
);
}
#[gpui::test]
fn text_input_undoes_by_word(cx: &mut TestAppContext) {
let (view, cx) = pair(cx);
let field = view.read_with(cx, |view, _| view.first.clone());
focus(&field, cx);
cx.simulate_input("alpha beta");
cx.simulate_keystrokes("cmd-z");
assert_eq!(field.read_with(cx, |field, _| field.text()), "alpha ");
cx.simulate_keystrokes("cmd-z");
assert_eq!(field.read_with(cx, |field, _| field.text()), "");
cx.simulate_keystrokes("cmd-shift-z cmd-shift-z");
assert_eq!(field.read_with(cx, |field, _| field.text()), "alpha beta");
}
#[gpui::test]
fn text_input_click_places_the_caret_and_drag_selects(cx: &mut TestAppContext) {
let (view, cx) = pair(cx);
let field = view.read_with(cx, |view, _| view.first.clone());
focus(&field, cx);
cx.simulate_input("hello world");
let (bounds, x_of) = field.read_with(cx, |field, _| {
let state = field.line();
let shaped = state.shaped.clone().expect("the field has been painted");
let bounds = state.bounds.expect("the field has been painted");
(bounds, move |index: usize| shaped.x_for_index(index))
});
let at = |index: usize| gpui::point(bounds.left() + x_of(index), bounds.center().y);
cx.simulate_click(at(2), Modifiers::none());
assert_eq!(field.read_with(cx, |field, _| field.edit().cursor()), 2);
cx.simulate_mouse_down(at(2), MouseButton::Left, Modifiers::none());
cx.simulate_mouse_move(at(7), MouseButton::Left, Modifiers::none());
assert_eq!(
field.read_with(cx, |field, _| field.edit().selected_text()),
Some("llo w".to_string())
);
cx.simulate_mouse_up(at(7), MouseButton::Left, Modifiers::none());
cx.simulate_input("X");
assert_eq!(field.read_with(cx, |field, _| field.text()), "heXorld");
}
#[gpui::test]
fn text_input_double_click_takes_a_word(cx: &mut TestAppContext) {
let (view, cx) = pair(cx);
let field = view.read_with(cx, |view, _| view.first.clone());
focus(&field, cx);
cx.simulate_input("hello world");
let (bounds, x_of) = field.read_with(cx, |field, _| {
let state = field.line();
let shaped = state.shaped.clone().expect("the field has been painted");
let bounds = state.bounds.expect("the field has been painted");
(bounds, move |index: usize| shaped.x_for_index(index))
});
let position = gpui::point(bounds.left() + x_of(8), bounds.center().y);
cx.simulate_event(gpui::MouseDownEvent {
button: MouseButton::Left,
position,
modifiers: Modifiers::none(),
click_count: 2,
first_mouse: false,
});
assert_eq!(
field.read_with(cx, |field, _| field.edit().selected_text()),
Some("world".to_string())
);
cx.simulate_event(gpui::MouseDownEvent {
button: MouseButton::Left,
position,
modifiers: Modifiers::none(),
click_count: 3,
first_mouse: false,
});
assert_eq!(
field.read_with(cx, |field, _| field.edit().selected_text()),
Some("hello world".to_string())
);
}
#[gpui::test]
fn text_input_honours_max_length(cx: &mut TestAppContext) {
cx.update(|cx| Theme::light().init(cx));
let (field, cx) = cx.add_window_view(|_window, cx| TextInput::new(cx).max_length(4));
focus(&field, cx);
cx.simulate_input("abcdefg");
assert_eq!(field.read_with(cx, |field, _| field.text()), "abcd");
cx.write_to_clipboard(gpui::ClipboardItem::new_string("xyz".into()));
cx.simulate_keystrokes("cmd-v");
assert_eq!(field.read_with(cx, |field, _| field.text()), "abcd");
}
#[gpui::test]
fn text_input_read_only_selects_but_never_edits(cx: &mut TestAppContext) {
cx.update(|cx| Theme::light().init(cx));
let (field, cx) =
cx.add_window_view(|_window, cx| TextInput::new(cx).read_only(true).value("locked"));
focus(&field, cx);
cx.simulate_input("nope");
cx.simulate_keystrokes("backspace");
assert_eq!(field.read_with(cx, |field, _| field.text()), "locked");
cx.simulate_keystrokes("cmd-a cmd-c");
assert_eq!(
cx.read_from_clipboard().and_then(|item| item.text()),
Some("locked".to_string())
);
}
#[gpui::test]
fn text_input_never_copies_a_password(cx: &mut TestAppContext) {
cx.update(|cx| Theme::light().init(cx));
cx.write_to_clipboard(gpui::ClipboardItem::new_string("untouched".into()));
let (field, cx) = cx.add_window_view(|_window, cx| TextInput::new(cx).password(true));
focus(&field, cx);
cx.simulate_input("hunter2");
cx.simulate_keystrokes("cmd-a cmd-c");
assert_eq!(
cx.read_from_clipboard().and_then(|item| item.text()),
Some("untouched".to_string())
);
cx.simulate_keystrokes("cmd-x");
assert_eq!(field.read_with(cx, |field, _| field.text()), "hunter2");
}
#[gpui::test]
fn chat_view_streams_a_reply_and_closes_it(cx: &mut TestAppContext) {
cx.update(|cx| Theme::light().init(cx));
let (chat, cx) = cx.add_window_view(|_window, cx| AIChatView::new(cx));
chat.update(cx, |chat, cx| {
chat.push(AITurn::user("hello"), cx);
chat.begin_reply(cx);
chat.push_delta("Hi", cx);
chat.push_delta(" there", cx);
chat.push_reasoning("weighing it up", cx);
});
chat.read_with(cx, |chat, _| {
assert_eq!(chat.turn_count(), 2);
let reply = chat.turn(1).unwrap();
assert_eq!(reply.body, "Hi there");
assert_eq!(reply.reasoning.as_deref(), Some("weighing it up"));
assert!(reply.streaming);
});
chat.update(cx, |chat, cx| chat.end_reply(cx));
chat.read_with(cx, |chat, _| assert!(!chat.turn(1).unwrap().streaming));
chat.update(cx, |chat, cx| chat.push_delta(" and more", cx));
chat.read_with(cx, |chat, _| {
assert_eq!(chat.turn(1).unwrap().body, "Hi there")
});
}
#[gpui::test]
fn chat_view_keeps_partial_text_when_a_reply_fails(cx: &mut TestAppContext) {
cx.update(|cx| Theme::light().init(cx));
let (chat, cx) = cx.add_window_view(|_window, cx| AIChatView::new(cx));
chat.update(cx, |chat, cx| {
chat.begin_reply(cx);
chat.push_delta("partial", cx);
chat.fail_reply("connection reset", cx);
});
chat.read_with(cx, |chat, _| {
let reply = chat.turn(0).unwrap();
assert_eq!(reply.body, "partial");
assert_eq!(reply.error.as_deref(), Some("connection reset"));
assert!(!reply.streaming);
});
chat.update(cx, |chat, cx| chat.fail_reply("late", cx));
chat.read_with(cx, |chat, _| {
assert_eq!(
chat.turn(0).unwrap().error.as_deref(),
Some("connection reset")
);
});
}
#[gpui::test]
fn chat_view_edits_are_bounds_checked(cx: &mut TestAppContext) {
cx.update(|cx| Theme::light().init(cx));
let (chat, cx) = cx.add_window_view(|_window, cx| AIChatView::new(cx));
chat.update(cx, |chat, cx| {
chat.update_turn(9, |turn| turn.body.push_str("nope"), cx);
assert!(chat.turn(9).is_none());
});
}
#[gpui::test]
fn chat_view_skips_turns_that_are_far_off_screen(cx: &mut TestAppContext) {
cx.update(|cx| Theme::light().init(cx));
let body = "## Heading\n\nA paragraph with **bold** and `code` in it.\n\n 1. one\n2. two\n\nAnother paragraph to give the turn some height.";
let turns: Vec<AITurn> = (0..60)
.map(|i| {
if i % 2 == 0 {
AITurn::user(format!("Question {i}"))
} else {
AITurn::assistant(body)
}
})
.collect();
let (chat, cx) = cx.add_window_view(|_window, cx| AIChatView::new(cx).turns(turns.clone()));
cx.run_until_parked();
let drawn = chat.update(cx, |chat, _| chat.drawn_count());
assert!(
drawn < 60,
"expected some turns to be skipped, drew {drawn}"
);
assert!(drawn > 0, "the visible turns must still be built");
let extent = chat.read_with(cx, |chat, _| chat.scroll_extent());
cx.run_until_parked();
let after = chat.read_with(cx, |chat, _| chat.scroll_extent());
assert_eq!(
extent, after,
"content height moved when turns became spacers"
);
cx.simulate_resize(gpui::size(gpui::px(500.0), gpui::px(700.0)));
cx.run_until_parked();
assert_eq!(
chat.update(cx, |chat, _| chat.drawn_count()),
60,
"a resize must re-measure every turn"
);
let (all, cx) = cx
.add_window_view(|_window, cx| AIChatView::new(cx).turns(turns.clone()).virtualize(false));
cx.run_until_parked();
assert_eq!(all.update(cx, |chat, _| chat.drawn_count()), 60);
}
#[gpui::test]
fn composer_sends_on_enter_and_refuses_blank_drafts(cx: &mut TestAppContext) {
cx.update(|cx| Theme::light().init(cx));
let (composer, cx) = cx.add_window_view(|_window, cx| AIComposer::new(cx));
let sent = std::rc::Rc::new(std::cell::RefCell::new(Vec::<String>::new()));
let sink = sent.clone();
cx.update(|_, cx| {
cx.subscribe(&composer, move |_composer, event: &AIComposerEvent, _cx| {
if let AIComposerEvent::Submit(text) = event {
sink.borrow_mut().push(text.clone());
}
})
.detach();
});
let input = composer.read_with(cx, |composer, _| composer.input().clone());
let handle = input.read_with(cx, |input, _| input.focus_handle());
cx.update(|window, _| window.focus(&handle));
cx.run_until_parked();
cx.simulate_input(" ");
cx.simulate_keystrokes("enter");
assert!(sent.borrow().is_empty());
cx.simulate_input("write a haiku");
cx.simulate_keystrokes("enter");
assert_eq!(sent.borrow().as_slice(), [" write a haiku"]);
assert_eq!(composer.read_with(cx, |composer, cx| composer.text(cx)), "");
cx.simulate_input("one");
cx.simulate_keystrokes("shift-enter");
cx.simulate_input("two");
assert_eq!(sent.borrow().len(), 1);
assert_eq!(
composer.read_with(cx, |composer, cx| composer.text(cx)),
"one\ntwo"
);
composer.update(cx, |composer, cx| composer.set_busy(true, cx));
cx.simulate_keystrokes("enter");
assert_eq!(sent.borrow().len(), 1);
}
struct Inspected {
devtools: Entity<DevTools>,
}
impl Render for Inspected {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.size_full()
.child(
crate::layout::Stack::new()
.child(crate::Button::new("save", "Save"))
.child(crate::Badge::new("new")),
)
.child(self.devtools.clone())
.probe("Inspected")
}
}
fn inspected(cx: &mut TestAppContext) -> (Entity<Inspected>, &mut gpui::VisualTestContext) {
cx.update(|cx| {
Theme::light().init(cx);
DevToolsState::new().init(cx);
});
cx.add_window_view(|_window, cx| Inspected {
devtools: cx.new(DevTools::new),
})
}
#[gpui::test]
fn the_recorder_rebuilds_the_component_tree(cx: &mut TestAppContext) {
let (view, cx) = inspected(cx);
cx.run_until_parked();
view.update(cx, |_this, cx| cx.notify());
cx.run_until_parked();
let names: Vec<String> = view.read_with(cx, |this, cx| {
this.devtools
.read(cx)
.tree()
.nodes
.iter()
.map(|node| node.name.to_string())
.collect()
});
assert!(names.contains(&"Inspected".to_string()), "{names:?}");
assert!(names.contains(&"Stack".to_string()), "{names:?}");
assert!(names.contains(&"Button".to_string()), "{names:?}");
assert!(names.contains(&"Badge".to_string()), "{names:?}");
}
#[gpui::test]
fn a_recorded_node_carries_its_attributes_style_and_source(cx: &mut TestAppContext) {
let (view, cx) = inspected(cx);
cx.run_until_parked();
view.update(cx, |_this, cx| cx.notify());
cx.run_until_parked();
view.read_with(cx, |this, cx| {
let tree = this.devtools.read(cx).tree().clone();
let button = tree
.nodes
.iter()
.find(|node| node.name.as_ref() == "Button")
.expect("the button should have reported itself");
assert!(button
.attrs
.iter()
.any(|(name, value)| name.as_ref() == "variant" && value.as_ref() == "filled"));
assert!(button.attrs.iter().any(|(name, _)| name.as_ref() == "size"));
let style = button
.style
.as_ref()
.expect("a styled root reports its style");
let declarations = crate::devtools::declarations(style);
assert!(declarations
.iter()
.any(|d| d.property.as_ref() == "background-color"));
let source = button.source.as_ref().expect("a probe records its caller");
assert_eq!(source.basename(), "button.rs");
assert!(f32::from(button.bounds.size.width) > 0.0);
});
}
#[gpui::test]
fn the_recorder_is_inert_until_an_inspector_exists(cx: &mut TestAppContext) {
assert!(!crate::devtools::is_recording());
let (view, cx) = inspected(cx);
cx.run_until_parked();
assert!(crate::devtools::is_recording());
view.update(cx, |this, cx| {
this.devtools = cx.new(DevTools::new);
});
cx.run_until_parked();
assert!(crate::devtools::is_recording());
}
#[gpui::test]
fn reported_records_read_back_out_of_the_store(cx: &mut TestAppContext) {
cx.update(|cx| {
DevToolsState::new().init(cx);
crate::devtools::log(cx, LogLevel::Warning, "cache miss");
crate::devtools::log(cx, LogLevel::Warning, "cache miss");
crate::devtools::log(cx, LogLevel::Error, "boom");
let id = crate::devtools::network_begin(
cx,
NetworkRecord::new("GET", "https://api.example.com/v1/items"),
)
.expect("the store is installed");
crate::devtools::network_update(cx, id, |record| {
record.state = RequestState::Finished;
record.status = Some(200);
});
crate::devtools::storage_set(
cx,
StorageDomain::new("prefs", "app.preferences")
.entry(StorageEntry::new("theme", "dark")),
);
});
cx.update(|cx| {
let state = cx.global::<DevToolsState>();
assert_eq!(state.logs().len(), 2);
assert_eq!(state.log_issues(), (2, 1));
assert_eq!(
state.logs()[0].source.as_ref().map(|s| s.basename()),
Some("apptests.rs")
);
assert_eq!(state.network()[0].status, Some(200));
assert_eq!(state.storage()[0].entries.len(), 1);
});
}
#[gpui::test]
fn reporting_without_the_store_installed_is_a_no_op(cx: &mut TestAppContext) {
cx.update(|cx| {
crate::devtools::log(cx, LogLevel::Error, "nobody is listening");
assert!(crate::devtools::network_begin(cx, NetworkRecord::new("GET", "/a")).is_none());
crate::devtools::storage_set(cx, StorageDomain::new("prefs", "Preferences"));
crate::devtools::clear(cx);
assert!(!cx.has_global::<DevToolsState>());
});
}
#[gpui::test]
fn clicking_a_source_link_switches_to_sources_and_tells_the_host(cx: &mut TestAppContext) {
let (view, cx) = inspected(cx);
let revealed = std::rc::Rc::new(std::cell::RefCell::new(Vec::<String>::new()));
let sink = revealed.clone();
view.update(cx, |this, cx| {
cx.subscribe(
&this.devtools,
move |_this, _devtools, event: &DevToolsEvent, _cx| {
if let DevToolsEvent::RevealSource(source) = event {
sink.borrow_mut().push(source.short());
}
},
)
.detach();
});
view.update(cx, |this, cx| {
this.devtools.update(cx, |devtools, cx| {
devtools.reveal_source(SourceRef::new("crates/guise/src/button.rs", 42, 9), cx);
});
});
cx.run_until_parked();
assert_eq!(revealed.borrow().as_slice(), ["button.rs:42:9"]);
assert_eq!(
view.read_with(cx, |this, cx| this.devtools.read(cx).active_tab()),
DevToolsTab::Sources
);
}
#[gpui::test]
fn picking_selects_the_deepest_node_under_the_point(cx: &mut TestAppContext) {
let (view, cx) = inspected(cx);
cx.run_until_parked();
view.update(cx, |_this, cx| cx.notify());
cx.run_until_parked();
let button_bounds = view.read_with(cx, |this, cx| {
this.devtools
.read(cx)
.tree()
.nodes
.iter()
.find(|node| node.name.as_ref() == "Button")
.map(|node| node.bounds)
.expect("the button should have reported itself")
});
view.update(cx, |this, cx| {
this.devtools.update(cx, |devtools, cx| {
devtools.set_picking(true, cx);
assert!(devtools.is_picking());
assert!(devtools.pick_at(button_bounds.center(), cx));
assert!(!devtools.is_picking());
assert_eq!(devtools.active_tab(), DevToolsTab::Elements);
assert_eq!(devtools.selected_bounds(), Some(button_bounds));
});
});
}