From 09165c15dc5d1fea93604231eaf30ca4c25f1cd6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 30 May 2026 14:37:39 -0600 Subject: [PATCH] gpui: Support prompt_for_paths in TestPlatform (#58139) Implements the previously-`unimplemented!()` `TestPlatform::prompt_for_paths` so tests can drive the platform Open dialog deterministically. Adds `TestAppContext::simulate_path_prompt_response` and `did_prompt_for_paths`, mirroring the existing `prompt_for_new_path` test helpers (`simulate_new_path_selection`). The simulated response validates that callers don't return multiple paths when `PathPromptOptions::multiple` is false. Release Notes: - N/A --- crates/gpui/src/app/test_context.rs | 65 +++++++++++++++++++++++ crates/gpui/src/platform/test/platform.rs | 46 +++++++++++++--- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 8a6d7e3f840..9e32c5dc2d4 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -336,6 +336,20 @@ impl TestAppContext { self.test_platform.simulate_new_path_selection(select_path); } + /// Simulates responding to a `prompt_for_paths` ("Open") dialog. + pub fn simulate_path_prompt_response( + &self, + select_paths: impl FnOnce(&crate::PathPromptOptions) -> Option>, + ) { + self.test_platform + .simulate_path_prompt_response(select_paths); + } + + /// Returns true if there's a path selection dialog pending. + pub fn did_prompt_for_paths(&self) -> bool { + self.test_platform.did_prompt_for_paths() + } + /// Simulates clicking a button in an platform-level alert dialog. #[track_caller] pub fn simulate_prompt_answer(&self, button: &str) { @@ -1098,3 +1112,54 @@ impl AnyWindowHandle { .unwrap() } } + +#[cfg(test)] +mod tests { + use crate::{PathPromptOptions, TestAppContext}; + use std::path::PathBuf; + + #[gpui::test] + async fn test_simulate_path_prompt_response(cx: &mut TestAppContext) { + assert!(!cx.did_prompt_for_paths()); + + let receiver = cx.update(|cx| { + cx.prompt_for_paths(PathPromptOptions { + files: false, + directories: true, + multiple: true, + prompt: None, + }) + }); + assert!(cx.did_prompt_for_paths()); + + let selected = vec![PathBuf::from("/a"), PathBuf::from("/b")]; + cx.simulate_path_prompt_response({ + let selected = selected.clone(); + move |options| { + assert!(options.multiple); + Some(selected) + } + }); + assert!(!cx.did_prompt_for_paths()); + + let response = receiver.await.unwrap().unwrap(); + assert_eq!(response, Some(selected)); + } + + #[gpui::test] + async fn test_simulate_path_prompt_cancellation(cx: &mut TestAppContext) { + let receiver = cx.update(|cx| { + cx.prompt_for_paths(PathPromptOptions { + files: true, + directories: false, + multiple: false, + prompt: None, + }) + }); + + cx.simulate_path_prompt_response(|_options| None); + + let response = receiver.await.unwrap().unwrap(); + assert_eq!(response, None); + } +} diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index cc8c5749bd4..b3bee3769e0 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -1,9 +1,10 @@ use crate::{ AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, - DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, - PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, - PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, - Task, TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, size, + DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, PathPromptOptions, Platform, + PlatformDisplay, PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, + PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, + SourceMetadata, Task, TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, + size, }; use anyhow::Result; use collections::VecDeque; @@ -85,6 +86,10 @@ struct TestPrompt { pub(crate) struct TestPrompts { multiple_choice: VecDeque, new_path: VecDeque<(PathBuf, oneshot::Sender>>)>, + paths: VecDeque<( + PathPromptOptions, + oneshot::Sender>>>, + )>, } impl TestPlatform { @@ -147,6 +152,33 @@ impl TestPlatform { tx.send(Ok(select_path(&path))).ok(); } + pub(crate) fn simulate_path_prompt_response( + &self, + select_paths: impl FnOnce(&PathPromptOptions) -> Option>, + ) { + let (options, tx) = self + .prompts + .borrow_mut() + .paths + .pop_front() + .expect("no pending paths prompt"); + let selection = select_paths(&options); + if let Some(paths) = &selection + && !options.multiple + && paths.len() > 1 + { + panic!( + "selected {} paths for a prompt that does not allow multiple selection", + paths.len() + ); + } + tx.send(Ok(selection)).ok(); + } + + pub(crate) fn did_prompt_for_paths(&self) -> bool { + !self.prompts.borrow().paths.is_empty() + } + #[track_caller] pub(crate) fn simulate_prompt_answer(&self, response: &str) { let prompt = self @@ -348,9 +380,11 @@ impl Platform for TestPlatform { fn prompt_for_paths( &self, - _options: crate::PathPromptOptions, + options: crate::PathPromptOptions, ) -> oneshot::Receiver>>> { - unimplemented!() + let (tx, rx) = oneshot::channel(); + self.prompts.borrow_mut().paths.push_back((options, tx)); + rx } fn prompt_for_new_path(