Better TestWindow support (#3876)

Adding guest roles led us down a rabbit hole where we'd have liked to
rely on a
side-effect of activating a window in tests; but the test window didn't
implement that.

Looking into that, I realized our TestWindow wasn't doing a great job of
emulating the MacWindow, so this makes the two more similar.
This commit is contained in:
Conrad Irwin 2024-01-05 09:59:48 -07:00 committed by GitHub
commit df0076a4eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 176 additions and 156 deletions

View File

@ -1572,6 +1572,7 @@ mod tests {
workspace::init_settings(cx); workspace::init_settings(cx);
Project::init_settings(cx); Project::init_settings(cx);
crate::init(cx); crate::init(cx);
editor::init(cx);
}); });
} }

View File

@ -930,10 +930,7 @@ mod tests {
fn do_work() { «test»(); } fn do_work() { «test»(); }
"}); "});
// Deactivating the window dismisses the highlight cx.cx.cx.deactivate_window();
cx.update_workspace(|workspace, cx| {
workspace.on_window_activation_changed(cx);
});
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {" cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
fn test() { do_work(); } fn test() { do_work(); }
fn do_work() { test(); } fn do_work() { test(); }

View File

@ -1,14 +1,13 @@
use crate::{ use crate::{
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
BackgroundExecutor, Bounds, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor, BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor,
InputEvent, IntoElement, KeyDownEvent, Keystroke, Model, ModelContext, Pixels, Platform, IntoElement, Keystroke, Model, ModelContext, Pixels, Platform, Render, Result, Size, Task,
PlatformWindow, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, TestDispatcher, TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext,
TestWindowHandlers, TextSystem, View, ViewContext, VisualContext, WindowBounds, WindowContext, WindowContext, WindowHandle, WindowOptions,
WindowHandle, WindowOptions,
}; };
use anyhow::{anyhow, bail}; use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt}; use futures::{Stream, StreamExt};
use std::{future::Future, mem, ops::Deref, rc::Rc, sync::Arc, time::Duration}; use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
#[derive(Clone)] #[derive(Clone)]
pub struct TestAppContext { pub struct TestAppContext {
@ -185,42 +184,7 @@ impl TestAppContext {
} }
pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) { pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
let (mut handlers, scale_factor) = self self.test_window(window_handle).simulate_resize(size);
.app
.borrow_mut()
.update_window(window_handle, |_, cx| {
let platform_window = cx.window.platform_window.as_test().unwrap();
let scale_factor = platform_window.scale_factor();
match &mut platform_window.bounds {
WindowBounds::Fullscreen | WindowBounds::Maximized => {
platform_window.bounds = WindowBounds::Fixed(Bounds {
origin: Point::default(),
size: size.map(|pixels| f64::from(pixels).into()),
});
}
WindowBounds::Fixed(bounds) => {
bounds.size = size.map(|pixels| f64::from(pixels).into());
}
}
(
mem::take(&mut platform_window.handlers.lock().resize),
scale_factor,
)
})
.unwrap();
for handler in &mut handlers {
handler(size, scale_factor);
}
self.app
.borrow_mut()
.update_window(window_handle, |_, cx| {
let platform_window = cx.window.platform_window.as_test().unwrap();
platform_window.handlers.lock().resize = handlers;
})
.unwrap();
} }
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R> pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
@ -313,41 +277,22 @@ impl TestAppContext {
keystroke: Keystroke, keystroke: Keystroke,
is_held: bool, is_held: bool,
) { ) {
let keystroke2 = keystroke.clone(); self.test_window(window)
let handled = window .simulate_keystroke(keystroke, is_held)
.update(self, |_, cx| {
cx.dispatch_event(InputEvent::KeyDown(KeyDownEvent { keystroke, is_held }))
})
.is_ok_and(|handled| handled);
if handled {
return;
} }
let input_handler = self.update_test_window(window, |window| window.input_handler.clone()); pub fn test_window(&self, window: AnyWindowHandle) -> TestWindow {
let Some(input_handler) = input_handler else { self.app
panic!( .borrow_mut()
"dispatch_keystroke {:?} failed to dispatch action or input", .windows
&keystroke2 .get_mut(window.id)
);
};
let text = keystroke2.ime_key.unwrap_or(keystroke2.key);
input_handler.lock().replace_text_in_range(None, &text);
}
pub fn update_test_window<R>(
&mut self,
window: AnyWindowHandle,
f: impl FnOnce(&mut TestWindow) -> R,
) -> R {
window
.update(self, |_, cx| {
f(cx.window
.platform_window
.as_any_mut()
.downcast_mut::<TestWindow>()
.unwrap())
})
.unwrap() .unwrap()
.as_mut()
.unwrap()
.platform_window
.as_test()
.unwrap()
.clone()
} }
pub fn notifications<T: 'static>(&mut self, entity: &impl Entity<T>) -> impl Stream<Item = ()> { pub fn notifications<T: 'static>(&mut self, entity: &impl Entity<T>) -> impl Stream<Item = ()> {
@ -563,11 +508,7 @@ impl<'a> VisualTestContext<'a> {
} }
pub fn window_title(&mut self) -> Option<String> { pub fn window_title(&mut self) -> Option<String> {
self.cx self.cx.test_window(self.window).0.lock().title.clone()
.update_window(self.window, |_, cx| {
cx.window.platform_window.as_test().unwrap().title.clone()
})
.unwrap()
} }
pub fn simulate_keystrokes(&mut self, keystrokes: &str) { pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
@ -578,37 +519,11 @@ impl<'a> VisualTestContext<'a> {
self.cx.simulate_input(self.window, input) self.cx.simulate_input(self.window, input)
} }
pub fn simulate_activation(&mut self) { pub fn deactivate_window(&mut self) {
self.simulate_window_events(&mut |handlers| { if Some(self.window) == self.test_platform.active_window() {
handlers self.test_platform.set_active_window(None)
.active_status_change
.iter_mut()
.for_each(|f| f(true));
})
} }
self.background_executor.run_until_parked();
pub fn simulate_deactivation(&mut self) {
self.simulate_window_events(&mut |handlers| {
handlers
.active_status_change
.iter_mut()
.for_each(|f| f(false));
})
}
fn simulate_window_events(&mut self, f: &mut dyn FnMut(&mut TestWindowHandlers)) {
let handlers = self
.cx
.update_window(self.window, |_, cx| {
cx.window
.platform_window
.as_test()
.unwrap()
.handlers
.clone()
})
.unwrap();
f(&mut *handlers.lock());
} }
} }

