1use crate::{
2 rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
3 tests::{test_server::open_channel_notes, TestServer},
4};
5use call::ActiveCall;
6use channel::ACKNOWLEDGE_DEBOUNCE_INTERVAL;
7use client::{Collaborator, ParticipantIndex, UserId};
8use collab_ui::channel_view::ChannelView;
9use collections::HashMap;
10use editor::{Anchor, Editor, ToOffset};
11use futures::future;
12use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext};
13use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
14use serde_json::json;
15use std::ops::Range;
16
17#[gpui::test]
18async fn test_core_channel_buffers(
19 executor: BackgroundExecutor,
20 cx_a: &mut TestAppContext,
21 cx_b: &mut TestAppContext,
22) {
23 let mut server = TestServer::start(executor.clone()).await;
24 let client_a = server.create_client(cx_a, "user_a").await;
25 let client_b = server.create_client(cx_b, "user_b").await;
26
27 let channel_id = server
28 .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
29 .await;
30
31 // Client A joins the channel buffer
32 let channel_buffer_a = client_a
33 .channel_store()
34 .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
35 .await
36 .unwrap();
37
38 // Client A edits the buffer
39 let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer());
40 buffer_a.update(cx_a, |buffer, cx| {
41 buffer.edit([(0..0, "hello world")], None, cx)
42 });
43 buffer_a.update(cx_a, |buffer, cx| {
44 buffer.edit([(5..5, ", cruel")], None, cx)
45 });
46 buffer_a.update(cx_a, |buffer, cx| {
47 buffer.edit([(0..5, "goodbye")], None, cx)
48 });
49 buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx));
50 assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world");
51 executor.run_until_parked();
52
53 // Client B joins the channel buffer
54 let channel_buffer_b = client_b
55 .channel_store()
56 .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
57 .await
58 .unwrap();
59 channel_buffer_b.read_with(cx_b, |buffer, _| {
60 assert_collaborators(
61 buffer.collaborators(),
62 &[client_a.user_id(), client_b.user_id()],
63 );
64 });
65
66 // Client B sees the correct text, and then edits it
67 let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer());
68 assert_eq!(
69 buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()),
70 buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id())
71 );
72 assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world");
73 buffer_b.update(cx_b, |buffer, cx| {
74 buffer.edit([(7..12, "beautiful")], None, cx)
75 });
76
77 // Both A and B see the new edit
78 executor.run_until_parked();
79 assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world");
80 assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world");
81
82 // Client A closes the channel buffer.
83 cx_a.update(|_| drop(channel_buffer_a));
84 executor.run_until_parked();
85
86 // Client B sees that client A is gone from the channel buffer.
87 channel_buffer_b.read_with(cx_b, |buffer, _| {
88 assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]);
89 });
90
91 // Client A rejoins the channel buffer
92 let _channel_buffer_a = client_a
93 .channel_store()
94 .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
95 .await
96 .unwrap();
97 executor.run_until_parked();
98
99 // Sanity test, make sure we saw A rejoining
100 channel_buffer_b.read_with(cx_b, |buffer, _| {
101 assert_collaborators(
102 &buffer.collaborators(),
103 &[client_a.user_id(), client_b.user_id()],
104 );
105 });
106
107 // Client A loses connection.
108 server.forbid_connections();
109 server.disconnect_client(client_a.peer_id().unwrap());
110 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
111
112 // Client B observes A disconnect
113 channel_buffer_b.read_with(cx_b, |buffer, _| {
114 assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]);
115 });
116
117 // TODO:
118 // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects
119 // - Test interaction with channel deletion while buffer is open
120}
121
122#[gpui::test]
123async fn test_channel_notes_participant_indices(
124 executor: BackgroundExecutor,
125 cx_a: &mut TestAppContext,
126 cx_b: &mut TestAppContext,
127 cx_c: &mut TestAppContext,
128) {
129 let mut server = TestServer::start(executor.clone()).await;
130 let client_a = server.create_client(cx_a, "user_a").await;
131 let client_b = server.create_client(cx_b, "user_b").await;
132 let client_c = server.create_client(cx_c, "user_c").await;
133
134 let active_call_a = cx_a.read(ActiveCall::global);
135 let active_call_b = cx_b.read(ActiveCall::global);
136
137 cx_a.update(editor::init);
138 cx_b.update(editor::init);
139 cx_c.update(editor::init);
140
141 let channel_id = server
142 .make_channel(
143 "the-channel",
144 None,
145 (&client_a, cx_a),
146 &mut [(&client_b, cx_b), (&client_c, cx_c)],
147 )
148 .await;
149
150 client_a
151 .fs()
152 .insert_tree("/root", json!({"file.txt": "123"}))
153 .await;
154 let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await;
155 let project_b = client_b.build_empty_local_project(cx_b);
156 let project_c = client_c.build_empty_local_project(cx_c);
157
158 let (workspace_a, mut cx_a) = client_a.build_workspace(&project_a, cx_a);
159 let (workspace_b, mut cx_b) = client_b.build_workspace(&project_b, cx_b);
160 let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c);
161
162 // Clients A, B, and C open the channel notes
163 let channel_view_a = cx_a
164 .update(|cx| ChannelView::open(channel_id, None, workspace_a.clone(), cx))
165 .await
166 .unwrap();
167 let channel_view_b = cx_b
168 .update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), cx))
169 .await
170 .unwrap();
171 let channel_view_c = cx_c
172 .update(|cx| ChannelView::open(channel_id, None, workspace_c.clone(), cx))
173 .await
174 .unwrap();
175
176 // Clients A, B, and C all insert and select some text
177 channel_view_a.update(cx_a, |notes, cx| {
178 notes.editor.update(cx, |editor, cx| {
179 editor.insert("a", cx);
180 editor.change_selections(None, cx, |selections| {
181 selections.select_ranges(vec![0..1]);
182 });
183 });
184 });
185 executor.run_until_parked();
186 channel_view_b.update(cx_b, |notes, cx| {
187 notes.editor.update(cx, |editor, cx| {
188 editor.move_down(&Default::default(), cx);
189 editor.insert("b", cx);
190 editor.change_selections(None, cx, |selections| {
191 selections.select_ranges(vec![1..2]);
192 });
193 });
194 });
195 executor.run_until_parked();
196 channel_view_c.update(cx_c, |notes, cx| {
197 notes.editor.update(cx, |editor, cx| {
198 editor.move_down(&Default::default(), cx);
199 editor.insert("c", cx);
200 editor.change_selections(None, cx, |selections| {
201 selections.select_ranges(vec![2..3]);
202 });
203 });
204 });
205
206 // Client A sees clients B and C without assigned colors, because they aren't
207 // in a call together.
208 executor.run_until_parked();
209 channel_view_a.update(cx_a, |notes, cx| {
210 notes.editor.update(cx, |editor, cx| {
211 assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
212 });
213 });
214
215 // Clients A and B join the same call.
216 for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] {
217 call.update(*cx, |call, cx| call.join_channel(channel_id, cx))
218 .await
219 .unwrap();
220 }
221
222 // Clients A and B see each other with two different assigned colors. Client C
223 // still doesn't have a color.
224 executor.run_until_parked();
225 channel_view_a.update(cx_a, |notes, cx| {
226 notes.editor.update(cx, |editor, cx| {
227 assert_remote_selections(
228 editor,
229 &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
230 cx,
231 );
232 });
233 });
234 channel_view_b.update(cx_b, |notes, cx| {
235 notes.editor.update(cx, |editor, cx| {
236 assert_remote_selections(
237 editor,
238 &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
239 cx,
240 );
241 });
242 });
243
244 // Client A shares a project, and client B joins.
245 let project_id = active_call_a
246 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
247 .await
248 .unwrap();
249 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
250 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
251
252 // Clients A and B open the same file.
253 let editor_a = workspace_a
254 .update(cx_a, |workspace, cx| {
255 workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
256 })
257 .await
258 .unwrap()
259 .downcast::<Editor>()
260 .unwrap();
261 let editor_b = workspace_b
262 .update(cx_b, |workspace, cx| {
263 workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
264 })
265 .await
266 .unwrap()
267 .downcast::<Editor>()
268 .unwrap();
269
270 editor_a.update(cx_a, |editor, cx| {
271 editor.change_selections(None, cx, |selections| {
272 selections.select_ranges(vec![0..1]);
273 });
274 });
275 editor_b.update(cx_b, |editor, cx| {
276 editor.change_selections(None, cx, |selections| {
277 selections.select_ranges(vec![2..3]);
278 });
279 });
280 executor.run_until_parked();
281
282 // Clients A and B see each other with the same colors as in the channel notes.
283 editor_a.update(cx_a, |editor, cx| {
284 assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx);
285 });
286 editor_b.update(cx_b, |editor, cx| {
287 assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx);
288 });
289}
290
291#[track_caller]
292fn assert_remote_selections(
293 editor: &mut Editor,
294 expected_selections: &[(Option<ParticipantIndex>, Range<usize>)],
295 cx: &mut ViewContext<Editor>,
296) {
297 let snapshot = editor.snapshot(cx);
298 let range = Anchor::min()..Anchor::max();
299 let remote_selections = snapshot
300 .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx)
301 .map(|s| {
302 let start = s.selection.start.to_offset(&snapshot.buffer_snapshot);
303 let end = s.selection.end.to_offset(&snapshot.buffer_snapshot);
304 (s.participant_index, start..end)
305 })
306 .collect::<Vec<_>>();
307 assert_eq!(
308 remote_selections, expected_selections,
309 "incorrect remote selections"
310 );
311}
312
313#[gpui::test]
314async fn test_multiple_handles_to_channel_buffer(
315 deterministic: BackgroundExecutor,
316 cx_a: &mut TestAppContext,
317) {
318 let mut server = TestServer::start(deterministic.clone()).await;
319 let client_a = server.create_client(cx_a, "user_a").await;
320
321 let channel_id = server
322 .make_channel("the-channel", None, (&client_a, cx_a), &mut [])
323 .await;
324
325 let channel_buffer_1 = client_a
326 .channel_store()
327 .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
328 let channel_buffer_2 = client_a
329 .channel_store()
330 .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
331 let channel_buffer_3 = client_a
332 .channel_store()
333 .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
334
335 // All concurrent tasks for opening a channel buffer return the same model handle.
336 let (channel_buffer, channel_buffer_2, channel_buffer_3) =
337 future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3)
338 .await
339 .unwrap();
340 let channel_buffer_model_id = channel_buffer.entity_id();
341 assert_eq!(channel_buffer, channel_buffer_2);
342 assert_eq!(channel_buffer, channel_buffer_3);
343
344 channel_buffer.update(cx_a, |buffer, cx| {
345 buffer.buffer().update(cx, |buffer, cx| {
346 buffer.edit([(0..0, "hello")], None, cx);
347 })
348 });
349 deterministic.run_until_parked();
350
351 cx_a.update(|_| {
352 drop(channel_buffer);
353 drop(channel_buffer_2);
354 drop(channel_buffer_3);
355 });
356 deterministic.run_until_parked();
357
358 // The channel buffer can be reopened after dropping it.
359 let channel_buffer = client_a
360 .channel_store()
361 .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
362 .await
363 .unwrap();
364 assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id);
365 channel_buffer.update(cx_a, |buffer, cx| {
366 buffer.buffer().update(cx, |buffer, _| {
367 assert_eq!(buffer.text(), "hello");
368 })
369 });
370}
371
372#[gpui::test]
373async fn test_channel_buffer_disconnect(
374 deterministic: BackgroundExecutor,
375 cx_a: &mut TestAppContext,
376 cx_b: &mut TestAppContext,
377) {
378 let mut server = TestServer::start(deterministic.clone()).await;
379 let client_a = server.create_client(cx_a, "user_a").await;
380 let client_b = server.create_client(cx_b, "user_b").await;
381
382 let channel_id = server
383 .make_channel(
384 "the-channel",
385 None,
386 (&client_a, cx_a),
387 &mut [(&client_b, cx_b)],
388 )
389 .await;
390
391 let channel_buffer_a = client_a
392 .channel_store()
393 .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
394 .await
395 .unwrap();
396
397 let channel_buffer_b = client_b
398 .channel_store()
399 .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
400 .await
401 .unwrap();
402
403 server.forbid_connections();
404 server.disconnect_client(client_a.peer_id().unwrap());
405 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
406
407 channel_buffer_a.update(cx_a, |buffer, cx| {
408 assert_eq!(buffer.channel(cx).unwrap().name, "the-channel");
409 assert!(!buffer.is_connected());
410 });
411
412 deterministic.run_until_parked();
413
414 server.allow_connections();
415 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
416
417 deterministic.run_until_parked();
418
419 client_a
420 .channel_store()
421 .update(cx_a, |channel_store, _| {
422 channel_store.remove_channel(channel_id)
423 })
424 .await
425 .unwrap();
426 deterministic.run_until_parked();
427
428 // Channel buffer observed the deletion
429 channel_buffer_b.update(cx_b, |buffer, cx| {
430 assert!(buffer.channel(cx).is_none());
431 assert!(!buffer.is_connected());
432 });
433}
434
435#[gpui::test]
436async fn test_rejoin_channel_buffer(
437 deterministic: BackgroundExecutor,
438 cx_a: &mut TestAppContext,
439 cx_b: &mut TestAppContext,
440) {
441 let mut server = TestServer::start(deterministic.clone()).await;
442 let client_a = server.create_client(cx_a, "user_a").await;
443 let client_b = server.create_client(cx_b, "user_b").await;
444
445 let channel_id = server
446 .make_channel(
447 "the-channel",
448 None,
449 (&client_a, cx_a),
450 &mut [(&client_b, cx_b)],
451 )
452 .await;
453
454 let channel_buffer_a = client_a
455 .channel_store()
456 .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
457 .await
458 .unwrap();
459 let channel_buffer_b = client_b
460 .channel_store()
461 .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
462 .await
463 .unwrap();
464
465 channel_buffer_a.update(cx_a, |buffer, cx| {
466 buffer.buffer().update(cx, |buffer, cx| {
467 buffer.edit([(0..0, "1")], None, cx);
468 })
469 });
470 deterministic.run_until_parked();
471
472 // Client A disconnects.
473 server.forbid_connections();
474 server.disconnect_client(client_a.peer_id().unwrap());
475
476 // Both clients make an edit.
477 channel_buffer_a.update(cx_a, |buffer, cx| {
478 buffer.buffer().update(cx, |buffer, cx| {
479 buffer.edit([(1..1, "2")], None, cx);
480 })
481 });
482 channel_buffer_b.update(cx_b, |buffer, cx| {
483 buffer.buffer().update(cx, |buffer, cx| {
484 buffer.edit([(0..0, "0")], None, cx);
485 })
486 });
487
488 // Both clients see their own edit.
489 deterministic.run_until_parked();
490 channel_buffer_a.read_with(cx_a, |buffer, cx| {
491 assert_eq!(buffer.buffer().read(cx).text(), "12");
492 });
493 channel_buffer_b.read_with(cx_b, |buffer, cx| {
494 assert_eq!(buffer.buffer().read(cx).text(), "01");
495 });
496
497 // Client A reconnects. Both clients see each other's edits, and see
498 // the same collaborators.
499 server.allow_connections();
500 deterministic.advance_clock(RECEIVE_TIMEOUT);
501 channel_buffer_a.read_with(cx_a, |buffer, cx| {
502 assert_eq!(buffer.buffer().read(cx).text(), "012");
503 });
504 channel_buffer_b.read_with(cx_b, |buffer, cx| {
505 assert_eq!(buffer.buffer().read(cx).text(), "012");
506 });
507
508 channel_buffer_a.read_with(cx_a, |buffer_a, _| {
509 channel_buffer_b.read_with(cx_b, |buffer_b, _| {
510 assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
511 });
512 });
513}
514
515#[gpui::test]
516async fn test_channel_buffers_and_server_restarts(
517 deterministic: BackgroundExecutor,
518 cx_a: &mut TestAppContext,
519 cx_b: &mut TestAppContext,
520 cx_c: &mut TestAppContext,
521) {
522 let mut server = TestServer::start(deterministic.clone()).await;
523 let client_a = server.create_client(cx_a, "user_a").await;
524 let client_b = server.create_client(cx_b, "user_b").await;
525 let client_c = server.create_client(cx_c, "user_c").await;
526
527 let channel_id = server
528 .make_channel(
529 "the-channel",
530 None,
531 (&client_a, cx_a),
532 &mut [(&client_b, cx_b), (&client_c, cx_c)],
533 )
534 .await;
535
536 let channel_buffer_a = client_a
537 .channel_store()
538 .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
539 .await
540 .unwrap();
541 let channel_buffer_b = client_b
542 .channel_store()
543 .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
544 .await
545 .unwrap();
546 let _channel_buffer_c = client_c
547 .channel_store()
548 .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx))
549 .await
550 .unwrap();
551
552 channel_buffer_a.update(cx_a, |buffer, cx| {
553 buffer.buffer().update(cx, |buffer, cx| {
554 buffer.edit([(0..0, "1")], None, cx);
555 })
556 });
557 deterministic.run_until_parked();
558
559 // Client C can't reconnect.
560 client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
561
562 // Server stops.
563 server.reset().await;
564 deterministic.advance_clock(RECEIVE_TIMEOUT);
565
566 // While the server is down, both clients make an edit.
567 channel_buffer_a.update(cx_a, |buffer, cx| {
568 buffer.buffer().update(cx, |buffer, cx| {
569 buffer.edit([(1..1, "2")], None, cx);
570 })
571 });
572 channel_buffer_b.update(cx_b, |buffer, cx| {
573 buffer.buffer().update(cx, |buffer, cx| {
574 buffer.edit([(0..0, "0")], None, cx);
575 })
576 });
577
578 // Server restarts.
579 server.start().await.unwrap();
580 deterministic.advance_clock(CLEANUP_TIMEOUT);
581
582 // Clients reconnects. Clients A and B see each other's edits, and see
583 // that client C has disconnected.
584 channel_buffer_a.read_with(cx_a, |buffer, cx| {
585 assert_eq!(buffer.buffer().read(cx).text(), "012");
586 });
587 channel_buffer_b.read_with(cx_b, |buffer, cx| {
588 assert_eq!(buffer.buffer().read(cx).text(), "012");
589 });
590
591 channel_buffer_a.read_with(cx_a, |buffer_a, _| {
592 channel_buffer_b.read_with(cx_b, |buffer_b, _| {
593 assert_collaborators(
594 buffer_a.collaborators(),
595 &[client_a.user_id(), client_b.user_id()],
596 );
597 assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
598 });
599 });
600}
601
602#[gpui::test]
603async fn test_channel_buffer_changes(
604 deterministic: BackgroundExecutor,
605 cx_a: &mut TestAppContext,
606 cx_b: &mut TestAppContext,
607) {
608 let (server, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
609 let (_, cx_a) = client_a.build_test_workspace(cx_a).await;
610 let (workspace_b, cx_b) = client_b.build_test_workspace(cx_b).await;
611 let channel_store_b = client_b.channel_store().clone();
612
613 // Editing the channel notes should set them to dirty
614 open_channel_notes(channel_id, cx_a).await.unwrap();
615 cx_a.simulate_keystrokes("1");
616 channel_store_b.read_with(cx_b, |channel_store, _| {
617 assert!(channel_store.has_channel_buffer_changed(channel_id))
618 });
619
620 // Opening the buffer should clear the changed flag.
621 open_channel_notes(channel_id, cx_b).await.unwrap();
622 channel_store_b.read_with(cx_b, |channel_store, _| {
623 assert!(!channel_store.has_channel_buffer_changed(channel_id))
624 });
625
626 // Editing the channel while the buffer is open should not show that the buffer has changed.
627 cx_a.simulate_keystrokes("2");
628 channel_store_b.read_with(cx_b, |channel_store, _| {
629 assert!(!channel_store.has_channel_buffer_changed(channel_id))
630 });
631
632 // Test that the server is tracking things correctly, and we retain our 'not changed'
633 // state across a disconnect
634 deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL);
635 server
636 .simulate_long_connection_interruption(client_b.peer_id().unwrap(), deterministic.clone());
637 channel_store_b.read_with(cx_b, |channel_store, _| {
638 assert!(!channel_store.has_channel_buffer_changed(channel_id))
639 });
640
641 // Closing the buffer should re-enable change tracking
642 cx_b.update(|cx| {
643 workspace_b.update(cx, |workspace, cx| {
644 workspace.close_all_items_and_panes(&Default::default(), cx)
645 });
646 });
647 deterministic.run_until_parked();
648
649 cx_a.simulate_keystrokes("3");
650 channel_store_b.read_with(cx_b, |channel_store, _| {
651 assert!(channel_store.has_channel_buffer_changed(channel_id))
652 });
653}
654
655#[gpui::test]
656async fn test_channel_buffer_changes_persist(
657 cx_a: &mut TestAppContext,
658 cx_b: &mut TestAppContext,
659 cx_b2: &mut TestAppContext,
660) {
661 let (mut server, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
662 let (_, cx_a) = client_a.build_test_workspace(cx_a).await;
663 let (_, cx_b) = client_b.build_test_workspace(cx_b).await;
664
665 // a) edits the notes
666 open_channel_notes(channel_id, cx_a).await.unwrap();
667 cx_a.simulate_keystrokes("1");
668 // b) opens them to observe the current version
669 open_channel_notes(channel_id, cx_b).await.unwrap();
670
671 // On boot the client should get the correct state.
672 let client_b2 = server.create_client(cx_b2, "user_b").await;
673 let channel_store_b2 = client_b2.channel_store().clone();
674 channel_store_b2.read_with(cx_b2, |channel_store, _| {
675 assert!(!channel_store.has_channel_buffer_changed(channel_id))
676 });
677}
678
679#[track_caller]
680fn assert_collaborators(collaborators: &HashMap<PeerId, Collaborator>, ids: &[Option<UserId>]) {
681 let mut user_ids = collaborators
682 .values()
683 .map(|collaborator| collaborator.user_id)
684 .collect::<Vec<_>>();
685 user_ids.sort();
686 assert_eq!(
687 user_ids,
688 ids.into_iter().map(|id| id.unwrap()).collect::<Vec<_>>()
689 );
690}
691
692fn buffer_text(channel_buffer: &Model<language::Buffer>, cx: &mut TestAppContext) -> String {
693 channel_buffer.read_with(cx, |buffer, _| buffer.text())
694}