Add notifications integration test

Max Brunsfeld created

Change summary

crates/collab/src/tests.rs                     |   1 
crates/collab/src/tests/notification_tests.rs  | 115 ++++++++++++++++++++
crates/collab/src/tests/test_server.rs         |   7 +
crates/collab_ui/src/notification_panel.rs     |  25 ---
crates/notifications/src/notification_store.rs |  36 ++++++
5 files changed, 163 insertions(+), 21 deletions(-)

Detailed changes

crates/collab/src/tests.rs 🔗

@@ -6,6 +6,7 @@ mod channel_message_tests;
 mod channel_tests;
 mod following_tests;
 mod integration_tests;
+mod notification_tests;
 mod random_channel_buffer_tests;
 mod random_project_collaboration_tests;
 mod randomized_test_helpers;

crates/collab/src/tests/notification_tests.rs 🔗

@@ -0,0 +1,115 @@
+use crate::tests::TestServer;
+use gpui::{executor::Deterministic, TestAppContext};
+use rpc::Notification;
+use std::sync::Arc;
+
+#[gpui::test]
+async fn test_notifications(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+
+    // Client A sends a contact request to client B.
+    client_a
+        .user_store()
+        .update(cx_a, |store, cx| store.request_contact(client_b.id(), cx))
+        .await
+        .unwrap();
+
+    // Client B receives a contact request notification and responds to the
+    // request, accepting it.
+    deterministic.run_until_parked();
+    client_b.notification_store().update(cx_b, |store, cx| {
+        assert_eq!(store.notification_count(), 1);
+        assert_eq!(store.unread_notification_count(), 1);
+
+        let entry = store.notification_at(0).unwrap();
+        assert_eq!(
+            entry.notification,
+            Notification::ContactRequest {
+                sender_id: client_a.id()
+            }
+        );
+        assert!(!entry.is_read);
+
+        store.respond_to_notification(entry.notification.clone(), true, cx);
+    });
+
+    // Client B sees the notification is now read, and that they responded.
+    deterministic.run_until_parked();
+    client_b.notification_store().read_with(cx_b, |store, _| {
+        assert_eq!(store.notification_count(), 1);
+        assert_eq!(store.unread_notification_count(), 0);
+
+        let entry = store.notification_at(0).unwrap();
+        assert!(entry.is_read);
+        assert_eq!(entry.response, Some(true));
+    });
+
+    // Client A receives a notification that client B accepted their request.
+    client_a.notification_store().read_with(cx_a, |store, _| {
+        assert_eq!(store.notification_count(), 1);
+        assert_eq!(store.unread_notification_count(), 1);
+
+        let entry = store.notification_at(0).unwrap();
+        assert_eq!(
+            entry.notification,
+            Notification::ContactRequestAccepted {
+                responder_id: client_b.id()
+            }
+        );
+        assert!(!entry.is_read);
+    });
+
+    // Client A creates a channel and invites client B to be a member.
+    let channel_id = client_a
+        .channel_store()
+        .update(cx_a, |store, cx| {
+            store.create_channel("the-channel", None, cx)
+        })
+        .await
+        .unwrap();
+    client_a
+        .channel_store()
+        .update(cx_a, |store, cx| {
+            store.invite_member(channel_id, client_b.id(), false, cx)
+        })
+        .await
+        .unwrap();
+
+    // Client B receives a channel invitation notification and responds to the
+    // invitation, accepting it.
+    deterministic.run_until_parked();
+    client_b.notification_store().update(cx_b, |store, cx| {
+        assert_eq!(store.notification_count(), 2);
+        assert_eq!(store.unread_notification_count(), 1);
+
+        let entry = store.notification_at(1).unwrap();
+        assert_eq!(
+            entry.notification,
+            Notification::ChannelInvitation {
+                channel_id,
+                channel_name: "the-channel".to_string()
+            }
+        );
+        assert!(!entry.is_read);
+
+        store.respond_to_notification(entry.notification.clone(), true, cx);
+    });
+
+    // Client B sees the notification is now read, and that they responded.
+    deterministic.run_until_parked();
+    client_b.notification_store().read_with(cx_b, |store, _| {
+        assert_eq!(store.notification_count(), 2);
+        assert_eq!(store.unread_notification_count(), 0);
+
+        let entry = store.notification_at(1).unwrap();
+        assert!(entry.is_read);
+        assert_eq!(entry.response, Some(true));
+    });
+}

