mirror of
https://github.com/emilk/egui.git
synced 2026-09-03 23:30:04 -04:00
Add kittest recording feature
This commit is contained in:
181
crates/egui_kittest/tests/recording.rs
Normal file
181
crates/egui_kittest/tests/recording.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
#![cfg(all(feature = "recording", feature = "wgpu"))]
|
||||
|
||||
use egui_kittest::{Harness, RecordingOptions, RecordingPlugin, RecordingTrigger};
|
||||
use kittest::Queryable as _;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn counter_harness(value: &mut u32) -> Harness<'_, &mut u32> {
|
||||
Harness::builder()
|
||||
.with_size(egui::Vec2::new(120.0, 60.0))
|
||||
.build_ui_state(
|
||||
|ui, state| {
|
||||
if ui.button(format!("count: {state}")).clicked() {
|
||||
**state += 1;
|
||||
}
|
||||
},
|
||||
value,
|
||||
)
|
||||
}
|
||||
|
||||
fn count_pngs(dir: &std::path::Path) -> usize {
|
||||
std::fs::read_dir(dir)
|
||||
.expect("png output dir exists")
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "png"))
|
||||
.count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_a_gif() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let gif_path = dir.path().join("counter.gif");
|
||||
|
||||
let mut value = 0;
|
||||
let mut harness = counter_harness(&mut value);
|
||||
harness.start_recording(RecordingOptions::gif(&gif_path, 12.0));
|
||||
|
||||
harness.run();
|
||||
harness.get_by_label_contains("count").click();
|
||||
harness.run();
|
||||
|
||||
assert!(harness.is_recording());
|
||||
harness.finish_recording().expect("save gif");
|
||||
assert!(!harness.is_recording());
|
||||
|
||||
let frames = decode_gif_frames(&gif_path);
|
||||
assert!(
|
||||
frames >= 2,
|
||||
"the click should have produced at least two different frames, got {frames}"
|
||||
);
|
||||
}
|
||||
|
||||
fn decode_gif_frames(path: &std::path::Path) -> usize {
|
||||
use image::AnimationDecoder as _;
|
||||
|
||||
let file = std::io::BufReader::new(std::fs::File::open(path).expect("gif exists"));
|
||||
image::codecs::gif::GifDecoder::new(file)
|
||||
.expect("decode gif")
|
||||
.into_frames()
|
||||
.count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_a_png_sequence() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let out = dir.path().join("frames");
|
||||
|
||||
let mut value = 0;
|
||||
let mut harness = counter_harness(&mut value);
|
||||
harness.start_recording(
|
||||
RecordingOptions::png_sequence(&out).with_trigger(RecordingTrigger::EveryFrame),
|
||||
);
|
||||
|
||||
harness.run();
|
||||
harness.get_by_label_contains("count").click();
|
||||
harness.run();
|
||||
|
||||
harness.finish_recording().expect("save png sequence");
|
||||
|
||||
assert!(count_pngs(&out) > 0, "expected at least one frame");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_frames_drops_identical_frames() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let out = dir.path().join("frames");
|
||||
|
||||
let mut value = 0;
|
||||
let mut harness = counter_harness(&mut value);
|
||||
harness.start_recording(
|
||||
RecordingOptions::png_sequence(&out).with_trigger(RecordingTrigger::ChangedFrames),
|
||||
);
|
||||
|
||||
for _ in 0..6 {
|
||||
harness.run();
|
||||
}
|
||||
harness.finish_recording().expect("save png sequence");
|
||||
|
||||
assert_eq!(
|
||||
count_pngs(&out),
|
||||
1,
|
||||
"nothing changed, so only the first frame should be kept"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_nth_frame_skips_frames() {
|
||||
let mut value = 0;
|
||||
let mut harness = counter_harness(&mut value);
|
||||
|
||||
harness.start_recording(
|
||||
RecordingOptions::gif(std::path::PathBuf::new(), 10.0)
|
||||
.with_trigger(RecordingTrigger::EveryNthFrame(2)),
|
||||
);
|
||||
harness.run_steps(4);
|
||||
|
||||
let frames = harness
|
||||
.with_recording(|plugin| plugin.frames().len())
|
||||
.expect("the plugin is registered");
|
||||
assert_eq!(frames, 2, "every second of the 4 passes should be captured");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finishing_without_recording_is_an_error() {
|
||||
let mut value = 0;
|
||||
let mut harness = counter_harness(&mut value);
|
||||
|
||||
let err = harness.finish_recording().expect_err("not recording");
|
||||
assert!(matches!(
|
||||
err,
|
||||
egui_kittest::RecordingError::NotRecording | egui_kittest::RecordingError::NoFrames
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recording_without_frames_is_an_error() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
|
||||
let mut value = 0;
|
||||
let mut harness = counter_harness(&mut value);
|
||||
harness.start_recording(RecordingOptions::gif(dir.path().join("empty.gif"), 10.0));
|
||||
|
||||
let err = harness.finish_recording().expect_err("no frames");
|
||||
assert!(matches!(err, egui_kittest::RecordingError::NoFrames));
|
||||
}
|
||||
|
||||
/// The recorder is a plain [`egui::Plugin`]: it records any [`egui::Context`],
|
||||
/// with no harness and no renderer of your own.
|
||||
#[test]
|
||||
fn records_a_plain_context() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let gif_path = dir.path().join("plain.gif");
|
||||
|
||||
let ctx = egui::Context::default();
|
||||
ctx.add_plugin(RecordingPlugin::new(RecordingOptions::gif(&gif_path, 10.0)));
|
||||
|
||||
let input = egui::RawInput {
|
||||
screen_rect: Some(egui::Rect::from_min_size(
|
||||
egui::Pos2::ZERO,
|
||||
egui::Vec2::new(120.0, 60.0),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for pass in 0..3 {
|
||||
let output = ctx.run_ui(input.clone(), |ui| {
|
||||
ui.label(format!("pass {pass}"));
|
||||
});
|
||||
// We have no renderer of our own; the plugin already rendered what it needed.
|
||||
output.drop_without_applying_deltas();
|
||||
}
|
||||
|
||||
let frames = ctx
|
||||
.with_plugin::<RecordingPlugin, _>(|plugin| {
|
||||
plugin.save().expect("save gif");
|
||||
plugin.frames().len()
|
||||
})
|
||||
.expect("the plugin is registered");
|
||||
|
||||
assert_eq!(frames, 3, "each pass shows a different label");
|
||||
assert!(gif_path.exists(), "the GIF should have been written");
|
||||
}
|
||||
61
crates/egui_kittest/tests/recording_env.rs
Normal file
61
crates/egui_kittest/tests/recording_env.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
//! Checks that `KITTEST_RECORD` records every harness and writes the GIFs next to the
|
||||
//! snapshots, in `recordings/{test_name}.gif`.
|
||||
//!
|
||||
//! This is a test binary of its own because it changes the environment and the working
|
||||
//! directory of the whole process.
|
||||
|
||||
#![cfg(all(feature = "recording", feature = "wgpu"))]
|
||||
#![expect(unsafe_code)] // To set the environment variable.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use egui_kittest::Harness;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Run the process in a temporary directory, with recording turned on.
|
||||
///
|
||||
/// Both the environment variable and the `kittest.toml` are read once per process,
|
||||
/// so this must happen before the first harness is built.
|
||||
fn setup() -> &'static std::path::Path {
|
||||
static SETUP: OnceLock<TempDir> = OnceLock::new();
|
||||
|
||||
SETUP
|
||||
.get_or_init(|| {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
// Write the recordings into the temporary directory.
|
||||
std::fs::write(dir.path().join("kittest.toml"), "output_path = \".\"\n")
|
||||
.expect("write kittest.toml");
|
||||
|
||||
// SAFETY: the `OnceLock` runs this once, before any other thread reads the
|
||||
// environment or the working directory.
|
||||
unsafe {
|
||||
std::env::set_current_dir(dir.path()).expect("chdir to the tempdir");
|
||||
std::env::set_var(egui_kittest::RECORD_ENV_VAR, "1");
|
||||
}
|
||||
|
||||
dir
|
||||
})
|
||||
.path()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_var_records_every_harness() {
|
||||
let dir = setup();
|
||||
|
||||
{
|
||||
let mut harness = Harness::new_ui(|ui| {
|
||||
ui.label("recorded by the environment variable");
|
||||
});
|
||||
harness.run();
|
||||
// Dropping the harness saves the recording.
|
||||
}
|
||||
|
||||
let gif = dir
|
||||
.join("recordings")
|
||||
.join("env_var_records_every_harness.gif");
|
||||
let size = std::fs::metadata(&gif)
|
||||
.unwrap_or_else(|err| panic!("{} should exist: {err}", gif.display()))
|
||||
.len();
|
||||
assert!(size > 0, "the GIF should not be empty");
|
||||
}
|
||||
Reference in New Issue
Block a user