From 19e414fa9365b24c6ae1635faaa0cc4e6c83ff91 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Mon, 27 Apr 2026 13:22:41 +0200 Subject: [PATCH] Fix plugin example and remove plugin test --- .../examples/require_button_labels_plugin.rs | 7 +- crates/egui_kittest/tests/plugin.rs | 257 ------------------ 2 files changed, 6 insertions(+), 258 deletions(-) delete mode 100644 crates/egui_kittest/tests/plugin.rs diff --git a/crates/egui_kittest/examples/require_button_labels_plugin.rs b/crates/egui_kittest/examples/require_button_labels_plugin.rs index 8d28a256c..54d853e28 100644 --- a/crates/egui_kittest/examples/require_button_labels_plugin.rs +++ b/crates/egui_kittest/examples/require_button_labels_plugin.rs @@ -1,5 +1,6 @@ //! Example that shows how to create a `egui_kittest` Plugin that ensures each button has a label. +use egui::accesskit; use egui_kittest::kittest::{NodeT as _, Queryable as _}; use egui_kittest::{Harness, Plugin}; @@ -7,7 +8,11 @@ use egui_kittest::{Harness, Plugin}; pub struct RequireButtonLabels; impl Plugin for RequireButtonLabels { - fn after_step(&mut self, harness: &mut Harness<'_, S>) { + fn after_step( + &mut self, + harness: &mut Harness<'_, S>, + _accesskit_update: &egui::accesskit::TreeUpdate, + ) { for button in harness.query_all_by_role(egui::accesskit::Role::Button) { let node = button.accesskit_node(); diff --git a/crates/egui_kittest/tests/plugin.rs b/crates/egui_kittest/tests/plugin.rs deleted file mode 100644 index 30e0a4f7c..000000000 --- a/crates/egui_kittest/tests/plugin.rs +++ /dev/null @@ -1,257 +0,0 @@ -#![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>, - _tree: &egui::accesskit::TreeUpdate, - ) { - 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:?}"); -} - -/// `step_no_side_effects` does NOT fire `before_step`/`after_step`. -#[test] -fn step_no_side_effects_skips_hooks() { - struct DrivingPlugin { - log: Log, - drove: bool, - } - impl Plugin for DrivingPlugin { - fn after_step(&mut self, h: &mut Harness<'_, S>, _tree: &egui::accesskit::TreeUpdate) { - self.log.lock().unwrap().push("after_step".into()); - if !self.drove { - self.drove = true; - // Call step_no_side_effects from inside a hook — must not recurse. - h.step_no_side_effects(); - } - } - } - - 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). step_no_side_effects 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>, _tree: &egui::accesskit::TreeUpdate) { - 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(); -} - -/// 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:?}"); -} - -/// `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}" - ); -}