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