607 lines · 18.2 KB · Rust
RawDownload
//! A clean testing API for GPUI applications.//!//! `TestApp` provides a simpler alternative to `TestAppContext` with://! - Automatic effect flushing after updates//! - Clean window creation and inspection//! - Input simulation helpers//!//! # Example//! ```ignore//! #[test]//! fn test_my_view() {//!     let mut app = TestApp::new();//!//!     let mut window = app.open_window(|window, cx| {//!         MyView::new(window, cx)//!     });//!//!     window.update(|view, window, cx| {//!         view.do_something(cx);//!     });//!//!     // Check rendered state//!     assert_eq!(window.title(), Some("Expected Title"));//! }//! ``` use crate::{    AnyWindowHandle, App, AppCell, AppContext, AsyncApp, BackgroundExecutor, BorrowAppContext,    Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke,    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform,    PlatformTextSystem, Point, Render, Size, Task, TestDispatcher, TestPlatform, TextSystem,    Window, WindowBounds, WindowHandle, WindowOptions, app::GpuiMode,};use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; /// A test application context with a clean API.////// Unlike `TestAppContext`, `TestApp` automatically flushes effects after/// each update and provides simpler window management.pub struct TestApp {    app: Rc<AppCell>,    platform: Rc<TestPlatform>,    background_executor: BackgroundExecutor,    foreground_executor: ForegroundExecutor,    #[allow(dead_code)]    dispatcher: TestDispatcher,    text_system: Arc<TextSystem>,} impl TestApp {    /// Create a new test application.    pub fn new() -> Self {        Self::with_seed(0)    }     /// Create a new test application with a specific random seed.    pub fn with_seed(seed: u64) -> Self {        Self::build(seed, None, Arc::new(()))    }     /// Create a new test application with a custom text system for real font shaping.    pub fn with_text_system(text_system: Arc<dyn PlatformTextSystem>) -> Self {        Self::build(0, Some(text_system), Arc::new(()))    }     /// Create a new test application with a custom text system and asset source.    pub fn with_text_system_and_assets(        text_system: Arc<dyn PlatformTextSystem>,        asset_source: Arc<dyn crate::AssetSource>,    ) -> Self {        Self::build(0, Some(text_system), asset_source)    }     fn build(        seed: u64,        platform_text_system: Option<Arc<dyn PlatformTextSystem>>,        asset_source: Arc<dyn crate::AssetSource>,    ) -> Self {        let dispatcher = TestDispatcher::new(seed);        let arc_dispatcher = Arc::new(dispatcher.clone());        let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());        let foreground_executor = ForegroundExecutor::new(arc_dispatcher);        let platform = match platform_text_system.clone() {            Some(ts) => TestPlatform::with_text_system(                background_executor.clone(),                foreground_executor.clone(),                ts,            ),            None => TestPlatform::new(background_executor.clone(), foreground_executor.clone()),        };        let http_client = http_client::FakeHttpClient::with_404_response();        let text_system = Arc::new(TextSystem::new(            platform_text_system.unwrap_or_else(|| platform.text_system.clone()),        ));         let app = App::new_app(platform.clone(), asset_source, http_client);        app.borrow_mut().mode = GpuiMode::test();         Self {            app,            platform,            background_executor,            foreground_executor,            dispatcher,            text_system,        }    }     /// Run a closure with mutable access to the App context.    /// Automatically runs until parked after the closure completes.    pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {        let result = {            let mut app = self.app.borrow_mut();            app.update(f)        };        self.run_until_parked();        result    }     /// Run a closure with read-only access to the App context.    pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {        let app = self.app.borrow();        f(&app)    }     /// Create a new entity in the app.    pub fn new_entity<T: 'static>(        &mut self,        build: impl FnOnce(&mut Context<T>) -> T,    ) -> Entity<T> {        self.update(|cx| cx.new(build))    }     /// Update an entity.    pub fn update_entity<T: 'static, R>(        &mut self,        entity: &Entity<T>,        f: impl FnOnce(&mut T, &mut Context<T>) -> R,    ) -> R {        self.update(|cx| entity.update(cx, f))    }     /// Read an entity.    pub fn read_entity<T: 'static, R>(        &self,        entity: &Entity<T>,        f: impl FnOnce(&T, &App) -> R,    ) -> R {        self.read(|cx| f(entity.read(cx), cx))    }     /// Open a test window with the given root view, using maximized bounds.    pub fn open_window<V: Render + 'static>(        &mut self,        build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,    ) -> TestAppWindow<V> {        let bounds = self.read(|cx| Bounds::maximized(None, cx));        let handle = self.update(|cx| {            cx.open_window(                WindowOptions {                    window_bounds: Some(WindowBounds::Windowed(bounds)),                    ..Default::default()                },                |window, cx| cx.new(|cx| build_view(window, cx)),            )            .unwrap()        });         TestAppWindow {            handle,            app: self.app.clone(),            platform: self.platform.clone(),            background_executor: self.background_executor.clone(),        }    }     /// Open a test window with specific options.    pub fn open_window_with_options<V: Render + 'static>(        &mut self,        options: WindowOptions,        build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,    ) -> TestAppWindow<V> {        let handle = self.update(|cx| {            cx.open_window(options, |window, cx| cx.new(|cx| build_view(window, cx)))                .unwrap()        });         TestAppWindow {            handle,            app: self.app.clone(),            platform: self.platform.clone(),            background_executor: self.background_executor.clone(),        }    }     /// Run pending tasks until there's nothing left to do.    pub fn run_until_parked(&self) {        self.background_executor.run_until_parked();    }     /// Advance the simulated clock by the given duration.    pub fn advance_clock(&self, duration: Duration) {        self.background_executor.advance_clock(duration);    }     /// Spawn a future on the foreground executor.    pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task<R>    where        Fut: Future<Output = R> + 'static,        R: 'static,    {        self.foreground_executor.spawn(f(self.to_async()))    }     /// Spawn a future on the background executor.    pub fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>    where        R: Send + 'static,    {        self.background_executor.spawn(future)    }     /// Get an async handle to the app.    pub fn to_async(&self) -> AsyncApp {        AsyncApp {            app: Rc::downgrade(&self.app),            background_executor: self.background_executor.clone(),            foreground_executor: self.foreground_executor.clone(),        }    }     /// Get the background executor.    pub fn background_executor(&self) -> &BackgroundExecutor {        &self.background_executor    }     /// Get the foreground executor.    pub fn foreground_executor(&self) -> &ForegroundExecutor {        &self.foreground_executor    }     /// Get the text system.    pub fn text_system(&self) -> &Arc<TextSystem> {        &self.text_system    }     /// Check if a global of the given type exists.    pub fn has_global<G: Global>(&self) -> bool {        self.read(|cx| cx.has_global::<G>())    }     /// Set a global value.    pub fn set_global<G: Global>(&mut self, global: G) {        self.update(|cx| cx.set_global(global));    }     /// Read a global value.    pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {        self.read(|cx| f(cx.global(), cx))    }     /// Update a global value.    pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {        self.update(|cx| cx.update_global(f))    }     // Platform simulation methods     /// Write text to the simulated clipboard.    pub fn write_to_clipboard(&self, item: ClipboardItem) {        self.platform.write_to_clipboard(item);    }     /// Read from the simulated clipboard.    pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {        self.platform.read_from_clipboard()    }     /// Get URLs that have been opened via `cx.open_url()`.    pub fn opened_url(&self) -> Option<String> {        self.platform.opened_url.borrow().clone()    }     /// Check if a file path prompt is pending.    pub fn did_prompt_for_new_path(&self) -> bool {        self.platform.did_prompt_for_new_path()    }     /// Simulate answering a path selection dialog.    pub fn simulate_new_path_selection(        &self,        select: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,    ) {        self.platform.simulate_new_path_selection(select);    }     /// Check if a prompt dialog is pending.    pub fn has_pending_prompt(&self) -> bool {        self.platform.has_pending_prompt()    }     /// Simulate answering a prompt dialog.    pub fn simulate_prompt_answer(&self, button: &str) {        self.platform.simulate_prompt_answer(button);    }     /// Get all open windows.    pub fn windows(&self) -> Vec<AnyWindowHandle> {        self.read(|cx| cx.windows())    }} impl Default for TestApp {    fn default() -> Self {        Self::new()    }} /// A test window with inspection and simulation capabilities.pub struct TestAppWindow<V> {    handle: WindowHandle<V>,    app: Rc<AppCell>,    platform: Rc<TestPlatform>,    background_executor: BackgroundExecutor,} impl<V: 'static + Render> TestAppWindow<V> {    /// Get the window handle.    pub fn handle(&self) -> WindowHandle<V> {        self.handle    }     /// Get the root view entity.    pub fn root(&self) -> Entity<V> {        let mut app = self.app.borrow_mut();        let any_handle: AnyWindowHandle = self.handle.into();        app.update_window(any_handle, |root_view, _, _| {            root_view.downcast::<V>().expect("root view type mismatch")        })        .expect("window not found")    }     /// Update the root view.    pub fn update<R>(&mut self, f: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R) -> R {        let result = {            let mut app = self.app.borrow_mut();            let any_handle: AnyWindowHandle = self.handle.into();            app.update_window(any_handle, |root_view, window, cx| {                let view = root_view.downcast::<V>().expect("root view type mismatch");                view.update(cx, |view, cx| f(view, window, cx))            })            .expect("window not found")        };        self.background_executor.run_until_parked();        result    }     /// Read the root view.    pub fn read<R>(&self, f: impl FnOnce(&V, &App) -> R) -> R {        let app = self.app.borrow();        let view = self            .app            .borrow()            .windows            .get(self.handle.window_id())            .and_then(|w| w.as_ref())            .and_then(|w| w.root.clone())            .and_then(|r| r.downcast::<V>().ok())            .expect("window or root view not found");        f(view.read(&app), &app)    }     /// Get the window title.    pub fn title(&self) -> Option<String> {        let app = self.app.borrow();        app.read_window(&self.handle, |_, _cx| {            // TODO: expose title through Window API            None        })        .unwrap()    }     /// Simulate a keystroke.    pub fn simulate_keystroke(&mut self, keystroke: &str) {        let keystroke = Keystroke::parse(keystroke).unwrap();        {            let mut app = self.app.borrow_mut();            let any_handle: AnyWindowHandle = self.handle.into();            app.update_window(any_handle, |_, window, cx| {                window.dispatch_keystroke(keystroke, cx);            })            .unwrap();        }        self.background_executor.run_until_parked();    }     /// Simulate multiple keystrokes (space-separated).    pub fn simulate_keystrokes(&mut self, keystrokes: &str) {        for keystroke in keystrokes.split(' ') {            self.simulate_keystroke(keystroke);        }    }     /// Simulate typing text.    pub fn simulate_input(&mut self, input: &str) {        for char in input.chars() {            self.simulate_keystroke(&char.to_string());        }    }     /// Simulate a mouse move.    pub fn simulate_mouse_move(&mut self, position: Point<Pixels>) {        self.simulate_event(MouseMoveEvent {            position,            modifiers: Default::default(),            pressed_button: None,        });    }     /// Simulate a mouse down event.    pub fn simulate_mouse_down(&mut self, position: Point<Pixels>, button: MouseButton) {        self.simulate_event(MouseDownEvent {            position,            button,            modifiers: Default::default(),            click_count: 1,            first_mouse: false,        });    }     /// Simulate a mouse up event.    pub fn simulate_mouse_up(&mut self, position: Point<Pixels>, button: MouseButton) {        self.simulate_event(MouseUpEvent {            position,            button,            modifiers: Default::default(),            click_count: 1,        });    }     /// Simulate a click at the given position.    pub fn simulate_click(&mut self, position: Point<Pixels>, button: MouseButton) {        self.simulate_mouse_down(position, button);        self.simulate_mouse_up(position, button);    }     /// Simulate a scroll event.    pub fn simulate_scroll(&mut self, position: Point<Pixels>, delta: Point<Pixels>) {        self.simulate_event(crate::ScrollWheelEvent {            position,            delta: crate::ScrollDelta::Pixels(delta),            modifiers: Default::default(),            touch_phase: crate::TouchPhase::Moved,        });    }     /// Simulate an input event.    pub fn simulate_event<E: InputEvent>(&mut self, event: E) {        let platform_input = event.to_platform_input();        {            let mut app = self.app.borrow_mut();            let any_handle: AnyWindowHandle = self.handle.into();            app.update_window(any_handle, |_, window, cx| {                window.dispatch_event(platform_input, cx);            })            .unwrap();        }        self.background_executor.run_until_parked();    }     /// Simulate resizing the window.    pub fn simulate_resize(&mut self, size: Size<Pixels>) {        let window_id = self.handle.window_id();        let mut app = self.app.borrow_mut();        if let Some(Some(window)) = app.windows.get_mut(window_id) {            if let Some(test_window) = window.platform_window.as_test() {                test_window.simulate_resize(size);            }        }        drop(app);        self.background_executor.run_until_parked();    }     /// Force a redraw of the window.    pub fn draw(&mut self) {        let mut app = self.app.borrow_mut();        let any_handle: AnyWindowHandle = self.handle.into();        app.update_window(any_handle, |_, window, cx| {            window.draw(cx).clear(cx);        })        .unwrap();    }} impl<V> Clone for TestAppWindow<V> {    fn clone(&self) -> Self {        Self {            handle: self.handle,            app: self.app.clone(),            platform: self.platform.clone(),

Lines 1–500 of 607.