#![allow( clippy::unwrap_used, clippy::disallowed_methods, clippy::disallowed_types, clippy::clone_on_ref_ptr )] use std::sync::{Arc, Mutex}; use egui_kittest::{ExceededMaxStepsError, Harness, Plugin, TestResult}; type Log = Arc>>; #[derive(Default)] struct CountingPlugin { log: Log, } impl CountingPlugin { fn new() -> (Self, Log) { let log: Log = Arc::default(); (Self { log: log.clone() }, log) } fn push(&self, tag: &str) { self.log.lock().unwrap().push(tag.to_owned()); } } impl Plugin for CountingPlugin { fn before_run(&mut self, _h: &mut Harness<'_, S>) { self.push("before_run"); } fn after_run(&mut self, _h: &mut Harness<'_, S>, result: Result) { self.push(if result.is_ok() { "after_run:ok" } else { "after_run:err" }); } fn before_step(&mut self, _h: &mut Harness<'_, S>) { self.push("before_step"); } fn after_step(&mut self, _h: &mut Harness<'_, S>) { self.push("after_step"); } fn on_event(&mut self, _h: &mut Harness<'_, S>, _event: &egui::Event) { self.push("on_event"); } #[cfg(any(feature = "wgpu", feature = "snapshot"))] fn on_render(&mut self, _h: &mut Harness<'_, S>, _image: &image::RgbaImage) { self.push("on_render"); } #[cfg(feature = "snapshot")] fn on_snapshot( &mut self, _h: &mut Harness<'_, S>, name: &str, _image: &image::RgbaImage, result: &egui_kittest::SnapshotResult, ) { self.push(&format!( "on_snapshot:{}:{}", name, if result.is_ok() { "ok" } else { "err" } )); } fn on_test_result(&mut self, _h: &mut Harness<'_, S>, result: TestResult<'_>) { self.push(match result { TestResult::Pass => "on_test_result:pass", TestResult::Fail { .. } => "on_test_result:fail", }); } } /// Lifecycle ordering: a simple run+drop cycle fires the expected hooks in order. #[test] fn hooks_fire_in_expected_order() { let (plugin, log) = CountingPlugin::new(); let mut harness = Harness::builder().with_plugin(plugin).build_ui(|ui| { ui.label("hi"); }); harness.run(); drop(harness); let log = log.lock().unwrap().clone(); // Construction calls `run_ok()`, so the first batch of hooks fires during `new_ui`: // before_run, before_step, after_step, after_run // Then `harness.run()` fires another set. // Then Drop fires `on_test_result:pass`. assert_eq!(log.first().map(String::as_str), Some("before_run")); assert!(log.contains(&"before_step".to_owned())); assert!(log.contains(&"after_step".to_owned())); assert!(log.contains(&"after_run:ok".to_owned())); assert_eq!(log.last().map(String::as_str), Some("on_test_result:pass")); // Every before_step has a matching after_step. let befores = log.iter().filter(|s| s == &"before_step").count(); let afters = log.iter().filter(|s| s == &"after_step").count(); assert_eq!(befores, afters); } /// `on_event` fires per queued event. #[test] fn on_event_fires_per_event() { let (plugin, log) = CountingPlugin::new(); let mut harness = Harness::builder().with_plugin(plugin).build_ui(|ui| { ui.label("hi"); }); log.lock().unwrap().clear(); // drop construction-time hooks harness.event(egui::Event::PointerMoved(egui::pos2(10.0, 10.0))); harness.event(egui::Event::PointerMoved(egui::pos2(20.0, 20.0))); harness.step(); let log = log.lock().unwrap(); let events = log.iter().filter(|s| s == &"on_event").count(); assert_eq!(events, 2, "expected 2 on_event calls, got log: {log:?}"); } /// `advance_frame` does NOT fire `before_step`/`after_step`. #[test] fn advance_frame_skips_hooks() { struct DrivingPlugin { log: Log, drove: bool, } impl Plugin for DrivingPlugin { fn after_step(&mut self, h: &mut Harness<'_, S>) { self.log.lock().unwrap().push("after_step".into()); if !self.drove { self.drove = true; // Call advance_frame from inside a hook — must not recurse. h.advance_frame(); } } } let log: Log = Arc::default(); let mut harness = Harness::builder() .with_plugin(DrivingPlugin { log: log.clone(), drove: false, }) .build_ui(|ui| { ui.label("hi"); }); log.lock().unwrap().clear(); harness.step(); let log = log.lock().unwrap(); // Exactly one after_step from the user's step(), plus any from construction-time run_ok // (cleared above). advance_frame must NOT have produced another after_step. assert_eq!(log.iter().filter(|s| s == &"after_step").count(), 1); } /// Registering a plugin inside a hook defers it to the next dispatch. #[test] fn mid_dispatch_registration_is_deferred() { struct Registrar { log: Log, registered: bool, } impl Plugin for Registrar { fn after_step(&mut self, h: &mut Harness<'_, S>) { self.log.lock().unwrap().push("registrar:after_step".into()); if !self.registered { self.registered = true; let (latecomer, latecomer_log) = CountingPlugin::new(); // Share the same log so we can see its hooks interleave. *latecomer.log.lock().unwrap() = std::mem::take(&mut self.log.lock().unwrap()); self.log = latecomer.log.clone(); let _ = latecomer_log; // dropped h.add_plugin(latecomer); } } } let log: Log = Arc::default(); let mut harness = Harness::builder() .with_plugin(Registrar { log: log.clone(), registered: false, }) .build_ui(|ui| { ui.label("hi"); }); harness.step(); // registrar hooks fire here; latecomer gets registered // The latecomer should NOT see this step's hooks (it was registered mid-dispatch). // On the next step, it should start seeing hooks. // Easier assertion: before the second step, the latecomer shouldn't have produced // any "before_step" entries. Since we merged logs, we can't easily isolate — instead, // verify the harness does not deadlock / recurse. harness.step(); assert!(harness.plugin::().is_some()); } /// Downcasting via `plugin::

