Add command palette tests and simulate_keystrokes

Conrad Irwin created

Change summary

crates/command_palette2/src/command_palette.rs | 240 +++++++++----------
crates/gpui2/src/action.rs                     |  11 
crates/gpui2/src/app/test_context.rs           |  60 ++++
crates/gpui2/src/elements/div.rs               |  14 +
crates/gpui2/src/platform/test/window.rs       |   6 
crates/gpui2/src/window.rs                     |   2 
6 files changed, 197 insertions(+), 136 deletions(-)

Detailed changes

crates/command_palette2/src/command_palette.rs 🔗

@@ -354,129 +354,117 @@ impl std::fmt::Debug for Command {
     }
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use std::sync::Arc;
-
-//     use super::*;
-//     use editor::Editor;
-//     use gpui::{executor::Deterministic, TestAppContext};
-//     use project::Project;
-//     use workspace::{AppState, Workspace};
-
-//     #[test]
-//     fn test_humanize_action_name() {
-//         assert_eq!(
-//             humanize_action_name("editor::GoToDefinition"),
-//             "editor: go to definition"
-//         );
-//         assert_eq!(
-//             humanize_action_name("editor::Backspace"),
-//             "editor: backspace"
-//         );
-//         assert_eq!(
-//             humanize_action_name("go_to_line::Deploy"),
-//             "go to line: deploy"
-//         );
-//     }
-
-//     #[gpui::test]
-//     async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-
-//         let project = Project::test(app_state.fs.clone(), [], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-//         let workspace = window.root(cx);
-//         let editor = window.add_view(cx, |cx| {
-//             let mut editor = Editor::single_line(None, cx);
-//             editor.set_text("abc", cx);
-//             editor
-//         });
-
-//         workspace.update(cx, |workspace, cx| {
-//             cx.focus(&editor);
-//             workspace.add_item(Box::new(editor.clone()), cx)
-//         });
-
-//         workspace.update(cx, |workspace, cx| {
-//             toggle_command_palette(workspace, &Toggle, cx);
-//         });
-
-//         let palette = workspace.read_with(cx, |workspace, _| {
-//             workspace.modal::<CommandPalette>().unwrap()
-//         });
-
-//         palette
-//             .update(cx, |palette, cx| {
-//                 // Fill up palette's command list by running an empty query;
-//                 // we only need it to subsequently assert that the palette is initially
-//                 // sorted by command's name.
-//                 palette.delegate_mut().update_matches("".to_string(), cx)
-//             })
-//             .await;
-
-//         palette.update(cx, |palette, _| {
-//             let is_sorted =
-//                 |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
-//             assert!(is_sorted(&palette.delegate().actions));
-//         });
-
-//         palette
-//             .update(cx, |palette, cx| {
-//                 palette
-//                     .delegate_mut()
-//                     .update_matches("bcksp".to_string(), cx)
-//             })
-//             .await;
-
-//         palette.update(cx, |palette, cx| {
-//             assert_eq!(palette.delegate().matches[0].string, "editor: backspace");
-//             palette.confirm(&Default::default(), cx);
-//         });
-//         deterministic.run_until_parked();
-//         editor.read_with(cx, |editor, cx| {
-//             assert_eq!(editor.text(cx), "ab");
-//         });
-
-//         // Add namespace filter, and redeploy the palette
-//         cx.update(|cx| {
-//             cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
-//                 filter.filtered_namespaces.insert("editor");
-//             })
-//         });
-
-//         workspace.update(cx, |workspace, cx| {
-//             toggle_command_palette(workspace, &Toggle, cx);
-//         });
-
-//         // Assert editor command not present
-//         let palette = workspace.read_with(cx, |workspace, _| {
-//             workspace.modal::<CommandPalette>().unwrap()
-//         });
-
-//         palette
-//             .update(cx, |palette, cx| {
-//                 palette
-//                     .delegate_mut()
-//                     .update_matches("bcksp".to_string(), cx)
-//             })
-//             .await;
-
-//         palette.update(cx, |palette, _| {
-//             assert!(palette.delegate().matches.is_empty())
-//         });
-//     }
-
-//     fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
-//         cx.update(|cx| {
-//             let app_state = AppState::test(cx);
-//             theme::init(cx);
-//             language::init(cx);
-//             editor::init(cx);
-//             workspace::init(app_state.clone(), cx);
-//             init(cx);
-//             Project::init_settings(cx);
-//             app_state
-//         })
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use std::sync::Arc;
+
+    use super::*;
+    use editor::Editor;
+    use gpui::TestAppContext;
+    use project::Project;
+    use workspace::{AppState, Workspace};
+
+    #[test]
+    fn test_humanize_action_name() {
+        assert_eq!(
+            humanize_action_name("editor::GoToDefinition"),
+            "editor: go to definition"
+        );
+        assert_eq!(
+            humanize_action_name("editor::Backspace"),
+            "editor: backspace"
+        );
+        assert_eq!(
+            humanize_action_name("go_to_line::Deploy"),
+            "go to line: deploy"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_command_palette(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
+        let cx = &mut cx;
+
+        let editor = cx.build_view(|cx| {
+            let mut editor = Editor::single_line(cx);
+            editor.set_text("abc", cx);
+            editor
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            workspace.add_item(Box::new(editor.clone()), cx);
+            editor.update(cx, |editor, cx| editor.focus(cx))
+        });
+
+        cx.simulate_keystrokes("cmd-shift-p");
+
+        let palette = workspace.update(cx, |workspace, cx| {
+            workspace
+                .current_modal::<CommandPalette>(cx)
+                .unwrap()
+                .read(cx)
+                .picker
+                .clone()
+        });
+
+        palette.update(cx, |palette, _| {
+            assert!(palette.delegate.commands.len() > 5);
+            let is_sorted =
+                |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
+            assert!(is_sorted(&palette.delegate.commands));
+        });
+
+        cx.simulate_keystrokes("b c k s p");
+
+        palette.update(cx, |palette, _| {
+            assert_eq!(palette.delegate.matches[0].string, "editor: backspace");
+        });
+
+        cx.simulate_keystrokes("enter");
+
+        workspace.update(cx, |workspace, cx| {
+            assert!(workspace.current_modal::<CommandPalette>(cx).is_none());
+            assert_eq!(editor.read(cx).text(cx), "ab")
+        });
+
+        // Add namespace filter, and redeploy the palette
+        cx.update(|cx| {
+            cx.set_global(CommandPaletteFilter::default());
+            cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
+                filter.filtered_namespaces.insert("editor");
+            })
+        });
+
+        cx.simulate_keystrokes("cmd-shift-p");
+        cx.simulate_keystrokes("b c k s p");
+
+        let palette = workspace.update(cx, |workspace, cx| {
+            workspace
+                .current_modal::<CommandPalette>(cx)
+                .unwrap()
+                .read(cx)
+                .picker
+                .clone()
+        });
+        palette.update(cx, |palette, _| {
+            assert!(palette.delegate.matches.is_empty())
+        });
+    }
+
+    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+        cx.update(|cx| {
+            let app_state = AppState::test(cx);
+            theme::init(cx);
+            language::init(cx);
+            editor::init(cx);
+            workspace::init(app_state.clone(), cx);
+            init(cx);
+            Project::init_settings(cx);
+            settings::load_default_keymap(cx);
+            app_state
+        })
+    }
+}