View File

@ -19,7 +19,7 @@ pub struct TestPlatform {
background_executor: BackgroundExecutor, background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor, foreground_executor: ForegroundExecutor,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>, pub(crate) active_window: RefCell<Option<TestWindow>>,
active_display: Rc<dyn PlatformDisplay>, active_display: Rc<dyn PlatformDisplay>,
active_cursor: Mutex<CursorStyle>, active_cursor: Mutex<CursorStyle>,
current_clipboard_item: Mutex<Option<ClipboardItem>>, current_clipboard_item: Mutex<Option<ClipboardItem>>,
@ -79,6 +79,28 @@ impl TestPlatform {
self.prompts.borrow_mut().multiple_choice.push_back(tx); self.prompts.borrow_mut().multiple_choice.push_back(tx);
rx rx
} }
pub(crate) fn set_active_window(&self, window: Option<TestWindow>) {
let executor = self.foreground_executor().clone();
let previous_window = self.active_window.borrow_mut().take();
*self.active_window.borrow_mut() = window.clone();
executor
.spawn(async move {
if let Some(previous_window) = previous_window {
if let Some(window) = window.as_ref() {
if Arc::ptr_eq(&previous_window.0, &window.0) {
return;
}
}
previous_window.simulate_active_status_change(false);
}
if let Some(window) = window {
window.simulate_active_status_change(true);
}
})
.detach();
}
} }
// todo!("implement out what our tests needed in GPUI 1") // todo!("implement out what our tests needed in GPUI 1")
@ -106,7 +128,7 @@ impl Platform for TestPlatform {
} }
fn activate(&self, _ignoring_other_apps: bool) { fn activate(&self, _ignoring_other_apps: bool) {
unimplemented!() //
} }
fn hide(&self) { fn hide(&self) {
@ -130,7 +152,10 @@ impl Platform for TestPlatform {
} }
fn active_window(&self) -> Option<crate::AnyWindowHandle> { fn active_window(&self) -> Option<crate::AnyWindowHandle> {
self.active_window.lock().clone() self.active_window
.borrow()
.as_ref()
.map(|window| window.0.lock().handle)
} }
fn open_window( fn open_window(
@ -139,12 +164,13 @@ impl Platform for TestPlatform {
options: WindowOptions, options: WindowOptions,
_draw: Box<dyn FnMut() -> Result<Scene>>, _draw: Box<dyn FnMut() -> Result<Scene>>,
) -> Box<dyn crate::PlatformWindow> { ) -> Box<dyn crate::PlatformWindow> {
*self.active_window.lock() = Some(handle); let window = TestWindow::new(
Box::new(TestWindow::new(
options, options,
handle,
self.weak.clone(), self.weak.clone(),
self.active_display.clone(), self.active_display.clone(),
)) );
Box::new(window)
} }
fn set_display_link_output_callback( fn set_display_link_output_callback(

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay, px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, InputEvent, KeyDownEvent,
PlatformInputHandler, PlatformWindow, Point, Size, TestPlatform, TileId, WindowAppearance, Keystroke, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
WindowBounds, WindowOptions, Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions,
}; };
use collections::HashMap; use collections::HashMap;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -10,51 +10,122 @@ use std::{
sync::{self, Arc}, sync::{self, Arc},
}; };
#[derive(Default)] pub struct TestWindowState {
pub(crate) struct TestWindowHandlers {
pub(crate) active_status_change: Vec<Box<dyn FnMut(bool)>>,
pub(crate) input: Vec<Box<dyn FnMut(crate::InputEvent) -> bool>>,
pub(crate) moved: Vec<Box<dyn FnMut()>>,
pub(crate) resize: Vec<Box<dyn FnMut(Size<Pixels>, f32)>>,
}
pub struct TestWindow {
pub(crate) bounds: WindowBounds, pub(crate) bounds: WindowBounds,
pub(crate) handle: AnyWindowHandle,
display: Rc<dyn PlatformDisplay>, display: Rc<dyn PlatformDisplay>,
pub(crate) title: Option<String>, pub(crate) title: Option<String>,
pub(crate) edited: bool, pub(crate) edited: bool,
pub(crate) input_handler: Option<Arc<Mutex<Box<dyn PlatformInputHandler>>>>,
pub(crate) handlers: Arc<Mutex<TestWindowHandlers>>,
platform: Weak<TestPlatform>, platform: Weak<TestPlatform>,
sprite_atlas: Arc<dyn PlatformAtlas>, sprite_atlas: Arc<dyn PlatformAtlas>,
input_callback: Option<Box<dyn FnMut(InputEvent) -> bool>>,
active_status_change_callback: Option<Box<dyn FnMut(bool)>>,
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
moved_callback: Option<Box<dyn FnMut()>>,
input_handler: Option<Box<dyn PlatformInputHandler>>,
} }
#[derive(Clone)]
pub struct TestWindow(pub(crate) Arc<Mutex<TestWindowState>>);
impl TestWindow { impl TestWindow {
pub fn new( pub fn new(
options: WindowOptions, options: WindowOptions,
handle: AnyWindowHandle,
platform: Weak<TestPlatform>, platform: Weak<TestPlatform>,
display: Rc<dyn PlatformDisplay>, display: Rc<dyn PlatformDisplay>,
) -> Self { ) -> Self {
Self { Self(Arc::new(Mutex::new(TestWindowState {
bounds: options.bounds, bounds: options.bounds,
display, display,
platform, platform,
input_handler: None, handle,
sprite_atlas: Arc::new(TestAtlas::new()), sprite_atlas: Arc::new(TestAtlas::new()),
handlers: Default::default(),
title: Default::default(), title: Default::default(),
edited: false, edited: false,
input_callback: None,
active_status_change_callback: None,
resize_callback: None,
moved_callback: None,
input_handler: None,
})))
} }
pub fn simulate_resize(&mut self, size: Size<Pixels>) {
let scale_factor = self.scale_factor();
let mut lock = self.0.lock();
let Some(mut callback) = lock.resize_callback.take() else {
return;
};
match &mut lock.bounds {
WindowBounds::Fullscreen | WindowBounds::Maximized => {
lock.bounds = WindowBounds::Fixed(Bounds {
origin: Point::default(),
size: size.map(|pixels| f64::from(pixels).into()),
});
}
WindowBounds::Fixed(bounds) => {
bounds.size = size.map(|pixels| f64::from(pixels).into());
}
}
drop(lock);
callback(size, scale_factor);
self.0.lock().resize_callback = Some(callback);
}
pub(crate) fn simulate_active_status_change(&self, active: bool) {
let mut lock = self.0.lock();
let Some(mut callback) = lock.active_status_change_callback.take() else {
return;
};
drop(lock);
callback(active);
self.0.lock().active_status_change_callback = Some(callback);
}
pub fn simulate_input(&mut self, event: InputEvent) -> bool {
let mut lock = self.0.lock();
let Some(mut callback) = lock.input_callback.take() else {
return false;
};
drop(lock);
let result = callback(event);
self.0.lock().input_callback = Some(callback);
result
}
pub fn simulate_keystroke(&mut self, keystroke: Keystroke, is_held: bool) {
if self.simulate_input(InputEvent::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
is_held,
})) {
return;
}
let mut lock = self.0.lock();
let Some(mut input_handler) = lock.input_handler.take() else {
panic!(
"simulate_keystroke {:?} input event was not handled and there was no active input",
&keystroke
);
};
drop(lock);
let text = keystroke.ime_key.unwrap_or(keystroke.key);
input_handler.replace_text_in_range(None, &text);
self.0.lock().input_handler = Some(input_handler);
} }
} }
impl PlatformWindow for TestWindow { impl PlatformWindow for TestWindow {
fn bounds(&self) -> WindowBounds { fn bounds(&self) -> WindowBounds {
self.bounds self.0.lock().bounds
} }
fn content_size(&self) -> Size<Pixels> { fn content_size(&self) -> Size<Pixels> {
let bounds = match self.bounds { let bounds = match self.bounds() {
WindowBounds::Fixed(bounds) => bounds, WindowBounds::Fixed(bounds) => bounds,
WindowBounds::Maximized | WindowBounds::Fullscreen => self.display().bounds(), WindowBounds::Maximized | WindowBounds::Fullscreen => self.display().bounds(),
}; };
@ -74,7 +145,7 @@ impl PlatformWindow for TestWindow {
} }
fn display(&self) -> std::rc::Rc<dyn crate::PlatformDisplay> { fn display(&self) -> std::rc::Rc<dyn crate::PlatformDisplay> {
self.display.clone() self.0.lock().display.clone()
} }
fn mouse_position(&self) -> Point<Pixels> { fn mouse_position(&self) -> Point<Pixels> {
@ -90,11 +161,11 @@ impl PlatformWindow for TestWindow {
} }
fn set_input_handler(&mut self, input_handler: Box<dyn crate::PlatformInputHandler>) { fn set_input_handler(&mut self, input_handler: Box<dyn crate::PlatformInputHandler>) {
self.input_handler = Some(Arc::new(Mutex::new(input_handler))); self.0.lock().input_handler = Some(input_handler);
} }
fn clear_input_handler(&mut self) { fn clear_input_handler(&mut self) {
self.input_handler = None; self.0.lock().input_handler = None;
} }
fn prompt( fn prompt(
@ -103,19 +174,29 @@ impl PlatformWindow for TestWindow {
_msg: &str, _msg: &str,
_answers: &[&str], _answers: &[&str],
) -> futures::channel::oneshot::Receiver<usize> { ) -> futures::channel::oneshot::Receiver<usize> {
self.platform.upgrade().expect("platform dropped").prompt() self.0
.lock()
.platform
.upgrade()
.expect("platform dropped")
.prompt()
} }
fn activate(&self) { fn activate(&self) {
unimplemented!() self.0
.lock()
.platform
.upgrade()
.unwrap()
.set_active_window(Some(self.clone()))
} }
fn set_title(&mut self, title: &str) { fn set_title(&mut self, title: &str) {
self.title = Some(title.to_owned()); self.0.lock().title = Some(title.to_owned());
} }
fn set_edited(&mut self, edited: bool) { fn set_edited(&mut self, edited: bool) {
self.edited = edited; self.0.lock().edited = edited;
} }
fn show_character_palette(&self) { fn show_character_palette(&self) {
@ -135,15 +216,15 @@ impl PlatformWindow for TestWindow {
} }
fn on_input(&self, callback: Box<dyn FnMut(crate::InputEvent) -> bool>) { fn on_input(&self, callback: Box<dyn FnMut(crate::InputEvent) -> bool>) {
self.handlers.lock().input.push(callback) self.0.lock().input_callback = Some(callback)
} }
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) { fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.handlers.lock().active_status_change.push(callback) self.0.lock().active_status_change_callback = Some(callback)
} }
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) { fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
self.handlers.lock().resize.push(callback) self.0.lock().resize_callback = Some(callback)
} }
fn on_fullscreen(&self, _callback: Box<dyn FnMut(bool)>) { fn on_fullscreen(&self, _callback: Box<dyn FnMut(bool)>) {
@ -151,7 +232,7 @@ impl PlatformWindow for TestWindow {
} }
fn on_moved(&self, callback: Box<dyn FnMut()>) { fn on_moved(&self, callback: Box<dyn FnMut()>) {
self.handlers.lock().moved.push(callback) self.0.lock().moved_callback = Some(callback)
} }
fn on_should_close(&self, _callback: Box<dyn FnMut() -> bool>) { fn on_should_close(&self, _callback: Box<dyn FnMut() -> bool>) {
@ -175,7 +256,7 @@ impl PlatformWindow for TestWindow {
} }
fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> { fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
self.sprite_atlas.clone() self.0.lock().sprite_atlas.clone()
} }
fn as_test(&mut self) -> Option<&mut TestWindow> { fn as_test(&mut self) -> Option<&mut TestWindow> {

View File

@ -395,6 +395,7 @@ mod tests {
language::init(cx); language::init(cx);
Project::init_settings(cx); Project::init_settings(cx);
workspace::init_settings(cx); workspace::init_settings(cx);
editor::init(cx);
}); });
} }