()` / `plugin_mut::

()` / `take_plugin::

()`. #[test] fn downcast_plugin_by_type() { let (plugin, _log) = CountingPlugin::new(); let mut harness = Harness::builder().with_plugin(plugin).build_ui(|ui| { ui.label("hi"); }); assert!(harness.plugin::().is_some()); assert!(harness.plugin_mut::().is_some()); let taken = harness.take_plugin::(); assert!(taken.is_some()); assert!(harness.plugin::().is_none()); } /// When `Harness::drop` fires while a panic is unwinding, `on_test_result` gets `Fail`. #[test] fn on_test_result_sees_panic() { let (plugin, log) = CountingPlugin::new(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let _harness = Harness::builder().with_plugin(plugin).build_ui(|ui| { ui.label("hi"); }); // Panic while the harness is alive so its Drop runs during unwind. panic!("kaboom"); })); assert!(result.is_err()); let log = log.lock().unwrap(); let last = log.last().map(String::as_str); assert_eq!(last, Some("on_test_result:fail"), "log = {log:?}"); } /// Calling `Harness::step` from inside a plugin hook should panic in debug builds /// (the `in_dispatch` guard). #[cfg(debug_assertions)] #[test] #[should_panic(expected = "inside a plugin hook")] fn reentrant_step_panics_in_debug() { struct Misbehaver; impl Plugin for Misbehaver { fn after_step(&mut self, h: &mut Harness<'_, S>) { // Forbidden: step from inside a hook. h.step(); } } let mut harness = Harness::builder().with_plugin(Misbehaver).build_ui(|ui| { ui.label("hi"); }); harness.step(); } /// `on_snapshot` fires with an Err result for a missing snapshot. #[cfg(feature = "snapshot")] #[test] fn on_snapshot_fires_with_err_for_missing() { let (plugin, log) = CountingPlugin::new(); let tmp = tempfile::tempdir().unwrap(); let mut harness = Harness::builder() .wgpu() .with_plugin(plugin) .with_options( egui_kittest::SnapshotOptions::default().output_path(tmp.path().to_path_buf()), ) .build_ui(|ui| { ui.label("snap"); }); let result = harness.try_snapshot("nonexistent_snapshot_for_plugin_test"); // Expect Err (no snapshot file exists in tmpdir). assert!(result.is_err(), "expected snapshot err, got {result:?}"); let log = log.lock().unwrap(); let on_snapshot_entry = log .iter() .find(|s| s.starts_with("on_snapshot:")) .expect("on_snapshot should have been logged"); assert!( on_snapshot_entry.ends_with(":err"), "entry = {on_snapshot_entry}" ); assert!( on_snapshot_entry.contains("nonexistent_snapshot_for_plugin_test"), "entry should contain the snapshot name: {on_snapshot_entry}" ); }