Add tests for the dock

K Simmons created

Change summary

crates/editor/src/editor.rs       |  22 +-
crates/editor/src/test.rs         |   2 
crates/workspace/src/dock.rs      | 322 +++++++++++++++++++++++++-------
crates/workspace/src/pane.rs      |   6 
crates/workspace/src/sidebar.rs   |   4 
crates/workspace/src/workspace.rs |   4 
6 files changed, 274 insertions(+), 86 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -7826,7 +7826,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
         cx.set_state("one «two threeˇ» four");
         cx.update_editor(|editor, cx| {
             editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
@@ -7974,7 +7974,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_newline_below(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
         cx.update(|cx| {
             cx.update_global::<Settings, _, _>(|settings, _| {
                 settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap());
@@ -8050,7 +8050,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_tab(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
         cx.update(|cx| {
             cx.update_global::<Settings, _, _>(|settings, _| {
                 settings.editor_overrides.tab_size = Some(NonZeroU32::new(3).unwrap());
@@ -8081,7 +8081,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_tab_on_blank_line_auto_indents(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
         let language = Arc::new(
             Language::new(
                 LanguageConfig::default(),
@@ -8139,7 +8139,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
 
         cx.set_state(indoc! {"
               «oneˇ» «twoˇ»
@@ -8208,7 +8208,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
         cx.update(|cx| {
             cx.update_global::<Settings, _, _>(|settings, _| {
                 settings.editor_overrides.hard_tabs = Some(true);
@@ -8416,7 +8416,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_backspace(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
 
         // Basic backspace
         cx.set_state(indoc! {"
@@ -8463,7 +8463,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_delete(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
 
         cx.set_state(indoc! {"
             onˇe two three
@@ -8800,7 +8800,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_clipboard(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
 
         cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
         cx.update_editor(|e, cx| e.cut(&Cut, cx));
@@ -8876,7 +8876,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
         let language = Arc::new(Language::new(
             LanguageConfig::default(),
             Some(tree_sitter_rust::language()),
@@ -9305,7 +9305,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_select_next(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
         cx.set_state("abc\nˇabc abc\ndefabc\nabc");
 
         cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));

crates/editor/src/test.rs 🔗

@@ -88,7 +88,7 @@ pub struct EditorTestContext<'a> {
 }
 
 impl<'a> EditorTestContext<'a> {
-    pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
+    pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
         let (window_id, editor) = cx.update(|cx| {
             cx.set_global(Settings::test(cx));
             crate::init(cx);

crates/workspace/src/dock.rs 🔗

@@ -26,7 +26,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Dock::move_dock);
 }
 
-#[derive(Copy, Clone, PartialEq, Eq)]
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
 pub enum DockPosition {
     Shown(DockAnchor),
     Hidden(DockAnchor),
@@ -372,12 +372,14 @@ impl StatusItemView for ToggleDockButton {
 
 #[cfg(test)]
 mod tests {
-    use super::*;
-    use gpui::{TestAppContext, ViewContext};
+    use std::ops::{Deref, DerefMut};
+
+    use gpui::{AppContext, TestAppContext, UpdateView, ViewContext};
     use project::{FakeFs, Project};
     use settings::Settings;
 
-    use crate::{tests::TestItem, ItemHandle, Workspace};
+    use super::*;
+    use crate::{sidebar::Sidebar, tests::TestItem, ItemHandle, Workspace};
 
     pub fn default_item_factory(
         _workspace: &mut Workspace,
@@ -388,83 +390,263 @@ mod tests {
 
     #[gpui::test]
     async fn test_dock_hides_when_pane_empty(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
-
-        Settings::test_async(cx);
-        let fs = FakeFs::new(cx.background());
+        let mut cx = DockTestContext::new(cx).await;
+
+        // Closing the last item in the dock hides the dock
+        cx.move_dock(DockAnchor::Right);
+        let old_items = cx.dock_items();
+        assert!(!old_items.is_empty());
+        cx.close_dock_items().await;
+        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
+
+        // Reopening the dock adds a new item
+        cx.move_dock(DockAnchor::Right);
+        let new_items = cx.dock_items();
+        assert!(!new_items.is_empty());
+        assert!(new_items
+            .into_iter()
+            .all(|new_item| !old_items.contains(&new_item)));
+    }
 
-        let project = Project::test(fs, [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
+    #[gpui::test]
+    async fn test_dock_panel_collisions(cx: &mut TestAppContext) {
+        let mut cx = DockTestContext::new(cx).await;
+
+        // Dock closes when expanded for either panel
+        cx.move_dock(DockAnchor::Expanded);
+        cx.open_sidebar(SidebarSide::Left);
+        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
+        cx.close_sidebar(SidebarSide::Left);
+        cx.move_dock(DockAnchor::Expanded);
+        cx.open_sidebar(SidebarSide::Right);
+        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
+
+        // Dock closes in the right position if the right sidebar is opened
+        cx.move_dock(DockAnchor::Right);
+        cx.open_sidebar(SidebarSide::Left);
+        cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
+        cx.open_sidebar(SidebarSide::Right);
+        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
+        cx.close_sidebar(SidebarSide::Right);
+
+        // Dock in bottom position ignores sidebars
+        cx.move_dock(DockAnchor::Bottom);
+        cx.open_sidebar(SidebarSide::Left);
+        cx.open_sidebar(SidebarSide::Right);
+        cx.assert_dock_position(DockPosition::Shown(DockAnchor::Bottom));
+
+        // Opening the dock in the right position closes the right sidebar
+        cx.move_dock(DockAnchor::Right);
+        cx.assert_sidebar_closed(SidebarSide::Right);
+    }
 
-        // Open dock
-        workspace.update(cx, |workspace, cx| {
-            Dock::show(workspace, cx);
-        });
+    #[gpui::test]
+    async fn test_focusing_panes_shows_and_hides_dock(cx: &mut TestAppContext) {
+        let mut cx = DockTestContext::new(cx).await;
+
+        // Focusing an item not in the dock when expanded hides the dock
+        let center_item = cx.add_item_to_center_pane();
+        cx.move_dock(DockAnchor::Expanded);
+        let dock_item = cx
+            .dock_items()
+            .get(0)
+            .cloned()
+            .expect("Dock should have an item at this point");
+        center_item.update(&mut cx, |_, cx| cx.focus_self());
+        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
+
+        // Focusing an item not in the dock when not expanded, leaves the dock open but inactive
+        cx.move_dock(DockAnchor::Right);
+        center_item.update(&mut cx, |_, cx| cx.focus_self());
+        cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
+        cx.assert_dock_pane_inactive();
+
+        // Focus dock item
+        dock_item.update(&mut cx, |_, cx| cx.focus_self());
+        cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
+        cx.assert_dock_pane_active();
+    }
 
-        // Ensure dock has an item in it
-        let dock_item_handle = workspace.read_with(cx, |workspace, cx| {
-            let dock = workspace.dock_pane().read(cx);
-            dock.items()
-                .next()
-                .expect("Dock should have an item in it")
-                .clone()
-        });
+    struct DockTestContext<'a> {
+        pub cx: &'a mut TestAppContext,
+        pub window_id: usize,
+        pub workspace: ViewHandle<Workspace>,
+    }
 
-        // Close item
-        let close_task = workspace.update(cx, |workspace, cx| {
-            Pane::close_item(
-                workspace,
-                workspace.dock_pane().clone(),
-                dock_item_handle.id(),
+    impl<'a> DockTestContext<'a> {
+        pub async fn new(cx: &'a mut TestAppContext) -> DockTestContext<'a> {
+            Settings::test_async(cx);
+            let fs = FakeFs::new(cx.background());
+
+            cx.update(|cx| init(cx));
+            let project = Project::test(fs, [], cx).await;
+            let (window_id, workspace) =
+                cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
+
+            workspace.update(cx, |workspace, cx| {
+                let left_panel = cx.add_view(|_| TestItem::new());
+                workspace.left_sidebar().update(cx, |sidebar, cx| {
+                    sidebar.add_item(
+                        "icons/folder_tree_16.svg",
+                        "Left Test Panel".to_string(),
+                        left_panel.clone(),
+                        cx,
+                    );
+                });
+
+                let right_panel = cx.add_view(|_| TestItem::new());
+                workspace.right_sidebar().update(cx, |sidebar, cx| {
+                    sidebar.add_item(
+                        "icons/folder_tree_16.svg",
+                        "Right Test Panel".to_string(),
+                        right_panel.clone(),
+                        cx,
+                    );
+                });
+            });
+
+            Self {
                 cx,
-            )
-        });
-        close_task.await.expect("Dock item closed successfully");
+                window_id,
+                workspace,
+            }
+        }
 
-        // Ensure dock closes
-        workspace.read_with(cx, |workspace, cx| {
-            assert!(workspace.dock.visible_pane().is_some())
-        });
+        pub fn workspace<F, T>(&self, read: F) -> T
+        where
+            F: FnOnce(&Workspace, &AppContext) -> T,
+        {
+            self.workspace.read_with(self.cx, read)
+        }
 
-        // Open again
-        workspace.update(cx, |workspace, cx| {
-            Dock::show(workspace, cx);
-        });
+        pub fn update_workspace<F, T>(&mut self, update: F) -> T
+        where
+            F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
+        {
+            self.workspace.update(self.cx, update)
+        }
 
-        // Ensure dock has item in it
-        workspace.read_with(cx, |workspace, cx| {
-            let dock = workspace.dock_pane().read(cx);
-            dock.items().next().expect("Dock should have an item in it");
-        });
+        pub fn sidebar<F, T>(&self, sidebar_side: SidebarSide, read: F) -> T
+        where
+            F: FnOnce(&Sidebar, &AppContext) -> T,
+        {
+            self.workspace(|workspace, cx| {
+                let sidebar = match sidebar_side {
+                    SidebarSide::Left => workspace.left_sidebar(),
+                    SidebarSide::Right => workspace.right_sidebar(),
+                }
+                .read(cx);
+
+                read(sidebar, cx)
+            })
+        }
+
+        pub fn add_item_to_center_pane(&mut self) -> ViewHandle<TestItem> {
+            self.update_workspace(|workspace, cx| {
+                let item = cx.add_view(|_| TestItem::new());
+                let pane = workspace
+                    .last_active_center_pane
+                    .clone()
+                    .unwrap_or_else(|| workspace.center.panes()[0].clone());
+                Pane::add_item(
+                    workspace,
+                    &pane,
+                    Box::new(item.clone()),
+                    true,
+                    true,
+                    None,
+                    cx,
+                );
+                item
+            })
+        }
+
+        pub fn dock_pane<F, T>(&self, read: F) -> T
+        where
+            F: FnOnce(&Pane, &AppContext) -> T,
+        {
+            self.workspace(|workspace, cx| {
+                let dock_pane = workspace.dock_pane().read(cx);
+                read(dock_pane, cx)
+            })
+        }
+
+        pub fn move_dock(&self, anchor: DockAnchor) {
+            self.cx.dispatch_action(self.window_id, MoveDock(anchor));
+        }
+
+        pub fn open_sidebar(&mut self, sidebar_side: SidebarSide) {
+            if !self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
+                self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
+            }
+        }
+
+        pub fn close_sidebar(&mut self, sidebar_side: SidebarSide) {
+            if self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
+                self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
+            }
+        }
+
+        pub fn dock_items(&self) -> Vec<ViewHandle<TestItem>> {
+            self.dock_pane(|pane, cx| {
+                pane.items()
+                    .map(|item| {
+                        item.act_as::<TestItem>(cx)
+                            .expect("Dock Test Context uses TestItems in the dock")
+                    })
+                    .collect()
+            })
+        }
+
+        pub async fn close_dock_items(&mut self) {
+            self.update_workspace(|workspace, cx| {
+                Pane::close_items(workspace, workspace.dock_pane().clone(), cx, |_| true)
+            })
+            .await
+            .expect("Could not close dock items")
+        }
+
+        pub fn assert_dock_position(&self, expected_position: DockPosition) {
+            self.workspace(|workspace, _| assert_eq!(workspace.dock.position, expected_position));
+        }
+
+        pub fn assert_sidebar_closed(&self, sidebar_side: SidebarSide) {
+            assert!(!self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()));
+        }
+
+        pub fn assert_dock_pane_active(&self) {
+            assert!(self.dock_pane(|pane, _| pane.is_active()))
+        }
+
+        pub fn assert_dock_pane_inactive(&self) {
+            assert!(!self.dock_pane(|pane, _| pane.is_active()))
+        }
     }
 
-    #[gpui::test]
-    async fn test_dock_panel_collisions(cx: &mut TestAppContext) {
-        // Open dock expanded
-        // Open left panel
-        // Ensure dock closes
-        // Open dock to the right
-        // Open left panel
-        // Ensure dock is left open
-        // Open right panel
-        // Ensure dock closes
-        // Open dock bottom
-        // Open left panel
-        // Open right panel
-        // Ensure dock still open
+    impl<'a> Deref for DockTestContext<'a> {
+        type Target = gpui::TestAppContext;
+
+        fn deref(&self) -> &Self::Target {
+            self.cx
+        }
     }
 
-    #[gpui::test]
-    async fn test_focusing_panes_shows_and_hides_dock(cx: &mut TestAppContext) {
-        // Open item in center pane
-        // Open dock expanded
-        // Focus new item
-        // Ensure the dock gets hidden
-        // Open dock to the right
-        // Focus new item
-        // Ensure dock stays shown but inactive
-        // Add item to dock and hide it
-        // Focus the added item
-        // Ensure the dock is open
+    impl<'a> DerefMut for DockTestContext<'a> {
+        fn deref_mut(&mut self) -> &mut Self::Target {
+            &mut self.cx
+        }
+    }
+
+    impl<'a> UpdateView for DockTestContext<'a> {
+        fn update_view<T, S>(
+            &mut self,
+            handle: &ViewHandle<T>,
+            update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
+        ) -> S
+        where
+            T: View,
+        {
+            handle.update(self.cx, update)
+        }
     }
 }

crates/workspace/src/pane.rs 🔗

@@ -706,7 +706,7 @@ impl Pane {
         pane: ViewHandle<Pane>,
         item_id_to_close: usize,
         cx: &mut ViewContext<Workspace>,
-    ) -> Task<Result<bool>> {
+    ) -> Task<Result<()>> {
         Self::close_items(workspace, pane, cx, move |view_id| {
             view_id == item_id_to_close
         })
@@ -717,7 +717,7 @@ impl Pane {
         pane: ViewHandle<Pane>,
         cx: &mut ViewContext<Workspace>,
         should_close: impl 'static + Fn(usize) -> bool,
-    ) -> Task<Result<bool>> {
+    ) -> Task<Result<()>> {
         let project = workspace.project().clone();
 
         // Find the items to close.
@@ -790,7 +790,7 @@ impl Pane {
             }
 
             pane.update(&mut cx, |_, cx| cx.notify());
-            Ok(true)
+            Ok(())
         })
     }
 

crates/workspace/src/sidebar.rs 🔗

@@ -11,7 +11,9 @@ pub trait SidebarItem: View {
     fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool {
         false
     }
-    fn should_show_badge(&self, cx: &AppContext) -> bool;
+    fn should_show_badge(&self, _: &AppContext) -> bool {
+        false
+    }
     fn contains_focused_view(&self, _: &AppContext) -> bool {
         false
     }

crates/workspace/src/workspace.rs 🔗

@@ -2934,6 +2934,8 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
 mod tests {
     use std::cell::Cell;
 
+    use crate::sidebar::SidebarItem;
+
     use super::*;
     use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
     use project::{FakeFs, Project, ProjectEntryId};
@@ -3724,4 +3726,6 @@ mod tests {
             vec![ItemEvent::UpdateTab, ItemEvent::Edit]
         }
     }
+
+    impl SidebarItem for TestItem {}
 }