@@ -1,9 +1,9 @@
use crate::{
- Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, Bounds,
- ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
- MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point, Render,
- Result, Size, Task, TextSystem, Window, WindowBounds, WindowHandle, WindowOptions,
- app::GpuiMode, current_platform,
+ Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor,
+ Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke,
+ Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point,
+ Render, Result, Size, Task, TestDispatcher, TextSystem, VisualTestPlatform, Window,
+ WindowBounds, WindowHandle, WindowOptions, app::GpuiMode,
};
use anyhow::anyhow;
use image::RgbaImage;
@@ -25,26 +25,52 @@ pub struct VisualTestAppContext {
pub background_executor: BackgroundExecutor,
/// The foreground executor for running tasks on the main thread
pub foreground_executor: ForegroundExecutor,
+ /// The test dispatcher for deterministic task scheduling
+ dispatcher: TestDispatcher,
platform: Rc<dyn Platform>,
text_system: Arc<TextSystem>,
}
impl VisualTestAppContext {
- /// Creates a new `VisualTestAppContext` with real macOS platform rendering.
+ /// Creates a new `VisualTestAppContext` with real macOS platform rendering
+ /// but deterministic task scheduling via TestDispatcher.
///
- /// This initializes the real macOS platform (not the test platform), which means:
- /// - Windows are actually rendered by Metal/the compositor
- /// - Screenshots can be captured via ScreenCaptureKit
- /// - All platform APIs work as they do in production
+ /// This provides:
+ /// - Real Metal/compositor rendering for accurate screenshots
+ /// - Deterministic task scheduling via TestDispatcher
+ /// - Controllable time via `advance_clock`
+ ///
+ /// Note: This uses a no-op asset source, so SVG icons won't render.
+ /// Use `with_asset_source` to provide real assets for icon rendering.
pub fn new() -> Self {
+ Self::with_asset_source(Arc::new(()))
+ }
+
+ /// Creates a new `VisualTestAppContext` with a custom asset source.
+ ///
+ /// Use this when you need SVG icons to render properly in visual tests.
+ /// Pass the real `Assets` struct to enable icon rendering.
+ pub fn with_asset_source(asset_source: Arc<dyn AssetSource>) -> Self {
+ // Use a seeded RNG for deterministic behavior
+ let seed = std::env::var("SEED")
+ .ok()
+ .and_then(|s| s.parse().ok())
+ .unwrap_or(0);
+
+ // Create liveness for task cancellation
let liveness = Arc::new(());
- let liveness_weak = Arc::downgrade(&liveness);
- let platform = current_platform(false, liveness_weak);
+
+ // Create a visual test platform that combines real Mac rendering
+ // with controllable TestDispatcher for deterministic task scheduling
+ let platform = Rc::new(VisualTestPlatform::new(seed, Arc::downgrade(&liveness)));
+
+ // Get the dispatcher and executors from the platform
+ let dispatcher = platform.dispatcher().clone();
let background_executor = platform.background_executor();
let foreground_executor = platform.foreground_executor();
+
let text_system = Arc::new(TextSystem::new(platform.text_system()));
- let asset_source = Arc::new(());
let http_client = http_client::FakeHttpClient::with_404_response();
let mut app = App::new_app(platform.clone(), liveness, asset_source, http_client);
@@ -54,6 +80,7 @@ impl VisualTestAppContext {
app,
background_executor,
foreground_executor,
+ dispatcher,
platform,
text_system,
}
@@ -120,9 +147,17 @@ impl VisualTestAppContext {
self.foreground_executor.clone()
}
- /// Runs pending background tasks until there's nothing left to do.
+ /// Runs all pending foreground and background tasks until there's nothing left to do.
+ /// This is essential for processing async operations like tooltip timers.
pub fn run_until_parked(&self) {
- self.background_executor.run_until_parked();
+ self.dispatcher.run_until_parked();
+ }
+
+ /// Advances the simulated clock by the given duration and processes any tasks
+ /// that become ready. This is essential for testing time-based behaviors like
+ /// tooltip delays.
+ pub fn advance_clock(&self, duration: Duration) {
+ self.dispatcher.advance_clock(duration);
}
/// Updates the app state.
@@ -444,3 +479,112 @@ impl AppContext for VisualTestAppContext {
callback(app.global::<G>(), &app)
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::Empty;
+ use std::cell::RefCell;
+
+ // Note: All VisualTestAppContext tests are ignored by default because they require
+ // the macOS main thread. Standard Rust tests run on worker threads, which causes
+ // SIGABRT when interacting with macOS AppKit/Cocoa APIs.
+ //
+ // To run these tests, use:
+ // cargo test -p gpui visual_test_context -- --ignored --test-threads=1
+
+ #[test]
+ #[ignore] // Requires macOS main thread
+ fn test_foreground_tasks_run_with_run_until_parked() {
+ let mut cx = VisualTestAppContext::new();
+
+ let task_ran = Rc::new(RefCell::new(false));
+
+ // Spawn a foreground task via the App's spawn method
+ // This should use our TestDispatcher, not the MacDispatcher
+ {
+ let task_ran = task_ran.clone();
+ cx.update(|cx| {
+ cx.spawn(async move |_| {
+ *task_ran.borrow_mut() = true;
+ })
+ .detach();
+ });
+ }
+
+ // The task should not have run yet
+ assert!(!*task_ran.borrow());
+
+ // Run until parked should execute the foreground task
+ cx.run_until_parked();
+
+ // Now the task should have run
+ assert!(*task_ran.borrow());
+ }
+
+ #[test]
+ #[ignore] // Requires macOS main thread
+ fn test_advance_clock_triggers_delayed_tasks() {
+ let mut cx = VisualTestAppContext::new();
+
+ let task_ran = Rc::new(RefCell::new(false));
+
+ // Spawn a task that waits for a timer
+ {
+ let task_ran = task_ran.clone();
+ let executor = cx.background_executor.clone();
+ cx.update(|cx| {
+ cx.spawn(async move |_| {
+ executor.timer(Duration::from_millis(500)).await;
+ *task_ran.borrow_mut() = true;
+ })
+ .detach();
+ });
+ }
+
+ // Run until parked - the task should be waiting on the timer
+ cx.run_until_parked();
+ assert!(!*task_ran.borrow());
+
+ // Advance clock past the timer duration
+ cx.advance_clock(Duration::from_millis(600));
+
+ // Now the task should have completed
+ assert!(*task_ran.borrow());
+ }
+
+ #[test]
+ #[ignore] // Requires macOS main thread - window creation fails on test threads
+ fn test_window_spawn_uses_test_dispatcher() {
+ let mut cx = VisualTestAppContext::new();
+
+ let task_ran = Rc::new(RefCell::new(false));
+
+ let window = cx
+ .open_offscreen_window_default(|_, cx| cx.new(|_| Empty))
+ .expect("Failed to open window");
+
+ // Spawn a task via window.spawn - this is the critical test case
+ // for tooltip behavior, as tooltips use window.spawn for delayed show
+ {
+ let task_ran = task_ran.clone();
+ cx.update_window(window.into(), |_, window, cx| {
+ window
+ .spawn(cx, async move |_| {
+ *task_ran.borrow_mut() = true;
+ })
+ .detach();
+ })
+ .ok();
+ }
+
+ // The task should not have run yet
+ assert!(!*task_ran.borrow());
+
+ // Run until parked should execute the foreground task spawned via window
+ cx.run_until_parked();
+
+ // Now the task should have run
+ assert!(*task_ran.borrow());
+ }
+}
@@ -0,0 +1,255 @@
+//! Visual test platform that combines real rendering (macOs-only for now) with controllable TestDispatcher.
+//!
+//! This platform is used for visual tests that need:
+//! - Real rendering (e.g. Metal/compositor) for accurate screenshots
+//! - Deterministic task scheduling via TestDispatcher
+//! - Controllable time via `advance_clock`
+
+#[cfg(feature = "screen-capture")]
+use crate::ScreenCaptureSource;
+use crate::{
+ AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor, Keymap,
+ MacPlatform, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, PlatformDisplay,
+ PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task,
+ TestDispatcher, WindowAppearance, WindowParams,
+};
+use anyhow::Result;
+use futures::channel::oneshot;
+use parking_lot::Mutex;
+use rand::SeedableRng;
+use std::{
+ path::{Path, PathBuf},
+ rc::Rc,
+ sync::Arc,
+};
+
+/// A platform that combines real Mac rendering with controllable TestDispatcher.
+///
+/// This allows visual tests to:
+/// - Render real UI via Metal for accurate screenshots
+/// - Control task scheduling deterministically via TestDispatcher
+/// - Advance simulated time for testing time-based behaviors (tooltips, animations, etc.)
+pub struct VisualTestPlatform {
+ dispatcher: TestDispatcher,
+ background_executor: BackgroundExecutor,
+ foreground_executor: ForegroundExecutor,
+ mac_platform: MacPlatform,
+ clipboard: Mutex<Option<ClipboardItem>>,
+ find_pasteboard: Mutex<Option<ClipboardItem>>,
+}
+
+impl VisualTestPlatform {
+ /// Creates a new VisualTestPlatform with the given random seed and liveness tracker.
+ ///
+ /// The seed is used for deterministic random number generation in the TestDispatcher.
+ /// The liveness weak reference is used to track when the app is being shut down.
+ pub fn new(seed: u64, liveness: std::sync::Weak<()>) -> Self {
+ let rng = rand::rngs::StdRng::seed_from_u64(seed);
+ let dispatcher = TestDispatcher::new(rng);
+ let arc_dispatcher = Arc::new(dispatcher.clone());
+
+ let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
+ let foreground_executor = ForegroundExecutor::new(arc_dispatcher, liveness.clone());
+
+ let mac_platform = MacPlatform::new(false, liveness);
+
+ Self {
+ dispatcher,
+ background_executor,
+ foreground_executor,
+ mac_platform,
+ clipboard: Mutex::new(None),
+ find_pasteboard: Mutex::new(None),
+ }
+ }
+
+ /// Returns a reference to the TestDispatcher for controlling task scheduling and time.
+ pub fn dispatcher(&self) -> &TestDispatcher {
+ &self.dispatcher
+ }
+}
+
+impl Platform for VisualTestPlatform {
+ fn background_executor(&self) -> BackgroundExecutor {
+ self.background_executor.clone()
+ }
+
+ fn foreground_executor(&self) -> ForegroundExecutor {
+ self.foreground_executor.clone()
+ }
+
+ fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
+ self.mac_platform.text_system()
+ }
+
+ fn run(&self, _on_finish_launching: Box<dyn 'static + FnOnce()>) {
+ panic!("VisualTestPlatform::run should not be called in tests")
+ }
+
+ fn quit(&self) {}
+
+ fn restart(&self, _binary_path: Option<PathBuf>) {}
+
+ fn activate(&self, _ignoring_other_apps: bool) {}
+
+ fn hide(&self) {}
+
+ fn hide_other_apps(&self) {}
+
+ fn unhide_other_apps(&self) {}
+
+ fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
+ self.mac_platform.displays()
+ }
+
+ fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
+ self.mac_platform.primary_display()
+ }
+
+ fn active_window(&self) -> Option<AnyWindowHandle> {
+ self.mac_platform.active_window()
+ }
+
+ fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
+ self.mac_platform.window_stack()
+ }
+
+ #[cfg(feature = "screen-capture")]
+ fn is_screen_capture_supported(&self) -> bool {
+ self.mac_platform.is_screen_capture_supported()
+ }
+
+ #[cfg(feature = "screen-capture")]
+ fn screen_capture_sources(
+ &self,
+ ) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
+ self.mac_platform.screen_capture_sources()
+ }
+
+ fn open_window(
+ &self,
+ handle: AnyWindowHandle,
+ options: WindowParams,
+ ) -> Result<Box<dyn PlatformWindow>> {
+ self.mac_platform.open_window(handle, options)
+ }
+
+ fn window_appearance(&self) -> WindowAppearance {
+ self.mac_platform.window_appearance()
+ }
+
+ fn open_url(&self, url: &str) {
+ self.mac_platform.open_url(url)
+ }
+
+ fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {}
+
+ fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> {
+ Task::ready(Ok(()))
+ }
+
+ fn prompt_for_paths(
+ &self,
+ _options: PathPromptOptions,
+ ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
+ let (tx, rx) = oneshot::channel();
+ tx.send(Ok(None)).ok();
+ rx
+ }
+
+ fn prompt_for_new_path(
+ &self,
+ _directory: &Path,
+ _suggested_name: Option<&str>,
+ ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
+ let (tx, rx) = oneshot::channel();
+ tx.send(Ok(None)).ok();
+ rx
+ }
+
+ fn can_select_mixed_files_and_dirs(&self) -> bool {
+ true
+ }
+
+ fn reveal_path(&self, path: &Path) {
+ self.mac_platform.reveal_path(path)
+ }
+
+ fn open_with_system(&self, path: &Path) {
+ self.mac_platform.open_with_system(path)
+ }
+
+ fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
+
+ fn on_reopen(&self, _callback: Box<dyn FnMut()>) {}
+
+ fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
+
+ fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
+ None
+ }
+
+ fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
+
+ fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {}
+
+ fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {}
+
+ fn on_validate_app_menu_command(&self, _callback: Box<dyn FnMut(&dyn crate::Action) -> bool>) {}
+
+ fn app_path(&self) -> Result<PathBuf> {
+ self.mac_platform.app_path()
+ }
+
+ fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
+ self.mac_platform.path_for_auxiliary_executable(name)
+ }
+
+ fn set_cursor_style(&self, style: CursorStyle) {
+ self.mac_platform.set_cursor_style(style)
+ }
+
+ fn should_auto_hide_scrollbars(&self) -> bool {
+ self.mac_platform.should_auto_hide_scrollbars()
+ }
+
+ fn read_from_clipboard(&self) -> Option<ClipboardItem> {
+ self.clipboard.lock().clone()
+ }
+
+ fn write_to_clipboard(&self, item: ClipboardItem) {
+ *self.clipboard.lock() = Some(item);
+ }
+
+ #[cfg(target_os = "macos")]
+ fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
+ self.find_pasteboard.lock().clone()
+ }
+
+ #[cfg(target_os = "macos")]
+ fn write_to_find_pasteboard(&self, item: ClipboardItem) {
+ *self.find_pasteboard.lock() = Some(item);
+ }
+
+ fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
+ Task::ready(Ok(()))
+ }
+
+ fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
+ Task::ready(Ok(None))
+ }
+
+ fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
+ Task::ready(Ok(()))
+ }
+
+ fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
+ self.mac_platform.keyboard_layout()
+ }
+
+ fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
+ self.mac_platform.keyboard_mapper()
+ }
+
+ fn on_keyboard_layout_change(&self, _callback: Box<dyn FnMut()>) {}
+}
@@ -1,15 +1,26 @@
+// Allow blocking process commands in this binary - it's a synchronous test runner
+#![allow(clippy::disallowed_methods)]
+
//! Visual Test Runner
//!
//! This binary runs visual regression tests for Zed's UI. It captures screenshots
//! of real Zed windows and compares them against baseline images.
//!
+//! **Note: This tool is macOS-only** because it uses `VisualTestAppContext` which
+//! depends on the macOS Metal renderer for accurate screenshot capture.
+//!
//! ## How It Works
//!
-//! This tool uses direct texture capture - it renders the scene to a Metal texture
-//! and reads the pixels back directly. This approach:
+//! This tool uses `VisualTestAppContext` which combines:
+//! - Real Metal/compositor rendering for accurate screenshots
+//! - Deterministic task scheduling via TestDispatcher
+//! - Controllable time via `advance_clock` for testing time-based behaviors
+//!
+//! This approach:
//! - Does NOT require Screen Recording permission
//! - Does NOT require the window to be visible on screen
//! - Captures raw GPUI output without system window chrome
+//! - Is fully deterministic - tooltips, animations, etc. work reliably
//!
//! ## Usage
//!
@@ -24,88 +35,198 @@
//! UPDATE_BASELINE - Set to update baseline images instead of comparing
//! VISUAL_TEST_OUTPUT_DIR - Directory to save test output (default: target/visual_tests)
-use anyhow::{Context, Result};
-use gpui::{
- App, AppContext as _, Application, Bounds, Pixels, Size, Window, WindowBounds, WindowHandle,
- WindowOptions, point, px,
-};
-use image::RgbaImage;
-use project_panel::ProjectPanel;
+// Stub main for non-macOS platforms
+#[cfg(not(target_os = "macos"))]
+fn main() {
+ eprintln!("Visual test runner is only supported on macOS");
+ std::process::exit(1);
+}
-use std::any::Any;
-use std::path::{Path, PathBuf};
-use std::rc::Rc;
-use std::sync::Arc;
-use workspace::{AppState, Workspace};
+// All macOS-specific imports grouped together
+#[cfg(target_os = "macos")]
+use {
+ acp_thread::{AgentConnection, StubAgentConnection},
+ agent_client_protocol as acp,
+ agent_servers::{AgentServer, AgentServerDelegate},
+ anyhow::{Context as _, Result},
+ assets::Assets,
+ gpui::{
+ App, AppContext as _, Bounds, KeyBinding, Modifiers, SharedString, VisualTestAppContext,
+ WindowBounds, WindowHandle, WindowOptions, point, px, size,
+ },
+ image::RgbaImage,
+ project_panel::ProjectPanel,
+ settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore},
+ std::{
+ any::Any,
+ path::{Path, PathBuf},
+ rc::Rc,
+ sync::Arc,
+ time::Duration,
+ },
+ workspace::{AppState, Workspace},
+};
-use acp_thread::{AgentConnection, StubAgentConnection};
-use agent_client_protocol as acp;
-use agent_servers::{AgentServer, AgentServerDelegate};
-use gpui::SharedString;
+// All macOS-specific constants grouped together
+#[cfg(target_os = "macos")]
+mod constants {
+ use std::time::Duration;
-/// Baseline images are stored relative to this file
-const BASELINE_DIR: &str = "crates/zed/test_fixtures/visual_tests";
+ /// Baseline images are stored relative to this file
+ pub const BASELINE_DIR: &str = "crates/zed/test_fixtures/visual_tests";
-/// Embedded test image (Zed app icon) for visual tests.
-const EMBEDDED_TEST_IMAGE: &[u8] = include_bytes!("../resources/app-icon.png");
+ /// Embedded test image (Zed app icon) for visual tests.
+ pub const EMBEDDED_TEST_IMAGE: &[u8] = include_bytes!("../resources/app-icon.png");
-/// Threshold for image comparison (0.0 to 1.0)
-/// Images must match at least this percentage to pass
-const MATCH_THRESHOLD: f64 = 0.99;
+ /// Threshold for image comparison (0.0 to 1.0)
+ /// Images must match at least this percentage to pass
+ pub const MATCH_THRESHOLD: f64 = 0.99;
-/// Window size for workspace tests (project panel, editor)
-fn workspace_window_size() -> Size<Pixels> {
- Size {
- width: px(1280.0),
- height: px(800.0),
- }
+ /// Tooltip show delay - must match TOOLTIP_SHOW_DELAY in gpui/src/elements/div.rs
+ pub const TOOLTIP_SHOW_DELAY: Duration = Duration::from_millis(500);
}
-/// Window size for agent panel tests
-fn agent_panel_window_size() -> Size<Pixels> {
- Size {
- width: px(500.0),
- height: px(900.0),
+#[cfg(target_os = "macos")]
+use constants::*;
+
+#[cfg(target_os = "macos")]
+fn main() {
+ // Set ZED_STATELESS early to prevent file system access to real config directories
+ // This must be done before any code accesses zed_env_vars::ZED_STATELESS
+ // SAFETY: We're at the start of main(), before any threads are spawned
+ unsafe {
+ std::env::set_var("ZED_STATELESS", "1");
}
-}
-/// Helper struct for setting up test workspaces
-struct TestWorkspace {
- window: WindowHandle<Workspace>,
+ env_logger::builder()
+ .filter_level(log::LevelFilter::Info)
+ .init();
+
+ let update_baseline = std::env::var("UPDATE_BASELINE").is_ok();
+
+ // Create a temporary directory for test files
+ // Canonicalize the path to resolve symlinks (on macOS, /var -> /private/var)
+ // which prevents "path does not exist" errors during worktree scanning
+ // Use keep() to prevent auto-cleanup - background worktree tasks may still be running
+ // when tests complete, so we let the OS clean up temp directories on process exit
+ let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
+ let temp_path = temp_dir.keep();
+ let canonical_temp = temp_path
+ .canonicalize()
+ .expect("Failed to canonicalize temp directory");
+ let project_path = canonical_temp.join("project");
+ std::fs::create_dir_all(&project_path).expect("Failed to create project directory");
+
+ // Create test files in the real filesystem
+ create_test_files(&project_path);
+
+ let test_result = std::panic::catch_unwind(|| run_visual_tests(project_path, update_baseline));
+
+ // Note: We don't delete temp_path here because background worktree tasks may still
+ // be running. The directory will be cleaned up when the process exits or by the OS.
+
+ match test_result {
+ Ok(Ok(())) => {}
+ Ok(Err(e)) => {
+ eprintln!("Visual tests failed: {}", e);
+ std::process::exit(1);
+ }
+ Err(_) => {
+ eprintln!("Visual tests panicked");
+ std::process::exit(1);
+ }
+ }
}
-impl TestWorkspace {
- async fn new(
- app_state: Arc<AppState>,
- window_size: Size<Pixels>,
- project_path: &Path,
- cx: &mut gpui::AsyncApp,
- ) -> Result<Self> {
- let project = cx.update(|cx| {
- project::Project::local(
- app_state.client.clone(),
- app_state.node_runtime.clone(),
- app_state.user_store.clone(),
- app_state.languages.clone(),
- app_state.fs.clone(),
- None,
- false,
- cx,
- )
+#[cfg(target_os = "macos")]
+fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()> {
+ // Create the visual test context with deterministic task scheduling
+ // Use real Assets so that SVG icons render properly
+ let mut cx = VisualTestAppContext::with_asset_source(Arc::new(Assets));
+
+ // Initialize settings store first (required by theme and other subsystems)
+ // and disable telemetry to prevent HTTP errors from FakeHttpClient
+ cx.update(|cx| {
+ let mut settings_store = SettingsStore::test(cx);
+ settings_store.update_user_settings(cx, |settings| {
+ settings.telemetry = Some(settings::TelemetrySettingsContent {
+ diagnostics: Some(false),
+ metrics: Some(false),
+ });
});
+ cx.set_global(settings_store);
+ });
- project
- .update(cx, |project, cx| {
- project.find_or_create_worktree(project_path, true, cx)
- })
- .await?;
+ // Create AppState using the test initialization
+ let app_state = cx.update(|cx| init_app_state(cx));
+
+ // Initialize all Zed subsystems
+ cx.update(|cx| {
+ gpui_tokio::init(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
+ client::init(&app_state.client, cx);
+ audio::init(cx);
+ workspace::init(app_state.clone(), cx);
+ release_channel::init(semver::Version::new(0, 0, 0), cx);
+ command_palette::init(cx);
+ editor::init(cx);
+ call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+ title_bar::init(cx);
+ project_panel::init(cx);
+ outline_panel::init(cx);
+ terminal_view::init(cx);
+ image_viewer::init(cx);
+ search::init(cx);
+ prompt_store::init(cx);
+ language_model::init(app_state.client.clone(), cx);
+ language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
+ git_ui::init(cx);
+
+ // Load default keymaps so tooltips can show keybindings like "f9" for ToggleBreakpoint
+ // We load a minimal set of editor keybindings needed for visual tests
+ cx.bind_keys([KeyBinding::new(
+ "f9",
+ editor::actions::ToggleBreakpoint,
+ Some("Editor"),
+ )]);
+
+ // Disable agent notifications during visual tests to avoid popup windows
+ agent_settings::AgentSettings::override_global(
+ agent_settings::AgentSettings {
+ notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
+ play_sound_when_agent_done: false,
+ ..agent_settings::AgentSettings::get_global(cx).clone()
+ },
+ cx,
+ );
+ });
+
+ // Run until all initialization tasks complete
+ cx.run_until_parked();
- let bounds = Bounds {
- origin: point(px(0.0), px(0.0)),
- size: window_size,
- };
+ // Open workspace window
+ let window_size = size(px(1280.0), px(800.0));
+ let bounds = Bounds {
+ origin: point(px(0.0), px(0.0)),
+ size: window_size,
+ };
+
+ // Create a project for the workspace
+ let project = cx.update(|cx| {
+ project::Project::local(
+ app_state.client.clone(),
+ app_state.node_runtime.clone(),
+ app_state.user_store.clone(),
+ app_state.languages.clone(),
+ app_state.fs.clone(),
+ None,
+ false,
+ cx,
+ )
+ });
- let window: WindowHandle<Workspace> = cx.update(|cx| {
+ let workspace_window: WindowHandle<Workspace> = cx
+ .update(|cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
@@ -119,276 +240,287 @@ impl TestWorkspace {
})
},
)
- })?;
+ })
+ .context("Failed to open workspace window")?;
- cx.background_executor()
- .timer(std::time::Duration::from_millis(100))
- .await;
+ cx.run_until_parked();
- Ok(Self { window })
- }
-}
+ // Add the test project as a worktree
+ let add_worktree_task = workspace_window
+ .update(&mut cx, |workspace, _window, cx| {
+ let project = workspace.project().clone();
+ project.update(cx, |project, cx| {
+ project.find_or_create_worktree(&project_path, true, cx)
+ })
+ })
+ .context("Failed to start adding worktree")?;
+
+ // Use block_test to wait for the worktree task
+ // block_test runs both foreground and background tasks, which is needed because
+ // worktree creation spawns foreground tasks via cx.spawn
+ // Allow parking since filesystem operations happen outside the test dispatcher
+ cx.background_executor.allow_parking();
+ let worktree_result = cx.background_executor.block_test(add_worktree_task);
+ cx.background_executor.forbid_parking();
+ worktree_result.context("Failed to add worktree")?;
+
+ cx.run_until_parked();
+
+ // Create and add the project panel
+ let (weak_workspace, async_window_cx) = workspace_window
+ .update(&mut cx, |workspace, window, cx| {
+ (workspace.weak_handle(), window.to_async(cx))
+ })
+ .context("Failed to get workspace handle")?;
+
+ cx.background_executor.allow_parking();
+ let panel = cx
+ .background_executor
+ .block_test(ProjectPanel::load(weak_workspace, async_window_cx))
+ .context("Failed to load project panel")?;
+ cx.background_executor.forbid_parking();
+
+ workspace_window
+ .update(&mut cx, |workspace, window, cx| {
+ workspace.add_panel(panel, window, cx);
+ })
+ .ok();
-async fn setup_project_panel(
- workspace: &TestWorkspace,
- cx: &mut gpui::AsyncApp,
-) -> Result<gpui::Entity<ProjectPanel>> {
- let panel_task = workspace.window.update(cx, |_workspace, window, cx| {
- let weak_workspace = cx.weak_entity();
- let async_window_cx = window.to_async(cx);
- window.spawn(cx, async move |_cx| {
- ProjectPanel::load(weak_workspace, async_window_cx).await
+ cx.run_until_parked();
+
+ // Open the project panel
+ workspace_window
+ .update(&mut cx, |workspace, window, cx| {
+ workspace.open_panel::<ProjectPanel>(window, cx);
})
- })?;
+ .ok();
+
+ cx.run_until_parked();
+
+ // Open main.rs in the editor
+ let open_file_task = workspace_window
+ .update(&mut cx, |workspace, window, cx| {
+ let worktree = workspace.project().read(cx).worktrees(cx).next();
+ if let Some(worktree) = worktree {
+ let worktree_id = worktree.read(cx).id();
+ let rel_path: std::sync::Arc<util::rel_path::RelPath> =
+ util::rel_path::rel_path("src/main.rs").into();
+ let project_path: project::ProjectPath = (worktree_id, rel_path).into();
+ Some(workspace.open_path(project_path, None, true, window, cx))
+ } else {
+ None
+ }
+ })
+ .ok()
+ .flatten();
- let panel = panel_task.await?;
+ if let Some(task) = open_file_task {
+ cx.background_executor.allow_parking();
+ let block_result = cx.background_executor.block_test(task);
+ cx.background_executor.forbid_parking();
+ if let Ok(item) = block_result {
+ workspace_window
+ .update(&mut cx, |workspace, window, cx| {
+ let pane = workspace.active_pane().clone();
+ pane.update(cx, |pane, cx| {
+ if let Some(index) = pane.index_for_item(item.as_ref()) {
+ pane.activate_item(index, true, true, window, cx);
+ }
+ });
+ })
+ .ok();
+ }
+ }
- workspace.window.update(cx, |ws, window, cx| {
- ws.add_panel(panel.clone(), window, cx);
- ws.open_panel::<ProjectPanel>(window, cx);
- })?;
+ cx.run_until_parked();
- cx.background_executor()
- .timer(std::time::Duration::from_millis(100))
- .await;
+ // Request a window refresh
+ cx.update_window(workspace_window.into(), |_, window, _cx| {
+ window.refresh();
+ })
+ .ok();
- Ok(panel)
-}
+ cx.run_until_parked();
-async fn open_file(
- workspace: &TestWorkspace,
- relative_path: &str,
- cx: &mut gpui::AsyncApp,
-) -> Result<()> {
- let open_file_task = workspace.window.update(cx, |ws, window, cx| {
- let worktree = ws.project().read(cx).worktrees(cx).next();
- if let Some(worktree) = worktree {
- let worktree_id = worktree.read(cx).id();
- let rel_path: std::sync::Arc<util::rel_path::RelPath> =
- util::rel_path::rel_path(relative_path).into();
- let project_path: project::ProjectPath = (worktree_id, rel_path).into();
- Some(ws.open_path(project_path, None, true, window, cx))
- } else {
- None
- }
- })?;
+ // Track test results
+ let mut passed = 0;
+ let mut failed = 0;
+ let mut updated = 0;
- if let Some(task) = open_file_task {
- let item = task.await?;
- workspace.window.update(cx, |ws, window, cx| {
- let pane = ws.active_pane().clone();
- pane.update(cx, |pane, cx| {
- if let Some(index) = pane.index_for_item(item.as_ref()) {
- pane.activate_item(index, true, true, window, cx);
- }
- });
- })?;
+ // Run Test 1: Project Panel (with project panel visible)
+ println!("\n--- Test 1: project_panel ---");
+ match run_visual_test(
+ "project_panel",
+ workspace_window.into(),
+ &mut cx,
+ update_baseline,
+ ) {
+ Ok(TestResult::Passed) => {
+ println!("✓ project_panel: PASSED");
+ passed += 1;
+ }
+ Ok(TestResult::BaselineUpdated(_)) => {
+ println!("✓ project_panel: Baseline updated");
+ updated += 1;
+ }
+ Err(e) => {
+ eprintln!("✗ project_panel: FAILED - {}", e);
+ failed += 1;
+ }
}
- cx.background_executor()
- .timer(std::time::Duration::from_millis(100))
- .await;
+ // Run Test 2: Workspace with Editor
+ println!("\n--- Test 2: workspace_with_editor ---");
- Ok(())
-}
-
-fn main() {
- env_logger::builder()
- .filter_level(log::LevelFilter::Info)
- .init();
+ // Close project panel for this test
+ workspace_window
+ .update(&mut cx, |workspace, window, cx| {
+ workspace.close_panel::<ProjectPanel>(window, cx);
+ })
+ .ok();
- let update_baseline = std::env::var("UPDATE_BASELINE").is_ok();
+ cx.run_until_parked();
- // Create a temporary directory for test files
- let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
- let project_path = temp_dir.path().join("project");
- std::fs::create_dir_all(&project_path).expect("Failed to create project directory");
+ match run_visual_test(
+ "workspace_with_editor",
+ workspace_window.into(),
+ &mut cx,
+ update_baseline,
+ ) {
+ Ok(TestResult::Passed) => {
+ println!("✓ workspace_with_editor: PASSED");
+ passed += 1;
+ }
+ Ok(TestResult::BaselineUpdated(_)) => {
+ println!("✓ workspace_with_editor: Baseline updated");
+ updated += 1;
+ }
+ Err(e) => {
+ eprintln!("✗ workspace_with_editor: FAILED - {}", e);
+ failed += 1;
+ }
+ }
- // Create test files in the real filesystem
- create_test_files(&project_path);
+ // Run Test 3: Agent Thread View tests
+ println!("\n--- Test 3: agent_thread_with_image (collapsed + expanded) ---");
+ match run_agent_thread_view_test(app_state.clone(), &mut cx, update_baseline) {
+ Ok(TestResult::Passed) => {
+ println!("✓ agent_thread_with_image (collapsed + expanded): PASSED");
+ passed += 1;
+ }
+ Ok(TestResult::BaselineUpdated(_)) => {
+ println!("✓ agent_thread_with_image: Baselines updated (collapsed + expanded)");
+ updated += 1;
+ }
+ Err(e) => {
+ eprintln!("✗ agent_thread_with_image: FAILED - {}", e);
+ failed += 1;
+ }
+ }
- let test_result = std::panic::catch_unwind(|| {
- let project_path = project_path;
- Application::new()
- .with_assets(assets::Assets)
- .run(move |cx| {
- // Load embedded fonts (Zed Sans and Zed Mono)
- assets::Assets.load_fonts(cx).unwrap();
-
- // Initialize settings store with real default settings (not test settings)
- // Test settings use Courier font, but we want the real Zed fonts for visual tests
- settings::init(cx);
-
- // Create AppState using the production-like initialization
- let app_state = init_app_state(cx);
-
- // Initialize all Zed subsystems
- gpui_tokio::init(cx);
- theme::init(theme::LoadThemes::JustBase, cx);
- client::init(&app_state.client, cx);
- audio::init(cx);
- workspace::init(app_state.clone(), cx);
- release_channel::init(semver::Version::new(0, 0, 0), cx);
- command_palette::init(cx);
- editor::init(cx);
- call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
- title_bar::init(cx);
- project_panel::init(cx);
- outline_panel::init(cx);
- terminal_view::init(cx);
- image_viewer::init(cx);
- search::init(cx);
- prompt_store::init(cx);
- language_model::init(app_state.client.clone(), cx);
- language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
-
- // Clone app_state for the async block
- let app_state_for_tests = app_state.clone();
-
- // Spawn async task to set up the UI and capture screenshot
- cx.spawn(async move |mut cx| {
- let project_path_clone = project_path.clone();
-
- // Create the test workspace
- let workspace = match TestWorkspace::new(
- app_state_for_tests.clone(),
- workspace_window_size(),
- &project_path_clone,
- &mut cx,
- )
- .await
- {
- Ok(ws) => ws,
- Err(e) => {
- log::error!("Failed to create workspace: {}", e);
- cx.update(|cx| cx.quit());
- std::process::exit(1);
- }
- };
-
- // Set up project panel
- if let Err(e) = setup_project_panel(&workspace, &mut cx).await {
- log::error!("Failed to setup project panel: {}", e);
- cx.update(|cx| cx.quit());
- std::process::exit(1);
- }
-
- // Open main.rs in the editor
- if let Err(e) = open_file(&workspace, "src/main.rs", &mut cx).await {
- log::error!("Failed to open file: {}", e);
- cx.update(|cx| cx.quit());
- std::process::exit(1);
- }
-
- // Request a window refresh to ensure all pending effects are processed
- cx.refresh();
- cx.background_executor()
- .timer(std::time::Duration::from_millis(500))
- .await;
-
- // Track if any test failed
- let mut any_failed = false;
-
- // Run Test 1: Project Panel (with project panel visible)
- if run_visual_test(
- "project_panel",
- workspace.window.into(),
- &mut cx,
- update_baseline,
- )
- .await
- .is_err()
- {
- any_failed = true;
- }
-
- // Close the project panel for the second test
- cx.update(|cx| {
- workspace
- .window
- .update(cx, |ws, window, cx| {
- ws.close_panel::<ProjectPanel>(window, cx);
- })
- .ok();
- });
+ // Run Test 4: Breakpoint Hover visual tests
+ println!("\n--- Test 4: breakpoint_hover (3 variants) ---");
+ match run_breakpoint_hover_visual_tests(app_state.clone(), &mut cx, update_baseline) {
+ Ok(TestResult::Passed) => {
+ println!("✓ breakpoint_hover: PASSED");
+ passed += 1;
+ }
+ Ok(TestResult::BaselineUpdated(_)) => {
+ println!("✓ breakpoint_hover: Baselines updated");
+ updated += 1;
+ }
+ Err(e) => {
+ eprintln!("✗ breakpoint_hover: FAILED - {}", e);
+ failed += 1;
+ }
+ }
- // Refresh and wait for panel to close
- cx.refresh();
- cx.background_executor()
- .timer(std::time::Duration::from_millis(100))
- .await;
-
- // Run Test 2: Workspace with Editor (without project panel)
- if run_visual_test(
- "workspace_with_editor",
- workspace.window.into(),
- &mut cx,
- update_baseline,
- )
- .await
- .is_err()
- {
- any_failed = true;
- }
-
- // Run Test 3: Agent Thread View with Image (collapsed and expanded)
- if run_agent_thread_view_test(
- app_state_for_tests.clone(),
- &mut cx,
- update_baseline,
- )
- .await
- .is_err()
- {
- any_failed = true;
- }
-
- if any_failed {
- cx.update(|cx| cx.quit());
- std::process::exit(1);
- }
-
- cx.update(|cx| cx.quit());
- })
- .detach();
+ // Clean up the main workspace's worktree to stop background scanning tasks
+ // This prevents "root path could not be canonicalized" errors when main() drops temp_dir
+ workspace_window
+ .update(&mut cx, |workspace, _window, cx| {
+ let project = workspace.project().clone();
+ project.update(cx, |project, cx| {
+ let worktree_ids: Vec<_> =
+ project.worktrees(cx).map(|wt| wt.read(cx).id()).collect();
+ for id in worktree_ids {
+ project.remove_worktree(id, cx);
+ }
});
+ })
+ .ok();
+
+ cx.run_until_parked();
+
+ // Close the main window
+ let _ = cx.update_window(workspace_window.into(), |_, window, _cx| {
+ window.remove_window();
});
- // Keep temp_dir alive until we're done
- drop(temp_dir);
+ // Run until all cleanup tasks complete
+ cx.run_until_parked();
+
+ // Give background tasks time to finish, including scrollbar hide timers (1 second)
+ for _ in 0..15 {
+ cx.advance_clock(Duration::from_millis(100));
+ cx.run_until_parked();
+ }
+
+ // Print summary
+ println!("\n=== Test Summary ===");
+ println!("Passed: {}", passed);
+ println!("Failed: {}", failed);
+ if updated > 0 {
+ println!("Baselines Updated: {}", updated);
+ }
- if test_result.is_err() {
- std::process::exit(1);
+ if failed > 0 {
+ eprintln!("\n=== Visual Tests FAILED ===");
+ Err(anyhow::anyhow!("{} tests failed", failed))
+ } else {
+ println!("\n=== All Visual Tests PASSED ===");
+ Ok(())
}
}
+#[cfg(target_os = "macos")]
enum TestResult {
Passed,
BaselineUpdated(PathBuf),
}
-async fn run_visual_test(
+#[cfg(target_os = "macos")]
+fn run_visual_test(
test_name: &str,
window: gpui::AnyWindowHandle,
- cx: &mut gpui::AsyncApp,
+ cx: &mut VisualTestAppContext,
update_baseline: bool,
) -> Result<TestResult> {
- // Capture the screenshot using direct texture capture (no ScreenCaptureKit needed)
- let screenshot = cx.update(|cx| capture_screenshot(window, cx))?;
+ // Ensure all pending work is done
+ cx.run_until_parked();
+
+ // Refresh the window to ensure it's fully rendered
+ cx.update_window(window, |_, window, _cx| {
+ window.refresh();
+ })?;
+
+ cx.run_until_parked();
+
+ // Capture the screenshot using direct texture capture
+ let screenshot = cx.capture_screenshot(window)?;
// Get paths
let baseline_path = get_baseline_path(test_name);
let output_dir = std::env::var("VISUAL_TEST_OUTPUT_DIR")
.unwrap_or_else(|_| "target/visual_tests".to_string());
- let actual_path = Path::new(&output_dir).join(format!("{}.png", test_name));
+ let output_path = PathBuf::from(&output_dir).join(format!("{}.png", test_name));
- // Create output directory
- if let Some(parent) = actual_path.parent() {
- std::fs::create_dir_all(parent)?;
- }
+ // Ensure output directory exists
+ std::fs::create_dir_all(&output_dir)?;
- // Save the actual screenshot
- screenshot.save(&actual_path)?;
+ // Always save the current screenshot
+ screenshot.save(&output_path)?;
+ println!(" Screenshot saved to: {}", output_path.display());
if update_baseline {
// Update the baseline
@@ -396,181 +528,177 @@ async fn run_visual_test(
std::fs::create_dir_all(parent)?;
}
screenshot.save(&baseline_path)?;
+ println!(" Baseline updated: {}", baseline_path.display());
return Ok(TestResult::BaselineUpdated(baseline_path));
}
- // Compare against baseline
+ // Compare with baseline
if !baseline_path.exists() {
return Err(anyhow::anyhow!(
- "Baseline image not found: {}\n\
- Run with UPDATE_BASELINE=1 to create it.",
+ "Baseline not found: {}. Run with UPDATE_BASELINE=1 to create it.",
baseline_path.display()
));
}
- let baseline = image::open(&baseline_path)
- .context("Failed to load baseline image")?
- .to_rgba8();
+ let baseline = image::open(&baseline_path)?.to_rgba8();
+ let comparison = compare_images(&screenshot, &baseline);
- let comparison = compare_images(&baseline, &screenshot);
+ println!(
+ " Match: {:.2}% ({} different pixels)",
+ comparison.match_percentage * 100.0,
+ comparison.diff_pixel_count
+ );
if comparison.match_percentage >= MATCH_THRESHOLD {
Ok(TestResult::Passed)
} else {
- // Save the diff image for debugging
- if let Some(diff_image) = comparison.diff_image {
- let diff_path = Path::new(&output_dir).join(format!("{}_diff.png", test_name));
- diff_image.save(&diff_path)?;
- }
+ // Save diff image
+ let diff_path = PathBuf::from(&output_dir).join(format!("{}_diff.png", test_name));
+ comparison.diff_image.save(&diff_path)?;
+ println!(" Diff image saved to: {}", diff_path.display());
Err(anyhow::anyhow!(
- "Screenshot does not match baseline.\n\
- Match: {:.2}% (threshold: {:.2}%)\n\
- Actual: {}\n\
- Baseline: {}\n\
- \n\
- Run with UPDATE_BASELINE=1 to update the baseline if this change is intentional.",
+ "Image mismatch: {:.2}% match (threshold: {:.2}%)",
comparison.match_percentage * 100.0,
- MATCH_THRESHOLD * 100.0,
- actual_path.display(),
- baseline_path.display()
+ MATCH_THRESHOLD * 100.0
))
}
}
+#[cfg(target_os = "macos")]
fn get_baseline_path(test_name: &str) -> PathBuf {
- // Find the workspace root by looking for Cargo.toml
- let mut path = std::env::current_dir().expect("Failed to get current directory");
- while !path.join("Cargo.toml").exists() || !path.join("crates").exists() {
- if !path.pop() {
- panic!("Could not find workspace root");
- }
- }
- path.join(BASELINE_DIR).join(format!("{}.png", test_name))
+ // Get the workspace root (where Cargo.toml is)
+ let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
+ let workspace_root = PathBuf::from(manifest_dir)
+ .parent()
+ .and_then(|p| p.parent())
+ .map(|p| p.to_path_buf())
+ .unwrap_or_else(|| PathBuf::from("."));
+
+ workspace_root
+ .join(BASELINE_DIR)
+ .join(format!("{}.png", test_name))
}
+#[cfg(target_os = "macos")]
struct ImageComparison {
match_percentage: f64,
- diff_image: Option<RgbaImage>,
+ diff_image: RgbaImage,
+ diff_pixel_count: u32,
+ #[allow(dead_code)]
+ total_pixels: u32,
}
-fn compare_images(baseline: &RgbaImage, actual: &RgbaImage) -> ImageComparison {
- // Check dimensions
- if baseline.dimensions() != actual.dimensions() {
- return ImageComparison {
- match_percentage: 0.0,
- diff_image: None,
- };
- }
+#[cfg(target_os = "macos")]
+fn compare_images(actual: &RgbaImage, expected: &RgbaImage) -> ImageComparison {
+ let width = actual.width().max(expected.width());
+ let height = actual.height().max(expected.height());
+ let total_pixels = width * height;
- let (width, height) = baseline.dimensions();
- let total_pixels = width as u64 * height as u64;
- let mut diff_count: u64 = 0;
let mut diff_image = RgbaImage::new(width, height);
+ let mut matching_pixels = 0u32;
for y in 0..height {
for x in 0..width {
- let baseline_pixel = baseline.get_pixel(x, y);
- let actual_pixel = actual.get_pixel(x, y);
-
- if pixels_match(baseline_pixel, actual_pixel) {
- // Matching pixel - show as dimmed version of actual
- diff_image.put_pixel(
- x,
- y,
- image::Rgba([
- actual_pixel[0] / 3,
- actual_pixel[1] / 3,
- actual_pixel[2] / 3,
- 255,
- ]),
- );
+ let actual_pixel = if x < actual.width() && y < actual.height() {
+ *actual.get_pixel(x, y)
+ } else {
+ image::Rgba([0, 0, 0, 0])
+ };
+
+ let expected_pixel = if x < expected.width() && y < expected.height() {
+ *expected.get_pixel(x, y)
+ } else {
+ image::Rgba([0, 0, 0, 0])
+ };
+
+ if pixels_are_similar(&actual_pixel, &expected_pixel) {
+ matching_pixels += 1;
+ // Semi-transparent green for matching pixels
+ diff_image.put_pixel(x, y, image::Rgba([0, 255, 0, 64]));
} else {
- diff_count += 1;
- // Different pixel - highlight in red
+ // Bright red for differing pixels
diff_image.put_pixel(x, y, image::Rgba([255, 0, 0, 255]));
}
}
}
- let match_percentage = if total_pixels > 0 {
- (total_pixels - diff_count) as f64 / total_pixels as f64
- } else {
- 1.0
- };
+ let match_percentage = matching_pixels as f64 / total_pixels as f64;
+ let diff_pixel_count = total_pixels - matching_pixels;
ImageComparison {
match_percentage,
- diff_image: Some(diff_image),
+ diff_image,
+ diff_pixel_count,
+ total_pixels,
}
}
-fn pixels_match(a: &image::Rgba<u8>, b: &image::Rgba<u8>) -> bool {
- a == b
-}
-
-fn capture_screenshot(window: gpui::AnyWindowHandle, cx: &mut gpui::App) -> Result<RgbaImage> {
- // Use direct texture capture - renders the scene to a texture and reads pixels back.
- // This does not require the window to be visible on screen.
- let screenshot = cx.update_window(window, |_view, window: &mut Window, _cx| {
- window.render_to_image()
- })??;
-
- Ok(screenshot)
+#[cfg(target_os = "macos")]
+fn pixels_are_similar(a: &image::Rgba<u8>, b: &image::Rgba<u8>) -> bool {
+ const TOLERANCE: i16 = 2;
+ (a.0[0] as i16 - b.0[0] as i16).abs() <= TOLERANCE
+ && (a.0[1] as i16 - b.0[1] as i16).abs() <= TOLERANCE
+ && (a.0[2] as i16 - b.0[2] as i16).abs() <= TOLERANCE
+ && (a.0[3] as i16 - b.0[3] as i16).abs() <= TOLERANCE
}
-/// Create test files in a real filesystem directory
+#[cfg(target_os = "macos")]
fn create_test_files(project_path: &Path) {
+ // Create src directory
let src_dir = project_path.join("src");
std::fs::create_dir_all(&src_dir).expect("Failed to create src directory");
- std::fs::write(src_dir.join("main.rs"), MAIN_RS_CONTENT).expect("Failed to write main.rs");
-
- std::fs::write(src_dir.join("lib.rs"), LIB_RS_CONTENT).expect("Failed to write lib.rs");
+ // Create main.rs
+ let main_rs = r#"fn main() {
+ println!("Hello, world!");
- std::fs::write(src_dir.join("utils.rs"), UTILS_RS_CONTENT).expect("Failed to write utils.rs");
+ let x = 42;
+ let y = x * 2;
- std::fs::write(project_path.join("Cargo.toml"), CARGO_TOML_CONTENT)
- .expect("Failed to write Cargo.toml");
+ if y > 50 {
+ println!("y is greater than 50");
+ } else {
+ println!("y is not greater than 50");
+ }
- std::fs::write(project_path.join("README.md"), README_MD_CONTENT)
- .expect("Failed to write README.md");
+ for i in 0..10 {
+ println!("i = {}", i);
+ }
}
-const MAIN_RS_CONTENT: &str = r#"fn main() {
- println!("Hello, world!");
-
- let message = greet("Zed");
- println!("{}", message);
+fn helper_function(a: i32, b: i32) -> i32 {
+ a + b
}
-fn greet(name: &str) -> String {
- format!("Welcome to {}, the editor of the future!", name)
+struct MyStruct {
+ field1: String,
+ field2: i32,
}
-#[cfg(test)]
-mod tests {
- use super::*;
+impl MyStruct {
+ fn new(name: &str, value: i32) -> Self {
+ Self {
+ field1: name.to_string(),
+ field2: value,
+ }
+ }
- #[test]
- fn test_greet() {
- assert_eq!(greet("World"), "Welcome to World, the editor of the future!");
+ fn get_value(&self) -> i32 {
+ self.field2
}
}
"#;
+ std::fs::write(src_dir.join("main.rs"), main_rs).expect("Failed to write main.rs");
-const LIB_RS_CONTENT: &str = r#"//! A sample library for visual testing.
+ // Create lib.rs
+ let lib_rs = r#"//! A sample library for visual testing
pub mod utils;
-/// Adds two numbers together.
-pub fn add(a: i32, b: i32) -> i32 {
- a + b
-}
-
-/// Subtracts the second number from the first.
-pub fn subtract(a: i32, b: i32) -> i32 {
- a - b
+/// A public function in the library
+pub fn library_function() -> String {
+ "Hello from lib".to_string()
}
#[cfg(test)]