mirror of
https://github.com/emilk/egui.git
synced 2026-09-03 23:30:04 -04:00
Add gif recording prototype
This commit is contained in:
124
crates/egui_kittest/tests/recording.rs
Normal file
124
crates/egui_kittest/tests/recording.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
#![cfg(all(feature = "recording", feature = "wgpu"))]
|
||||
|
||||
use egui_kittest::{Harness, RecordingOptions, 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(|e| e.ok())
|
||||
.filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("png"))
|
||||
.count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_gif_with_diffing() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let gif_path = dir.path().join("counter.gif");
|
||||
|
||||
let mut value = 0u32;
|
||||
let mut harness = counter_harness(&mut value);
|
||||
harness.start_recording(RecordingOptions::gif(&gif_path, 12.0));
|
||||
|
||||
harness.run();
|
||||
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 metadata = std::fs::metadata(&gif_path).expect("gif exists");
|
||||
assert!(metadata.len() > 0, "GIF should be non-empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_png_sequence() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let out = dir.path().join("frames");
|
||||
|
||||
let mut value = 0u32;
|
||||
let mut harness = counter_harness(&mut value);
|
||||
harness.start_recording(
|
||||
RecordingOptions::png_sequence(&out).with_trigger(RecordingTrigger::EveryStep),
|
||||
);
|
||||
|
||||
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 diff_every_step_dedupes_unchanged_frames() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let out = dir.path().join("frames");
|
||||
|
||||
let mut value = 0u32;
|
||||
let mut harness = counter_harness(&mut value);
|
||||
harness.start_recording(
|
||||
RecordingOptions::png_sequence(&out).with_trigger(RecordingTrigger::DiffEveryStep),
|
||||
);
|
||||
|
||||
for _ in 0..6 {
|
||||
harness.run();
|
||||
}
|
||||
harness.finish_recording().expect("save png sequence");
|
||||
|
||||
assert_eq!(
|
||||
count_pngs(&out),
|
||||
1,
|
||||
"DiffEveryStep should dedupe unchanged frames"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_run_trigger_captures_per_run_only() {
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let out = dir.path().join("frames");
|
||||
|
||||
let mut value = 0u32;
|
||||
let mut harness = counter_harness(&mut value);
|
||||
harness.start_recording(
|
||||
RecordingOptions::png_sequence(&out).with_trigger(RecordingTrigger::OnRun),
|
||||
);
|
||||
|
||||
harness.run();
|
||||
harness.get_by_label_contains("count").click();
|
||||
harness.run();
|
||||
harness.run();
|
||||
|
||||
harness.finish_recording().expect("save png sequence");
|
||||
|
||||
assert_eq!(
|
||||
count_pngs(&out),
|
||||
3,
|
||||
"OnRun should produce one frame per run() call"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finish_recording_without_start_errors() {
|
||||
let mut value = 0u32;
|
||||
let mut harness = counter_harness(&mut value);
|
||||
let err = harness.finish_recording().expect_err("not recording");
|
||||
assert!(matches!(err, egui_kittest::RecordingError::NotRecording));
|
||||
}
|
||||
65
crates/egui_kittest/tests/recording_env.rs
Normal file
65
crates/egui_kittest/tests/recording_env.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
//! Verifies that the `KITTEST_RECORD` env var auto-records harnesses and writes them next to
|
||||
//! snapshots under `recordings/{test_name}.gif`.
|
||||
//!
|
||||
//! This is its own integration test binary because the env var is read once via `OnceLock`,
|
||||
//! and we redirect the snapshot output path via `kittest.toml` lookup is process-global too.
|
||||
|
||||
#![cfg(all(feature = "recording", feature = "snapshot", feature = "wgpu"))]
|
||||
#![allow(unsafe_code)] // tests need set_var / set_current_dir
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use egui_kittest::Harness;
|
||||
use tempfile::TempDir;
|
||||
|
||||
static SETUP: OnceLock<TempDir> = OnceLock::new();
|
||||
|
||||
fn setup_env() -> &'static std::path::Path {
|
||||
SETUP
|
||||
.get_or_init(|| {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
// Point the snapshot output at our temp dir (used as the recording root).
|
||||
std::fs::write(dir.path().join("kittest.toml"), "output_path = \".\"\n")
|
||||
.expect("write kittest.toml");
|
||||
|
||||
// SAFETY: this OnceLock guarantees a single initialization before any harness
|
||||
// reads the env var or cwd, so no concurrent env access happens.
|
||||
unsafe {
|
||||
std::env::set_current_dir(dir.path()).expect("chdir to tmp");
|
||||
std::env::set_var("KITTEST_RECORD", "1");
|
||||
}
|
||||
dir
|
||||
})
|
||||
.path()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_var_records_to_recordings_dir() {
|
||||
let dir = setup_env();
|
||||
|
||||
{
|
||||
let mut harness = Harness::new_ui(|ui| {
|
||||
ui.label("env-recorded");
|
||||
});
|
||||
harness.run();
|
||||
// Drop here triggers the auto-save.
|
||||
}
|
||||
|
||||
let recordings = dir.join("recordings");
|
||||
let entries: Vec<_> = std::fs::read_dir(&recordings)
|
||||
.expect("recordings dir exists")
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|x| x.to_str()) == Some("gif"))
|
||||
.collect();
|
||||
assert!(
|
||||
!entries.is_empty(),
|
||||
"KITTEST_RECORD should produce a GIF in {}",
|
||||
recordings.display()
|
||||
);
|
||||
for entry in &entries {
|
||||
let len = std::fs::metadata(entry).expect("stat").len();
|
||||
assert!(len > 0, "GIF {} should be non-empty", entry.display());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user