View File

@ -3266,6 +3266,7 @@ impl Workspace {
let user_store = project.read(cx).user_store(); let user_store = project.read(cx).user_store();
let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx)); let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
cx.activate_window();
let app_state = Arc::new(AppState { let app_state = Arc::new(AppState {
languages: project.read(cx).languages().clone(), languages: project.read(cx).languages().clone(),
workspace_store, workspace_store,
@ -4764,8 +4765,7 @@ mod tests {
}); });
// Deactivating the window saves the file. // Deactivating the window saves the file.
cx.simulate_deactivation(); cx.deactivate_window();
cx.executor().run_until_parked();
item.update(cx, |item, _| assert_eq!(item.save_count, 1)); item.update(cx, |item, _| assert_eq!(item.save_count, 1));
// Autosave on focus change. // Autosave on focus change.
@ -4785,14 +4785,13 @@ mod tests {
item.update(cx, |item, _| assert_eq!(item.save_count, 2)); item.update(cx, |item, _| assert_eq!(item.save_count, 2));
// Deactivating the window still saves the file. // Deactivating the window still saves the file.
cx.simulate_activation(); cx.update(|cx| cx.activate_window());
item.update(cx, |item, cx| { item.update(cx, |item, cx| {
cx.focus_self(); cx.focus_self();
item.is_dirty = true; item.is_dirty = true;
}); });
cx.simulate_deactivation(); cx.deactivate_window();
cx.executor().run_until_parked();
item.update(cx, |item, _| assert_eq!(item.save_count, 3)); item.update(cx, |item, _| assert_eq!(item.save_count, 3));
// Autosave after delay. // Autosave after delay.