diff --git a/crates/collab-net/src/sessions_tests.rs b/crates/collab-net/src/sessions_tests.rs index 9e76e54e..4a2579f8 100644 --- a/crates/collab-net/src/sessions_tests.rs +++ b/crates/collab-net/src/sessions_tests.rs @@ -129,12 +129,11 @@ fn two_files_are_hosted_and_joined_independently() { // Each joined file opens in front when its welcome arrives, and the two joins race: the file // whose welcome came last is in front. assert_eq!(guest.ds().unwrap().uid, guest.docs[2].uid, "the file that arrived last is in front"); - guest.execute("doc.switch", &json!({"index": guest.docs.iter().position(|d| d.uid == gb).unwrap()})).unwrap(); - // An edit in A reaches the guest's copy of A only, while B is in front at the guest. - // Concurrent joins may complete in either order. Put B in front explicitly so the - // following assertions exercise A's background session independent of network timing. + // Concurrent joins may complete in either order. Put B in front explicitly so the following + // assertions exercise A's background session independent of network timing. guest.execute("doc.switch", &json!({"index": guest.docs.iter().position(|d| d.uid == gb).unwrap()})).unwrap(); assert_eq!(guest.ds().unwrap().uid, gb, "B is in front while A receives edits"); + // An edit in A reaches the guest's copy of A only, while B is in front at the guest. let in_a = rect(&mut host); host.execute("doc.switch", &json!({"index": 1})).unwrap(); let in_b = rect(&mut host); diff --git a/crates/ui-egui/src/clipboard_fence_tests.rs b/crates/ui-egui/src/clipboard_fence_tests.rs index 76ddf81c..9b7ed4fe 100644 --- a/crates/ui-egui/src/clipboard_fence_tests.rs +++ b/crates/ui-egui/src/clipboard_fence_tests.rs @@ -131,6 +131,37 @@ fn a_copy_command_invoked_for_a_synthetic_click_still_keeps_the_clipboard() { assert_eq!(w.writes(), (1, 0)); } +/// Asserts a real `ui.menu.invoke` of Copy link is fenced from the system clipboard, while the +/// same menu item invoked by the user writes the selected layer's link. +#[test] +fn a_synthetic_ui_menu_invoke_copy_link_keeps_the_clipboard_then_a_user_copy_writes_the_link() { + let mut w = Win::new(); + let selected = *w.app.session.selection().first().expect("a selected layer"); + let link = format!("figcraft://node/{}", selected.0); + + let (tx, rx) = std::sync::mpsc::channel(); + w.app.control_queue.push(ControlRequest { + id: json!(1), + method: "ui.menu.invoke".into(), + params: json!({"id": "edit.copyLink"}), + reply: tx, + state: Default::default(), + }); + w.frame(vec![]); + let reply = rx.try_recv().expect("the menu invocation is acknowledged"); + assert_eq!(reply.id, json!(1)); + assert!(reply.result.is_ok(), "the menu item ran: {:?}", reply.result); + assert_eq!(w.writes(), (0, 0), "a synthetic menu invocation never replaces the user's clipboard"); + assert_eq!(w.app.ui.toast.as_ref().map(|t| t.0.as_str()), Some("Link copied")); + assert!(!w.app.control_synthetic, "the fence ends with the frame"); + + crate::menus::invoke(&mut w.app, "edit.copyLink"); + assert_eq!(w.writes(), (1, 0), "the user's invocation writes exactly one text clipboard entry"); + let recorded = w.recorded.lock().unwrap(); + assert_eq!(recorded.text, vec![link.clone()]); + assert_eq!(recorded.last_text.as_deref(), Some(link.as_str())); +} + /// Return the characters of a text node in the test window. fn edit_text(w: &Win, id: NodeId) -> String { match &w.app.session.doc().unwrap().node(id).unwrap().kind { diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 3428844e..4b3483af 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -241,8 +241,10 @@ pub fn invoke(app: &mut FigApp, id: &str) { app.toast(tr("Copied as text")); } "edit.copyLink" => { + // Only a selected layer has a link, and the copy goes through the control-channel fence + // (`clipboard_set_text`): a synthetic gesture never replaces the user's clipboard. if let Some(id) = app.session.selection().first() { - app.services.clipboard_set_text(&format!("figcraft://node/{}", id.0)); + app.clipboard_set_text(&format!("figcraft://node/{}", id.0)); app.toast(tr("Link copied")); } }