crates/gpui2/src/action.rs 🔗

@@ -54,6 +54,9 @@ pub trait Action: std::fmt::Debug + 'static {
     where
         Self: Sized;
     fn build(value: Option<serde_json::Value>) -> Result<Box<dyn Action>>
+    where
+        Self: Sized;
+    fn is_registered() -> bool
     where
         Self: Sized;
 
@@ -88,6 +91,14 @@ where
         Ok(Box::new(action))
     }
 
+    fn is_registered() -> bool {
+        ACTION_REGISTRY
+            .read()
+            .names_by_type_id
+            .get(&TypeId::of::<A>())
+            .is_some()
+    }
+
     fn partial_eq(&self, action: &dyn Action) -> bool {
         action
             .as_any()

crates/gpui2/src/app/test_context.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
     div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
     BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent,
-    Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View,
-    ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
+    Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TestWindow,
+    View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
 };
 use anyhow::{anyhow, bail};
 use futures::{Stream, StreamExt};
@@ -220,7 +220,21 @@ impl TestAppContext {
     {
         window
             .update(self, |_, cx| cx.dispatch_action(action.boxed_clone()))
-            .unwrap()
+            .unwrap();
+
+        self.background_executor.run_until_parked()
+    }
+
+    pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
+        for keystroke in keystrokes
+            .split(" ")
+            .map(Keystroke::parse)
+            .map(Result::unwrap)
+        {
+            self.dispatch_keystroke(window, keystroke.into(), false);
+        }
+
+        self.background_executor.run_until_parked()
     }
 
     pub fn dispatch_keystroke(
@@ -229,15 +243,41 @@ impl TestAppContext {
         keystroke: Keystroke,
         is_held: bool,
     ) {
+        let keystroke2 = keystroke.clone();
         let handled = window
             .update(self, |_, cx| {
                 cx.dispatch_event(InputEvent::KeyDown(KeyDownEvent { keystroke, is_held }))
             })
             .is_ok_and(|handled| handled);
-
-        if !handled {
-            // todo!() simluate input here
+        if handled {
+            return;
         }
+
+        let input_handler = self.update_test_window(window, |window| window.input_handler.clone());
+        let Some(input_handler) = input_handler else {
+            panic!(
+                "dispatch_keystroke {:?} failed to dispatch action or input",
+                &keystroke2
+            );
+        };
+        let text = keystroke2.ime_key.unwrap_or(keystroke2.key);
+        input_handler.lock().replace_text_in_range(None, &text);
+    }
+
+    pub fn update_test_window<R>(
+        &mut self,
+        window: AnyWindowHandle,
+        f: impl FnOnce(&mut TestWindow) -> R,
+    ) -> R {
+        window
+            .update(self, |_, cx| {
+                f(cx.window
+                    .platform_window
+                    .as_any_mut()
+                    .downcast_mut::<TestWindow>()
+                    .unwrap())
+            })
+            .unwrap()
     }
 
     pub fn notifications<T: 'static>(&mut self, entity: &Model<T>) -> impl Stream<Item = ()> {
@@ -401,12 +441,20 @@ impl<'a> VisualTestContext<'a> {
         Self { cx, window }
     }
 
+    pub fn run_until_parked(&self) {
+        self.cx.background_executor.run_until_parked();
+    }
+
     pub fn dispatch_action<A>(&mut self, action: A)
     where
         A: Action,
     {
         self.cx.dispatch_action(self.window, action)
     }
+
+    pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
+        self.cx.simulate_keystrokes(self.window, keystrokes)
+    }
 }
 
 impl<'a> Context for VisualTestContext<'a> {

crates/gpui2/src/elements/div.rs 🔗

@@ -229,6 +229,20 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
         mut self,
         listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
     ) -> Self {
+        // NOTE: this debug assert has the side-effect of working around
+        // a bug where a crate consisting only of action definitions does
+        // not register the actions in debug builds:
+        //
+        // https://github.com/rust-lang/rust/issues/47384
+        // https://github.com/mmastrac/rust-ctor/issues/280
+        //
+        // if we are relying on this side-effect still, removing the debug_assert!
+        // likely breaks the command_palette tests.
+        debug_assert!(
+            A::is_registered(),
+            "{:?} is not registered as an action",
+            A::qualified_name()
+        );
         self.interactivity().action_listeners.push((
             TypeId::of::<A>(),
             Box::new(move |view, action, phase, cx| {

crates/gpui2/src/platform/test/window.rs 🔗

@@ -22,7 +22,7 @@ pub struct TestWindow {
     bounds: WindowBounds,
     current_scene: Mutex<Option<Scene>>,
     display: Rc<dyn PlatformDisplay>,
-    input_handler: Option<Box<dyn PlatformInputHandler>>,
+    pub(crate) input_handler: Option<Arc<Mutex<Box<dyn PlatformInputHandler>>>>,
     handlers: Mutex<Handlers>,
     platform: Weak<TestPlatform>,
     sprite_atlas: Arc<dyn PlatformAtlas>,
@@ -80,11 +80,11 @@ impl PlatformWindow for TestWindow {
     }
 
     fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
-        todo!()
+        self
     }
 
     fn set_input_handler(&mut self, input_handler: Box<dyn crate::PlatformInputHandler>) {
-        self.input_handler = Some(input_handler);
+        self.input_handler = Some(Arc::new(Mutex::new(input_handler)));
     }
 
     fn prompt(

crates/gpui2/src/window.rs 🔗

@@ -189,7 +189,7 @@ impl Drop for FocusHandle {
 pub struct Window {
     pub(crate) handle: AnyWindowHandle,
     pub(crate) removed: bool,
-    platform_window: Box<dyn PlatformWindow>,
+    pub(crate) platform_window: Box<dyn PlatformWindow>,
     display_id: DisplayId,
     sprite_atlas: Arc<dyn PlatformAtlas>,
     rem_size: Pixels,