ZIm/crates/collab/src/tests/channel_tests.rs
Nathan Sobo 6fca1d2b0b
Eliminate GPUI View, ViewContext, and WindowContext types (#22632)
There's still a bit more work to do on this, but this PR is compiling
(with warnings) after eliminating the key types. When the tasks below
are complete, this will be the new narrative for GPUI:

- `Entity<T>` - This replaces `View<T>`/`Model<T>`. It represents a unit
of state, and if `T` implements `Render`, then `Entity<T>` implements
`Element`.
- `&mut App` This replaces `AppContext` and represents the app.
- `&mut Context<T>` This replaces `ModelContext` and derefs to `App`. It
is provided by the framework when updating an entity.
- `&mut Window` Broken out of `&mut WindowContext` which no longer
exists. Every method that once took `&mut WindowContext` now takes `&mut
Window, &mut App` and every method that took `&mut ViewContext<T>` now
takes `&mut Window, &mut Context<T>`

Not pictured here are the two other failed attempts. It's been quite a
month!

Tasks:

- [x] Remove `View`, `ViewContext`, `WindowContext` and thread through
`Window`
- [x] [@cole-miller @mikayla-maki] Redraw window when entities change
- [x] [@cole-miller @mikayla-maki] Get examples and Zed running
- [x] [@cole-miller @mikayla-maki] Fix Zed rendering
- [x] [@mikayla-maki] Fix todo! macros and comments
- [x] Fix a bug where the editor would not be redrawn because of view
caching
- [x] remove publicness window.notify() and replace with
`AppContext::notify`
- [x] remove `observe_new_window_models`, replace with
`observe_new_models` with an optional window
- [x] Fix a bug where the project panel would not be redrawn because of
the wrong refresh() call being used
- [x] Fix the tests
- [x] Fix warnings by eliminating `Window` params or using `_`
- [x] Fix conflicts
- [x] Simplify generic code where possible
- [x] Rename types
- [ ] Update docs

### issues post merge

- [x] Issues switching between normal and insert mode
- [x] Assistant re-rendering failure
- [x] Vim test failures
- [x] Mac build issue



Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Joseph <joseph@zed.dev>
Co-authored-by: max <max@zed.dev>
Co-authored-by: Michael Sloan <michael@zed.dev>
Co-authored-by: Mikayla Maki <mikaylamaki@Mikaylas-MacBook-Pro.local>
Co-authored-by: Mikayla <mikayla.c.maki@gmail.com>
Co-authored-by: joão <joao@zed.dev>
2025-01-26 03:02:45 +00:00

1460 lines
40 KiB
Rust

use crate::{
db::{self, UserId},
rpc::RECONNECT_TIMEOUT,
tests::{room_participants, RoomParticipants, TestServer},
};
use call::ActiveCall;
use channel::{ChannelMembership, ChannelStore};
use client::{ChannelId, User};
use futures::future::try_join_all;
use gpui::{BackgroundExecutor, Entity, SharedString, TestAppContext};
use rpc::{
proto::{self, ChannelRole},
RECEIVE_TIMEOUT,
};
use std::sync::Arc;
#[gpui::test]
async fn test_core_channels(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let channel_a_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("channel-a", None, cx)
})
.await
.unwrap();
let channel_b_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("channel-b", Some(channel_a_id), cx)
})
.await
.unwrap();
executor.run_until_parked();
assert_channels(
client_a.channel_store(),
cx_a,
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".into(),
depth: 0,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".into(),
depth: 1,
},
],
);
cx_b.read(|cx| {
client_b.channel_store().read_with(cx, |channels, _| {
assert!(channels.ordered_channels().collect::<Vec<_>>().is_empty())
})
});
// Invite client B to channel A as client A.
client_a
.channel_store()
.update(cx_a, |store, cx| {
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
let invite = store.invite_member(
channel_a_id,
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
cx,
);
// Make sure we're synchronously storing the pending invite
assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
invite
})
.await
.unwrap();
// Client A sees that B has been invited.
executor.run_until_parked();
assert_channel_invitations(
client_b.channel_store(),
cx_b,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a".into(),
depth: 0,
}],
);
let members = client_a
.channel_store()
.update(cx_a, |store, cx| {
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
store.fuzzy_search_members(channel_a_id, "".to_string(), 10, cx)
})
.await
.unwrap();
assert_members_eq(
&members,
&[
(
client_a.user_id().unwrap(),
proto::ChannelRole::Admin,
proto::channel_member::Kind::Member,
),
(
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
proto::channel_member::Kind::Invitee,
),
],
);
// Client B accepts the invitation.
client_b
.channel_store()
.update(cx_b, |channels, cx| {
channels.respond_to_channel_invite(channel_a_id, true, cx)
})
.await
.unwrap();
executor.run_until_parked();
// Client B now sees that they are a member of channel A and its existing subchannels.
assert_channel_invitations(client_b.channel_store(), cx_b, &[]);
assert_channels(
client_b.channel_store(),
cx_b,
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".into(),
depth: 0,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".into(),
depth: 1,
},
],
);
let channel_c_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("channel-c", Some(channel_b_id), cx)
})
.await
.unwrap();
executor.run_until_parked();
assert_channels(
client_b.channel_store(),
cx_b,
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".into(),
depth: 0,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".into(),
depth: 1,
},
ExpectedChannel {
id: channel_c_id,
name: "channel-c".into(),
depth: 2,
},
],
);
// Update client B's membership to channel A to be an admin.
client_a
.channel_store()
.update(cx_a, |store, cx| {
store.set_member_role(
channel_a_id,
client_b.user_id().unwrap(),
proto::ChannelRole::Admin,
cx,
)
})
.await
.unwrap();
executor.run_until_parked();
// Observe that client B is now an admin of channel A, and that
// their admin privileges extend to subchannels of channel A.
assert_channel_invitations(client_b.channel_store(), cx_b, &[]);
assert_channels(
client_b.channel_store(),
cx_b,
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".into(),
depth: 0,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".into(),
depth: 1,
},
ExpectedChannel {
id: channel_c_id,
name: "channel-c".into(),
depth: 2,
},
],
);
// Client A deletes the channel, deletion also deletes subchannels.
client_a
.channel_store()
.update(cx_a, |channel_store, _| {
channel_store.remove_channel(channel_b_id)
})
.await
.unwrap();
executor.run_until_parked();
assert_channels(
client_a.channel_store(),
cx_a,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a".into(),
depth: 0,
}],
);
assert_channels(
client_b.channel_store(),
cx_b,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a".into(),
depth: 0,
}],
);
// Remove client B
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.remove_member(channel_a_id, client_b.user_id().unwrap(), cx)
})
.await
.unwrap();
executor.run_until_parked();
// Client A still has their channel
assert_channels(
client_a.channel_store(),
cx_a,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a".into(),
depth: 0,
}],
);
// Client B no longer has access to the channel
assert_channels(client_b.channel_store(), cx_b, &[]);
server.forbid_connections();
server.disconnect_client(client_a.peer_id().unwrap());
executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
server
.app_state
.db
.rename_channel(
db::ChannelId::from_proto(channel_a_id.0),
UserId::from_proto(client_a.id()),
"channel-a-renamed",
)
.await
.unwrap();
server.allow_connections();
executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
assert_channels(
client_a.channel_store(),
cx_a,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a-renamed".into(),
depth: 0,
}],
);
}
#[track_caller]
fn assert_participants_eq(participants: &[Arc<User>], expected_partitipants: &[u64]) {
assert_eq!(
participants.iter().map(|p| p.id).collect::<Vec<_>>(),
expected_partitipants
);
}
#[track_caller]
fn assert_members_eq(
members: &[ChannelMembership],
expected_members: &[(u64, proto::ChannelRole, proto::channel_member::Kind)],
) {
assert_eq!(
members
.iter()
.map(|member| (member.user.id, member.role, member.kind))
.collect::<Vec<_>>(),
expected_members
);
}
#[gpui::test]
async fn test_joining_channel_ancestor_member(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let parent_id = server
.make_channel("parent", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
.await;
let sub_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("sub_channel", Some(parent_id), cx)
})
.await
.unwrap();
let active_call_b = cx_b.read(ActiveCall::global);
assert!(active_call_b
.update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx))
.await
.is_ok());
}
#[gpui::test]
async fn test_channel_room(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
let zed_id = server
.make_channel(
"zed",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
active_call_a
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
// Give everyone a chance to observe user A joining
executor.run_until_parked();
let room_a =
cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone()));
cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected())));
cx_a.read(|cx| {
client_a.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_a.user_id().unwrap()],
);
})
});
assert_channels(
client_b.channel_store(),
cx_b,
&[ExpectedChannel {
id: zed_id,
name: "zed".into(),
depth: 0,
}],
);
cx_b.read(|cx| {
client_b.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_a.user_id().unwrap()],
);
})
});
cx_c.read(|cx| {
client_c.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_a.user_id().unwrap()],
);
})
});
active_call_b
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
executor.run_until_parked();
cx_a.read(|cx| {
client_a.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
);
})
});
cx_b.read(|cx| {
client_b.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
);
})
});
cx_c.read(|cx| {
client_c.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
);
})
});
let room_a =
cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone()));
cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected())));
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: vec!["user_b".to_string()],
pending: vec![]
}
);
let room_b =
cx_b.read(|cx| active_call_b.read_with(cx, |call, _| call.room().unwrap().clone()));
cx_b.read(|cx| room_b.read_with(cx, |room, _| assert!(room.is_connected())));
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: vec!["user_a".to_string()],
pending: vec![]
}
);
// Make sure that leaving and rejoining works
active_call_a
.update(cx_a, |active_call, cx| active_call.hang_up(cx))
.await
.unwrap();
executor.run_until_parked();
cx_a.read(|cx| {
client_a.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_b.user_id().unwrap()],
);
})
});
cx_b.read(|cx| {
client_b.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_b.user_id().unwrap()],
);
})
});
cx_c.read(|cx| {
client_c.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_b.user_id().unwrap()],
);
})
});
active_call_b
.update(cx_b, |active_call, cx| active_call.hang_up(cx))
.await
.unwrap();
executor.run_until_parked();
cx_a.read(|cx| {
client_a.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(channels.channel_participants(zed_id), &[]);
})
});
cx_b.read(|cx| {
client_b.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(channels.channel_participants(zed_id), &[]);
})
});
cx_c.read(|cx| {
client_c.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(channels.channel_participants(zed_id), &[]);
})
});
active_call_a
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
active_call_b
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
executor.run_until_parked();
let room_a =
cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone()));
cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected())));
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: vec!["user_b".to_string()],
pending: vec![]
}
);
let room_b =
cx_b.read(|cx| active_call_b.read_with(cx, |call, _| call.room().unwrap().clone()));
cx_b.read(|cx| room_b.read_with(cx, |room, _| assert!(room.is_connected())));
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: vec!["user_a".to_string()],
pending: vec![]
}
);
}
#[gpui::test]
async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppContext) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let zed_id = server
.make_channel("zed", None, (&client_a, cx_a), &mut [])
.await;
let rust_id = server
.make_channel("rust", None, (&client_a, cx_a), &mut [])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
active_call_a
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
// Give everything a chance to observe user A joining
executor.run_until_parked();
cx_a.read(|cx| {
client_a.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(
channels.channel_participants(zed_id),
&[client_a.user_id().unwrap()],
);
assert_participants_eq(channels.channel_participants(rust_id), &[]);
})
});
active_call_a
.update(cx_a, |active_call, cx| {
active_call.join_channel(rust_id, cx)
})
.await
.unwrap();
executor.run_until_parked();
cx_a.read(|cx| {
client_a.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(channels.channel_participants(zed_id), &[]);
assert_participants_eq(
channels.channel_participants(rust_id),
&[client_a.user_id().unwrap()],
);
})
});
}
#[gpui::test]
async fn test_permissions_update_while_invited(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let rust_id = server
.make_channel("rust", None, (&client_a, cx_a), &mut [])
.await;
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.invite_member(
rust_id,
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
cx,
)
})
.await
.unwrap();
executor.run_until_parked();
assert_channel_invitations(
client_b.channel_store(),
cx_b,
&[ExpectedChannel {
depth: 0,
id: rust_id,
name: "rust".into(),
}],
);
assert_channels(client_b.channel_store(), cx_b, &[]);
// Update B's invite before they've accepted it
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.set_member_role(
rust_id,
client_b.user_id().unwrap(),
proto::ChannelRole::Admin,
cx,
)
})
.await
.unwrap();
executor.run_until_parked();
assert_channel_invitations(
client_b.channel_store(),
cx_b,
&[ExpectedChannel {
depth: 0,
id: rust_id,
name: "rust".into(),
}],
);
assert_channels(client_b.channel_store(), cx_b, &[]);
}
#[gpui::test]
async fn test_channel_rename(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let rust_id = server
.make_channel("rust", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
.await;
// Rename the channel
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.rename(rust_id, "#rust-archive", cx)
})
.await
.unwrap();
executor.run_until_parked();
// Client A sees the channel with its new name.
assert_channels(
client_a.channel_store(),
cx_a,
&[ExpectedChannel {
depth: 0,
id: rust_id,
name: "rust-archive".into(),
}],
);
// Client B sees the channel with its new name.
assert_channels(
client_b.channel_store(),
cx_b,
&[ExpectedChannel {
depth: 0,
id: rust_id,
name: "rust-archive".into(),
}],
);
}
#[gpui::test]
async fn test_call_from_channel(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
server
.make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let channel_id = server
.make_channel(
"x",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
active_call_a
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
.await
.unwrap();
// Client A calls client B while in the channel.
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_b.user_id().unwrap(), None, cx)
})
.await
.unwrap();
// Client B accepts the call.
executor.run_until_parked();
active_call_b
.update(cx_b, |call, cx| call.accept_incoming(cx))
.await
.unwrap();
// Client B sees that they are now in the channel
executor.run_until_parked();
cx_b.read(|cx| {
active_call_b.read_with(cx, |call, cx| {
assert_eq!(call.channel_id(cx), Some(channel_id));
})
});
cx_b.read(|cx| {
client_b.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(
channels.channel_participants(channel_id),
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
);
})
});
// Clients A and C also see that client B is in the channel.
cx_a.read(|cx| {
client_a.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(
channels.channel_participants(channel_id),
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
);
})
});
cx_c.read(|cx| {
client_c.channel_store().read_with(cx, |channels, _| {
assert_participants_eq(
channels.channel_participants(channel_id),
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
);
})
});
}
#[gpui::test]
async fn test_lost_channel_creation(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let channel_id = server
.make_channel("x", None, (&client_a, cx_a), &mut [])
.await;
// Invite a member
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.invite_member(
channel_id,
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
cx,
)
})
.await
.unwrap();
executor.run_until_parked();
// Sanity check, B has the invitation
assert_channel_invitations(
client_b.channel_store(),
cx_b,
&[ExpectedChannel {
depth: 0,
id: channel_id,
name: "x".into(),
}],
);
// A creates a subchannel while the invite is still pending.
let subchannel_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("subchannel", Some(channel_id), cx)
})
.await
.unwrap();
executor.run_until_parked();
// Make sure A sees their new channel
assert_channels(
client_a.channel_store(),
cx_a,
&[
ExpectedChannel {
depth: 0,
id: channel_id,
name: "x".into(),
},
ExpectedChannel {
depth: 1,
id: subchannel_id,
name: "subchannel".into(),
},
],
);
// Client B accepts the invite
client_b
.channel_store()
.update(cx_b, |channel_store, cx| {
channel_store.respond_to_channel_invite(channel_id, true, cx)
})
.await
.unwrap();
executor.run_until_parked();
// Client B should now see the channel
assert_channels(
client_b.channel_store(),
cx_b,
&[
ExpectedChannel {
depth: 0,
id: channel_id,
name: "x".into(),
},
ExpectedChannel {
depth: 1,
id: subchannel_id,
name: "subchannel".into(),
},
],
);
}
#[gpui::test]
async fn test_channel_link_notifications(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
let user_b = client_b.user_id().unwrap();
let user_c = client_c.user_id().unwrap();
let channels = server
.make_channel_tree(&[("zed", None)], (&client_a, cx_a))
.await;
let zed_channel = channels[0];
try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| {
[
channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx),
channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Member, cx),
channel_store.invite_member(zed_channel, user_c, proto::ChannelRole::Guest, cx),
]
}))
.await
.unwrap();
executor.run_until_parked();
client_b
.channel_store()
.update(cx_b, |channel_store, cx| {
channel_store.respond_to_channel_invite(zed_channel, true, cx)
})
.await
.unwrap();
client_c
.channel_store()
.update(cx_c, |channel_store, cx| {
channel_store.respond_to_channel_invite(zed_channel, true, cx)
})
.await
.unwrap();
executor.run_until_parked();
// we have an admin (a), member (b) and guest (c) all part of the zed channel.
// create a new private channel, make it public, and move it under the previous one, and verify it shows for b and not c
let active_channel = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("active", Some(zed_channel), cx)
})
.await
.unwrap();
executor.run_until_parked();
// the new channel shows for b and not c
assert_channels_list_shape(
client_a.channel_store(),
cx_a,
&[(zed_channel, 0), (active_channel, 1)],
);
assert_channels_list_shape(
client_b.channel_store(),
cx_b,
&[(zed_channel, 0), (active_channel, 1)],
);
assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]);
let vim_channel = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("vim", Some(zed_channel), cx)
})
.await
.unwrap();
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx)
})
.await
.unwrap();
executor.run_until_parked();
// the new channel shows for b and c
assert_channels_list_shape(
client_a.channel_store(),
cx_a,
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 1)],
);
assert_channels_list_shape(
client_b.channel_store(),
cx_b,
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 1)],
);
assert_channels_list_shape(
client_c.channel_store(),
cx_c,
&[(zed_channel, 0), (vim_channel, 1)],
);
let helix_channel = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("helix", Some(zed_channel), cx)
})
.await
.unwrap();
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.move_channel(helix_channel, vim_channel, cx)
})
.await
.unwrap();
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.set_channel_visibility(
helix_channel,
proto::ChannelVisibility::Public,
cx,
)
})
.await
.unwrap();
cx_a.run_until_parked();
// the new channel shows for b and c
assert_channels_list_shape(
client_b.channel_store(),
cx_b,
&[
(zed_channel, 0),
(active_channel, 1),
(vim_channel, 1),
(helix_channel, 2),
],
);
assert_channels_list_shape(
client_c.channel_store(),
cx_c,
&[(zed_channel, 0), (vim_channel, 1), (helix_channel, 2)],
);
}
#[gpui::test]
async fn test_channel_membership_notifications(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_c").await;
let user_b = client_b.user_id().unwrap();
let channels = server
.make_channel_tree(
&[("zed", None), ("vim", Some("zed")), ("opensource", None)],
(&client_a, cx_a),
)
.await;
let zed_channel = channels[0];
let vim_channel = channels[1];
let opensource_channel = channels[2];
try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| {
[
channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx),
channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx),
channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Admin, cx),
channel_store.invite_member(opensource_channel, user_b, proto::ChannelRole::Member, cx),
]
}))
.await
.unwrap();
executor.run_until_parked();
client_b
.channel_store()
.update(cx_b, |channel_store, cx| {
channel_store.respond_to_channel_invite(zed_channel, true, cx)
})
.await
.unwrap();
executor.run_until_parked();
// we have an admin (a), and a guest (b) with access to all of zed, and membership in vim.
assert_channels(
client_b.channel_store(),
cx_b,
&[
ExpectedChannel {
depth: 0,
id: zed_channel,
name: "zed".into(),
},
ExpectedChannel {
depth: 1,
id: vim_channel,
name: "vim".into(),
},
],
);
client_b.channel_store().update(cx_b, |channel_store, _| {
channel_store.is_channel_admin(zed_channel)
});
client_b
.channel_store()
.update(cx_b, |channel_store, cx| {
channel_store.respond_to_channel_invite(opensource_channel, true, cx)
})
.await
.unwrap();
cx_a.run_until_parked();
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.set_member_role(opensource_channel, user_b, ChannelRole::Admin, cx)
})
.await
.unwrap();
cx_a.run_until_parked();
client_b.channel_store().update(cx_b, |channel_store, _| {
channel_store.is_channel_admin(opensource_channel)
});
}
#[gpui::test]
async fn test_guest_access(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let channels = server
.make_channel_tree(
&[("channel-a", None), ("channel-b", Some("channel-a"))],
(&client_a, cx_a),
)
.await;
let channel_a = channels[0];
let channel_b = channels[1];
let active_call_b = cx_b.read(ActiveCall::global);
// Non-members should not be allowed to join
assert!(active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_a, cx))
.await
.is_err());
// Make channels A and B public
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.set_channel_visibility(channel_a, proto::ChannelVisibility::Public, cx)
})
.await
.unwrap();
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.set_channel_visibility(channel_b, proto::ChannelVisibility::Public, cx)
})
.await
.unwrap();
// Client B joins channel A as a guest
active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_a, cx))
.await
.unwrap();
executor.run_until_parked();
assert_channels_list_shape(
client_a.channel_store(),
cx_a,
&[(channel_a, 0), (channel_b, 1)],
);
assert_channels_list_shape(
client_b.channel_store(),
cx_b,
&[(channel_a, 0), (channel_b, 1)],
);
client_a.channel_store().update(cx_a, |channel_store, _| {
let participants = channel_store.channel_participants(channel_a);
assert_eq!(participants.len(), 1);
assert_eq!(participants[0].id, client_b.user_id().unwrap());
});
}
#[gpui::test]
async fn test_invite_access(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let channels = server
.make_channel_tree(
&[("channel-a", None), ("channel-b", Some("channel-a"))],
(&client_a, cx_a),
)
.await;
let channel_a_id = channels[0];
let channel_b_id = channels[0];
let active_call_b = cx_b.read(ActiveCall::global);
// should not be allowed to join
assert!(active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
.await
.is_err());
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.invite_member(
channel_a_id,
client_b.user_id().unwrap(),
ChannelRole::Member,
cx,
)
})
.await
.unwrap();
active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
.await
.unwrap();
executor.run_until_parked();
client_b.channel_store().update(cx_b, |channel_store, _| {
assert!(channel_store.channel_for_id(channel_b_id).is_some());
assert!(channel_store.channel_for_id(channel_a_id).is_some());
});
client_a.channel_store().update(cx_a, |channel_store, _| {
let participants = channel_store.channel_participants(channel_b_id);
assert_eq!(participants.len(), 1);
assert_eq!(participants[0].id, client_b.user_id().unwrap());
})
}
#[gpui::test]
async fn test_leave_channel(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let (_server, _client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
client_b
.channel_store()
.update(cx_b, |channel_store, cx| {
channel_store.remove_member(channel_id, client_b.user_id().unwrap(), cx)
})
.await
.unwrap();
cx_a.run_until_parked();
assert_eq!(
client_b
.channel_store()
.read_with(cx_b, |store, _| store.channels().count()),
0
);
}
#[gpui::test]
async fn test_channel_moving(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
_cx_b: &mut TestAppContext,
_cx_c: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let channels = server
.make_channel_tree(
&[
("channel-a", None),
("channel-b", Some("channel-a")),
("channel-c", Some("channel-b")),
("channel-d", Some("channel-c")),
],
(&client_a, cx_a),
)
.await;
let channel_a_id = channels[0];
let channel_b_id = channels[1];
let channel_c_id = channels[2];
let channel_d_id = channels[3];
// Current shape:
// a - b - c - d
assert_channels_list_shape(
client_a.channel_store(),
cx_a,
&[
(channel_a_id, 0),
(channel_b_id, 1),
(channel_c_id, 2),
(channel_d_id, 3),
],
);
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.move_channel(channel_d_id, channel_b_id, cx)
})
.await
.unwrap();
// Current shape:
// /- d
// a - b -- c
assert_channels_list_shape(
client_a.channel_store(),
cx_a,
&[
(channel_a_id, 0),
(channel_b_id, 1),
(channel_c_id, 2),
(channel_d_id, 2),
],
);
}
#[derive(Debug, PartialEq)]
struct ExpectedChannel {
depth: usize,
id: ChannelId,
name: SharedString,
}
#[track_caller]
fn assert_channel_invitations(
channel_store: &Entity<ChannelStore>,
cx: &TestAppContext,
expected_channels: &[ExpectedChannel],
) {
let actual = cx.read(|cx| {
channel_store.read_with(cx, |store, _| {
store
.channel_invitations()
.iter()
.map(|channel| ExpectedChannel {
depth: 0,
name: channel.name.clone(),
id: channel.id,
})
.collect::<Vec<_>>()
})
});
assert_eq!(actual, expected_channels);
}
#[track_caller]
fn assert_channels(
channel_store: &Entity<ChannelStore>,
cx: &TestAppContext,
expected_channels: &[ExpectedChannel],
) {
let actual = cx.read(|cx| {
channel_store.read_with(cx, |store, _| {
store
.ordered_channels()
.map(|(depth, channel)| ExpectedChannel {
depth,
name: channel.name.clone(),
id: channel.id,
})
.collect::<Vec<_>>()
})
});
pretty_assertions::assert_eq!(actual, expected_channels);
}
#[track_caller]
fn assert_channels_list_shape(
channel_store: &Entity<ChannelStore>,
cx: &TestAppContext,
expected_channels: &[(ChannelId, usize)],
) {
let actual = cx.read(|cx| {
channel_store.read_with(cx, |store, _| {
store
.ordered_channels()
.map(|(depth, channel)| (channel.id, depth))
.collect::<Vec<_>>()
})
});
pretty_assertions::assert_eq!(actual, expected_channels);
}