1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00

Fix plugin example and remove plugin test

This commit is contained in:
lucasmerlin
2026-04-27 13:22:41 +02:00
parent 97c661263e
commit 19e414fa93
2 changed files with 6 additions and 258 deletions

View File

@@ -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<S> Plugin<S> 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();

View File

@@ -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<Mutex<Vec<String>>>;
#[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<S> Plugin<S> 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<u64, &ExceededMaxStepsError>) {
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<S: 'static> Plugin<S> 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<S: 'static> Plugin<S> 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}"
);
}