crates/collab/src/tests/test_server.rs 🔗

@@ -16,6 +16,7 @@ use futures::{channel::oneshot, StreamExt as _};
 use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
 use language::LanguageRegistry;
 use node_runtime::FakeNodeRuntime;
+use notifications::NotificationStore;
 use parking_lot::Mutex;
 use project::{Project, WorktreeId};
 use rpc::RECEIVE_TIMEOUT;
@@ -46,6 +47,7 @@ pub struct TestClient {
     pub username: String,
     pub app_state: Arc<workspace::AppState>,
     channel_store: ModelHandle<ChannelStore>,
+    notification_store: ModelHandle<NotificationStore>,
     state: RefCell<TestClientState>,
 }
 
@@ -244,6 +246,7 @@ impl TestServer {
             app_state,
             username: name.to_string(),
             channel_store: cx.read(ChannelStore::global).clone(),
+            notification_store: cx.read(NotificationStore::global).clone(),
             state: Default::default(),
         };
         client.wait_for_current_user(cx).await;
@@ -449,6 +452,10 @@ impl TestClient {
         &self.channel_store
     }
 
+    pub fn notification_store(&self) -> &ModelHandle<NotificationStore> {
+        &self.notification_store
+    }
+
     pub fn user_store(&self) -> &ModelHandle<UserStore> {
         &self.app_state.user_store
     }

crates/collab_ui/src/notification_panel.rs 🔗

@@ -386,7 +386,8 @@ impl NotificationPanel {
     ) {
         match event {
             NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
-            NotificationEvent::NotificationRemoved { entry } => self.remove_toast(entry, cx),
+            NotificationEvent::NotificationRemoved { entry }
+            | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry, cx),
             NotificationEvent::NotificationsUpdated {
                 old_range,
                 new_count,
@@ -450,25 +451,9 @@ impl NotificationPanel {
         response: bool,
         cx: &mut ViewContext<Self>,
     ) {
-        match notification {
-            Notification::ContactRequest {
-                sender_id: actor_id,
-            } => {
-                self.user_store
-                    .update(cx, |store, cx| {
-                        store.respond_to_contact_request(actor_id, response, cx)
-                    })
-                    .detach();
-            }
-            Notification::ChannelInvitation { channel_id, .. } => {
-                self.channel_store
-                    .update(cx, |store, cx| {
-                        store.respond_to_channel_invite(channel_id, response, cx)
-                    })
-                    .detach();
-            }
-            _ => {}
-        }
+        self.notification_store.update(cx, |store, cx| {
+            store.respond_to_notification(notification, response, cx);
+        });
     }
 }
 

crates/notifications/src/notification_store.rs 🔗

@@ -36,6 +36,9 @@ pub enum NotificationEvent {
     NotificationRemoved {
         entry: NotificationEntry,
     },
+    NotificationRead {
+        entry: NotificationEntry,
+    },
 }
 
 #[derive(Debug, PartialEq, Eq, Clone)]
@@ -272,7 +275,13 @@ impl NotificationStore {
 
             if let Some(existing_notification) = cursor.item() {
                 if existing_notification.id == id {
-                    if new_notification.is_none() {
+                    if let Some(new_notification) = &new_notification {
+                        if new_notification.is_read {
+                            cx.emit(NotificationEvent::NotificationRead {
+                                entry: new_notification.clone(),
+                            });
+                        }
+                    } else {
                         cx.emit(NotificationEvent::NotificationRemoved {
                             entry: existing_notification.clone(),
                         });
@@ -303,6 +312,31 @@ impl NotificationStore {
             new_count,
         });
     }
+
+    pub fn respond_to_notification(
+        &mut self,
+        notification: Notification,
+        response: bool,
+        cx: &mut ModelContext<Self>,
+    ) {
+        match notification {
+            Notification::ContactRequest { sender_id } => {
+                self.user_store
+                    .update(cx, |store, cx| {
+                        store.respond_to_contact_request(sender_id, response, cx)
+                    })
+                    .detach();
+            }
+            Notification::ChannelInvitation { channel_id, .. } => {
+                self.channel_store
+                    .update(cx, |store, cx| {
+                        store.respond_to_channel_invite(channel_id, response, cx)
+                    })
+                    .detach();
+            }
+            _ => {}
+        }
+    }
 }
 
 impl Entity for NotificationStore {