1use crate::{
2 AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
3 DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
4 PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
5 PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata,
6 Task, TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, size,
7};
8use anyhow::Result;
9use collections::VecDeque;
10use futures::channel::oneshot;
11use parking_lot::Mutex;
12use std::{
13 cell::RefCell,
14 path::{Path, PathBuf},
15 rc::{Rc, Weak},
16 sync::Arc,
17};
18
19/// TestPlatform implements the Platform trait for use in tests.
20pub(crate) struct TestPlatform {
21 background_executor: BackgroundExecutor,
22 foreground_executor: ForegroundExecutor,
23
24 pub(crate) active_window: RefCell<Option<TestWindow>>,
25 active_display: Rc<dyn PlatformDisplay>,
26 active_cursor: Mutex<CursorStyle>,
27 current_clipboard_item: Mutex<Option<ClipboardItem>>,
28 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
29 current_primary_item: Mutex<Option<ClipboardItem>>,
30 #[cfg(target_os = "macos")]
31 current_find_pasteboard_item: Mutex<Option<ClipboardItem>>,
32 pub(crate) prompts: RefCell<TestPrompts>,
33 screen_capture_sources: RefCell<Vec<TestScreenCaptureSource>>,
34 pub opened_url: RefCell<Option<String>>,
35 pub text_system: Arc<dyn PlatformTextSystem>,
36 pub expect_restart: RefCell<Option<oneshot::Sender<Option<PathBuf>>>>,
37 headless_renderer_factory: Option<Box<dyn Fn() -> Option<Box<dyn PlatformHeadlessRenderer>>>>,
38 weak: Weak<Self>,
39}
40
41#[derive(Clone)]
42/// A fake screen capture source, used for testing.
43pub struct TestScreenCaptureSource {}
44
45/// A fake screen capture stream, used for testing.
46pub struct TestScreenCaptureStream {}
47
48impl ScreenCaptureSource for TestScreenCaptureSource {
49 fn metadata(&self) -> Result<SourceMetadata> {
50 Ok(SourceMetadata {
51 id: 0,
52 is_main: None,
53 label: None,
54 resolution: size(DevicePixels(1), DevicePixels(1)),
55 })
56 }
57
58 fn stream(
59 &self,
60 _foreground_executor: &ForegroundExecutor,
61 _frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
62 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>> {
63 let (mut tx, rx) = oneshot::channel();
64 let stream = TestScreenCaptureStream {};
65 tx.send(Ok(Box::new(stream) as Box<dyn ScreenCaptureStream>))
66 .ok();
67 rx
68 }
69}
70
71impl ScreenCaptureStream for TestScreenCaptureStream {
72 fn metadata(&self) -> Result<SourceMetadata> {
73 TestScreenCaptureSource {}.metadata()
74 }
75}
76
77struct TestPrompt {
78 msg: String,
79 detail: Option<String>,
80 answers: Vec<String>,
81 tx: oneshot::Sender<usize>,
82}
83
84#[derive(Default)]
85pub(crate) struct TestPrompts {
86 multiple_choice: VecDeque<TestPrompt>,
87 new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
88}
89
90impl TestPlatform {
91 pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc<Self> {
92 Self::with_platform(
93 executor,
94 foreground_executor,
95 Arc::new(NoopTextSystem),
96 None,
97 )
98 }
99
100 pub fn with_text_system(
101 executor: BackgroundExecutor,
102 foreground_executor: ForegroundExecutor,
103 text_system: Arc<dyn PlatformTextSystem>,
104 ) -> Rc<Self> {
105 Self::with_platform(executor, foreground_executor, text_system, None)
106 }
107
108 pub fn with_platform(
109 executor: BackgroundExecutor,
110 foreground_executor: ForegroundExecutor,
111 text_system: Arc<dyn PlatformTextSystem>,
112 headless_renderer_factory: Option<
113 Box<dyn Fn() -> Option<Box<dyn PlatformHeadlessRenderer>>>,
114 >,
115 ) -> Rc<Self> {
116 Rc::new_cyclic(|weak| TestPlatform {
117 background_executor: executor,
118 foreground_executor,
119 prompts: Default::default(),
120 screen_capture_sources: Default::default(),
121 active_cursor: Default::default(),
122 active_display: Rc::new(TestDisplay::new()),
123 active_window: Default::default(),
124 expect_restart: Default::default(),
125 current_clipboard_item: Mutex::new(None),
126 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
127 current_primary_item: Mutex::new(None),
128 #[cfg(target_os = "macos")]
129 current_find_pasteboard_item: Mutex::new(None),
130 weak: weak.clone(),
131 opened_url: Default::default(),
132 text_system,
133 headless_renderer_factory,
134 })
135 }
136
137 pub(crate) fn simulate_new_path_selection(
138 &self,
139 select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
140 ) {
141 let (path, tx) = self
142 .prompts
143 .borrow_mut()
144 .new_path
145 .pop_front()
146 .expect("no pending new path prompt");
147 tx.send(Ok(select_path(&path))).ok();
148 }
149
150 #[track_caller]
151 pub(crate) fn simulate_prompt_answer(&self, response: &str) {
152 let prompt = self
153 .prompts
154 .borrow_mut()
155 .multiple_choice
156 .pop_front()
157 .expect("no pending multiple choice prompt");
158 let Some(ix) = prompt.answers.iter().position(|a| a == response) else {
159 panic!(
160 "PROMPT: {}\n{:?}\n{:?}\nCannot respond with {}",
161 prompt.msg, prompt.detail, prompt.answers, response
162 )
163 };
164 prompt.tx.send(ix).ok();
165 }
166
167 pub(crate) fn has_pending_prompt(&self) -> bool {
168 !self.prompts.borrow().multiple_choice.is_empty()
169 }
170
171 pub(crate) fn pending_prompt(&self) -> Option<(String, String)> {
172 let prompts = self.prompts.borrow();
173 let prompt = prompts.multiple_choice.front()?;
174 Some((
175 prompt.msg.clone(),
176 prompt.detail.clone().unwrap_or_default(),
177 ))
178 }
179
180 pub(crate) fn set_screen_capture_sources(&self, sources: Vec<TestScreenCaptureSource>) {
181 *self.screen_capture_sources.borrow_mut() = sources;
182 }
183
184 pub(crate) fn prompt(
185 &self,
186 msg: &str,
187 detail: Option<&str>,
188 answers: &[PromptButton],
189 ) -> oneshot::Receiver<usize> {
190 let (tx, rx) = oneshot::channel();
191 let answers: Vec<String> = answers.iter().map(|s| s.label().to_string()).collect();
192 self.prompts
193 .borrow_mut()
194 .multiple_choice
195 .push_back(TestPrompt {
196 msg: msg.to_string(),
197 detail: detail.map(|s| s.to_string()),
198 answers,
199 tx,
200 });
201 rx
202 }
203
204 pub(crate) fn set_active_window(&self, window: Option<TestWindow>) {
205 let executor = self.foreground_executor();
206 let previous_window = self.active_window.borrow_mut().take();
207 self.active_window.borrow_mut().clone_from(&window);
208
209 executor
210 .spawn(async move {
211 if let Some(previous_window) = previous_window {
212 if let Some(window) = window.as_ref()
213 && Rc::ptr_eq(&previous_window.0, &window.0)
214 {
215 return;
216 }
217 previous_window.simulate_active_status_change(false);
218 }
219 if let Some(window) = window {
220 window.simulate_active_status_change(true);
221 }
222 })
223 .detach();
224 }
225
226 pub(crate) fn did_prompt_for_new_path(&self) -> bool {
227 !self.prompts.borrow().new_path.is_empty()
228 }
229}
230
231impl Platform for TestPlatform {
232 fn background_executor(&self) -> BackgroundExecutor {
233 self.background_executor.clone()
234 }
235
236 fn foreground_executor(&self) -> ForegroundExecutor {
237 self.foreground_executor.clone()
238 }
239
240 fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
241 self.text_system.clone()
242 }
243
244 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
245 Box::new(TestKeyboardLayout)
246 }
247
248 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
249 Rc::new(DummyKeyboardMapper)
250 }
251
252 fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
253
254 fn on_thermal_state_change(&self, _: Box<dyn FnMut()>) {}
255
256 fn thermal_state(&self) -> ThermalState {
257 ThermalState::Nominal
258 }
259
260 fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
261 unimplemented!()
262 }
263
264 fn quit(&self) {}
265
266 fn restart(&self, path: Option<PathBuf>) {
267 if let Some(tx) = self.expect_restart.take() {
268 tx.send(path).unwrap();
269 }
270 }
271
272 fn activate(&self, _ignoring_other_apps: bool) {
273 //
274 }
275
276 fn hide(&self) {
277 unimplemented!()
278 }
279
280 fn hide_other_apps(&self) {
281 unimplemented!()
282 }
283
284 fn unhide_other_apps(&self) {
285 unimplemented!()
286 }
287
288 fn displays(&self) -> Vec<std::rc::Rc<dyn crate::PlatformDisplay>> {
289 vec![self.active_display.clone()]
290 }
291
292 fn primary_display(&self) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
293 Some(self.active_display.clone())
294 }
295
296 fn is_screen_capture_supported(&self) -> bool {
297 true
298 }
299
300 fn screen_capture_sources(
301 &self,
302 ) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
303 let (mut tx, rx) = oneshot::channel();
304 tx.send(Ok(self
305 .screen_capture_sources
306 .borrow()
307 .iter()
308 .map(|source| Rc::new(source.clone()) as Rc<dyn ScreenCaptureSource>)
309 .collect()))
310 .ok();
311 rx
312 }
313
314 fn active_window(&self) -> Option<crate::AnyWindowHandle> {
315 self.active_window
316 .borrow()
317 .as_ref()
318 .map(|window| window.0.lock().handle)
319 }
320
321 fn open_window(
322 &self,
323 handle: AnyWindowHandle,
324 params: WindowParams,
325 ) -> anyhow::Result<Box<dyn crate::PlatformWindow>> {
326 let renderer = self.headless_renderer_factory.as_ref().and_then(|f| f());
327 let window = TestWindow::new(
328 handle,
329 params,
330 self.weak.clone(),
331 self.active_display.clone(),
332 renderer,
333 );
334 Ok(Box::new(window))
335 }
336
337 fn window_appearance(&self) -> WindowAppearance {
338 WindowAppearance::Light
339 }
340
341 fn open_url(&self, url: &str) {
342 *self.opened_url.borrow_mut() = Some(url.to_string())
343 }
344
345 fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {
346 unimplemented!()
347 }
348
349 fn prompt_for_paths(
350 &self,
351 _options: crate::PathPromptOptions,
352 ) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
353 unimplemented!()
354 }
355
356 fn prompt_for_new_path(
357 &self,
358 directory: &std::path::Path,
359 _suggested_name: Option<&str>,
360 ) -> oneshot::Receiver<Result<Option<std::path::PathBuf>>> {
361 let (tx, rx) = oneshot::channel();
362 self.prompts
363 .borrow_mut()
364 .new_path
365 .push_back((directory.to_path_buf(), tx));
366 rx
367 }
368
369 fn can_select_mixed_files_and_dirs(&self) -> bool {
370 true
371 }
372
373 fn reveal_path(&self, _path: &std::path::Path) {
374 unimplemented!()
375 }
376
377 fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
378
379 fn on_reopen(&self, _callback: Box<dyn FnMut()>) {
380 unimplemented!()
381 }
382
383 fn set_menus(&self, _menus: Vec<crate::Menu>, _keymap: &Keymap) {}
384 fn set_dock_menu(&self, _menu: Vec<crate::MenuItem>, _keymap: &Keymap) {}
385
386 fn add_recent_document(&self, _paths: &Path) {}
387
388 fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {}
389
390 fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {}
391
392 fn on_validate_app_menu_command(&self, _callback: Box<dyn FnMut(&dyn crate::Action) -> bool>) {}
393
394 fn app_path(&self) -> Result<std::path::PathBuf> {
395 unimplemented!()
396 }
397
398 fn path_for_auxiliary_executable(&self, _name: &str) -> Result<std::path::PathBuf> {
399 unimplemented!()
400 }
401
402 fn set_cursor_style(&self, style: crate::CursorStyle) {
403 *self.active_cursor.lock() = style;
404 }
405
406 fn should_auto_hide_scrollbars(&self) -> bool {
407 false
408 }
409
410 fn read_from_clipboard(&self) -> Option<ClipboardItem> {
411 self.current_clipboard_item.lock().clone()
412 }
413
414 fn write_to_clipboard(&self, item: ClipboardItem) {
415 *self.current_clipboard_item.lock() = Some(item);
416 }
417
418 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
419 fn read_from_primary(&self) -> Option<ClipboardItem> {
420 self.current_primary_item.lock().clone()
421 }
422
423 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
424 fn write_to_primary(&self, item: ClipboardItem) {
425 *self.current_primary_item.lock() = Some(item);
426 }
427
428 #[cfg(target_os = "macos")]
429 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
430 self.current_find_pasteboard_item.lock().clone()
431 }
432
433 #[cfg(target_os = "macos")]
434 fn write_to_find_pasteboard(&self, item: ClipboardItem) {
435 *self.current_find_pasteboard_item.lock() = Some(item);
436 }
437
438 fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
439 Task::ready(Ok(()))
440 }
441
442 fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
443 Task::ready(Ok(None))
444 }
445
446 fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
447 Task::ready(Ok(()))
448 }
449
450 fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
451 unimplemented!()
452 }
453
454 fn open_with_system(&self, _path: &Path) {
455 unimplemented!()
456 }
457}
458
459impl TestScreenCaptureSource {
460 /// Create a fake screen capture source, for testing.
461 pub fn new() -> Self {
462 Self {}
463 }
464}
465
466struct TestKeyboardLayout;
467
468impl PlatformKeyboardLayout for TestKeyboardLayout {
469 fn id(&self) -> &str {
470 "zed.keyboard.example"
471 }
472
473 fn name(&self) -> &str {
474 "zed.keyboard.example"
475 }
476}