mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 14:49:06 -04:00
Fix plugin example and remove plugin test
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user