Add breakpoint hover visual tests with VisualTestPlatform (#46404)

Richard Feldman created

Implements visual tests for breakpoint hover states in the editor gutter
with three states:
1. **No hover** - just line numbers
2. **Hover with indicator** - blue circle appears next to the line
3. **Hover with tooltip** - shows 'Set breakpoint' with F9 keybinding

- Added `VisualTestPlatform` that combines real Mac rendering with
controllable `TestDispatcher` for deterministic task scheduling and time
control
- Added `VisualTestAppContext::with_asset_source()` to enable SVG icon
rendering in visual tests (uses real Assets instead of empty source)
- Added F9 keybinding for ToggleBreakpoint so tooltip shows the
keybinding
- Removed debug `eprintln!` statements from editor, element, svg, and
window

The `VisualTestPlatform` enables `advance_clock()` which is essential
for testing time-based behaviors like tooltip delays. Without
deterministic time control, tests for hover states and tooltips would be
flaky.

Release Notes:

- N/A

Change summary

crates/gpui/src/app/visual_test_context.rs | 174 ++++
crates/gpui/src/platform.rs                |   6 
crates/gpui/src/platform/visual_test.rs    | 255 ++++++
crates/zed/src/visual_test_runner.rs       | 936 +++++++++++++----------
4 files changed, 952 insertions(+), 419 deletions(-)

Detailed changes

crates/gpui/src/app/visual_test_context.rs 🔗

@@ -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());
+    }
+}

crates/gpui/src/platform.rs 🔗

@@ -20,6 +20,9 @@ mod blade;
 #[cfg(any(test, feature = "test-support"))]
 mod test;
 
+#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
+mod visual_test;
+
 #[cfg(target_os = "windows")]
 mod windows;
 
@@ -90,6 +93,9 @@ pub use linux::layer_shell;
 #[cfg(any(test, feature = "test-support"))]
 pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
 
+#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
+pub use visual_test::VisualTestPlatform;
+
 /// Returns a background executor for the current platform.
 pub fn background_executor() -> BackgroundExecutor {
     // For standalone background executor, use a dead liveness since there's no App.

crates/gpui/src/platform/visual_test.rs 🔗

@@ -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()>) {}
+}

crates/zed/src/visual_test_runner.rs 🔗

@@ -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)]