1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

Add Harness::spawn_eframe_app (#8120)

This lets you start up the test app from within the test itself, which
can be very useful when you have a specific test scenario set up that
you need to debug.

### Related
* Previous attempt: https://github.com/emilk/egui/pull/5418

### macOS
On macOS, you may only run UIs on the main loop, so you need a few
additional steps. Not ideal, but works!


```diff
diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml
index f9a153268..4e0cc14ee 100644
--- a/crates/egui_demo_app/Cargo.toml
+++ b/crates/egui_demo_app/Cargo.toml
@@ -84,3 +84,7 @@ web-sys.workspace = true
 
 [dev-dependencies]
 egui_kittest = { workspace = true, features = ["eframe", "snapshot", "wgpu"] }
+
+[[test]]
+name = "test_demo_app"
+harness = false
diff --git a/crates/egui_demo_app/tests/test_demo_app.rs b/crates/egui_demo_app/tests/test_demo_app.rs
index e083c8455..7ad9ed516 100644
--- a/crates/egui_demo_app/tests/test_demo_app.rs
+++ b/crates/egui_demo_app/tests/test_demo_app.rs
@@ -4,7 +4,10 @@ use egui_demo_app::{Anchor, WrapApp};
 use egui_kittest::SnapshotResults;
 use egui_kittest::kittest::Queryable as _;
 
-#[test]
+fn main() {
+    test_demo_app();
+}
+
 fn test_demo_app() {
     let mut harness = egui_kittest::Harness::builder()
         .with_size(Vec2::new(900.0, 600.0))
@@ -73,5 +76,8 @@ fn test_demo_app() {
         harness.run_steps(4);
 
         results.add(harness.try_snapshot(anchor.to_string()));
+
+        harness.spawn_eframe_app();
+        break;
     }
 }
```
This commit is contained in:
Emil Ernerfeldt
2026-04-20 14:07:45 +02:00
committed by GitHub
parent d7e55b8381
commit 56aabda7b3
10 changed files with 192 additions and 26 deletions

View File

@@ -214,7 +214,7 @@ impl<'a, State> Harness<'a, State> {
#[track_caller]
pub fn new_eframe(builder: impl FnOnce(&mut eframe::CreationContext<'a>) -> State) -> Self
where
State: eframe::App,
State: eframe::App + 'static,
{
Self::builder().build_eframe(builder)
}
@@ -460,6 +460,11 @@ impl<'a, State> Harness<'a, State> {
&mut self.state
}
/// Consume the harness and return the state.
pub fn into_state(self) -> State {
self.state
}
/// Queue an event to be processed in the next frame.
pub fn event(&self, event: egui::Event) {
self.queued_events.lock().push(EventType::Event(event));
@@ -681,6 +686,110 @@ impl<'a, State> Harness<'a, State> {
queue: &self.queued_events,
}
}
/// Spawn a real native eframe window running this harness's app, reusing its [`egui::Context`].
///
/// Blocks until the window is closed.
///
/// Useful for interactively debugging a failing test: add a call to this before the failing
/// assertion to poke at the UI yourself.
///
/// # macOS: must be called on the main thread
/// `AppKit` requires UI work to happen on the main thread, but by default cargo's test harness
/// runs each test on a spawned worker thread, so this function will panic on macOS unless
/// you opt out of the default harness.
///
/// To fix this, disable the default libtest harness for your test target and run tests on
/// the main thread yourself. In `Cargo.toml`:
///
/// ```toml
/// [[test]]
/// name = "your_test"
/// harness = false
/// ```
///
/// Then write a `fn main()` in the test file that invokes your test directly.
///
/// See also: <https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-harness-field>
#[cfg(feature = "eframe")]
#[deprecated = "Only for debugging, don't commit this."]
pub fn spawn_eframe_app(self)
where
'a: 'static,
State: 'static,
{
#[cfg(target_os = "macos")]
{
// AppKit requires UI work to happen on the main thread, but by default cargo's
// test harness runs each test on a spawned worker thread.
#[expect(unsafe_code)]
// SAFETY: `pthread_main_np` is a thread-safe libc query with no arguments.
let is_main_thread = unsafe {
unsafe extern "C" {
fn pthread_main_np() -> std::ffi::c_int;
}
pthread_main_np() != 0
};
assert!(
is_main_thread,
"spawn_eframe_app must be called on the main thread on macOS, \
but the default `cargo test` harness runs each test on a worker thread.\n\
\n\
To fix this, disable the default libtest harness for your test target and run \
tests on the main thread yourself. In Cargo.toml:\n\
\n\
[[test]]\n\
name = \"your_test\"\n\
harness = false\n\
\n\
Then write a `fn main()` in the test file that invokes your test directly.\n\
\n\
See: https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-harness-field"
);
}
struct UiApp {
f: Box<dyn FnMut(&mut egui::Ui)>,
}
impl eframe::App for UiApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
(self.f)(ui);
}
}
struct UiStateApp<State> {
f: Box<dyn FnMut(&mut egui::Ui, &mut State)>,
state: State,
}
impl<State: 'static> eframe::App for UiStateApp<State> {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let Self { f, state } = self;
f(ui, state);
}
}
use crate::app_kind::AppKindEframe;
let Self {
ctx, state, app, ..
} = self;
let eframe_app: Box<dyn eframe::App> = match app {
AppKind::Ui(f) => Box::new(UiApp { f }),
AppKind::UiState(f) => Box::new(UiStateApp { f, state }),
AppKind::Eframe(AppKindEframe { take_app, .. }) => take_app(state),
};
eframe::run_native_ext(
"egui_kittest",
eframe::NativeOptions::default(),
Some(ctx),
Box::new(|_cc| Ok(eframe_app)),
)
.unwrap();
}
}
/// Utilities for stateless harnesses.