1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-02 23:00:04 -04:00

Add egui testing library (#5166)

- closes #3491 
- closes #3926

This adds a testing library to egui based on
[kittest](https://github.com/rerun-io/kittest). Kittest is a new
[AccessKit](https://github.com/AccessKit/accesskit/)-based testing
library. The api is inspired by the js
[testing-library](https://testing-library.com/) where the idea is also
to query the dom based on accessibility attributes.
We made kittest with egui in mind but it should work with any rust gui
framework with AccessKit support.

It currently has support for:
- running the egui app, frame by frame
- building the AccessKit tree
- ergonomic queries via kittest
  - via e.g. get_by_name, get_by_role
- simulating events based on the accesskit node id
- creating arbitrary events based on Harness::input_mut
- rendering screenshots via wgpu
- snapshot tests with these screenshots

A simple test looks like this: 
```rust
fn main() {
    let mut checked = false;
    let app = |ctx: &Context| {
        CentralPanel::default().show(ctx, |ui| {
            ui.checkbox(&mut checked, "Check me!");
        });
    };

    let mut harness = Harness::builder().with_size(egui::Vec2::new(200.0, 100.0)).build(app);
    
    let checkbox = harness.get_by_name("Check me!");
    assert_eq!(checkbox.toggled(), Some(Toggled::False));
    checkbox.click();
    
    harness.run();

    let checkbox = harness.get_by_name("Check me!");
    assert_eq!(checkbox.toggled(), Some(Toggled::True));

    // You can even render the ui and do image snapshot tests
    #[cfg(all(feature = "wgpu", feature = "snapshot"))]
    egui_kittest::image_snapshot(&egui_kittest::wgpu::TestRenderer::new().render(&harness), "readme_example");
}
```

~Since getting wgpu to run in ci is a hassle, I'm taking another shot at
creating a software renderer for egui (ideally without a huge dependency
like skia)~ (this didn't work as well as I hoped and it turns out in CI
you can just run tests on a mac runner which comes with a real GPU)
 
Here is a example of a failed snapshot test in ci, it will say which
snapshot failed and upload an artifact with the before / after and diff
images:

https://github.com/emilk/egui/actions/runs/11183049487/job/31090724606?pr=5166
This commit is contained in:
lucasmerlin
2024-10-22 12:39:00 +02:00
committed by GitHub
parent 707cd03357
commit 70a01138b7
45 changed files with 1262 additions and 11 deletions

View File

@@ -55,8 +55,13 @@ serde = { workspace = true, optional = true }
[dev-dependencies]
criterion.workspace = true
# when running tests we always want to use the `chrono` feature
egui_demo_lib = { workspace = true, features = ["chrono"] }
criterion.workspace = true
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
wgpu = { workspace = true, features = ["metal"] }
egui = { workspace = true, features = ["default_fonts"] }
[[bench]]
name = "benchmark"

View File

@@ -377,3 +377,52 @@ fn file_menu_button(ui: &mut Ui) {
}
});
}
#[cfg(test)]
mod tests {
use crate::demo::demo_app_windows::Demos;
use egui::Vec2;
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
#[test]
fn demos_should_match_snapshot() {
let demos = Demos::default();
let mut errors = Vec::new();
for mut demo in demos.demos {
// Remove the emoji from the demo name
let name = demo
.name()
.split_once(' ')
.map_or(demo.name(), |(_, name)| name);
// Widget Gallery needs to be customized (to set a specific date) and has its own test
if name == "Widget Gallery" {
continue;
}
let mut harness = Harness::new(|ctx| {
demo.show(ctx, &mut true);
});
let window = harness.node().children().next().unwrap();
// TODO(lucasmerlin): Windows should probably have a label?
//let window = harness.get_by_name(name);
let size = window.raw_bounds().expect("window bounds").size();
harness.set_size(Vec2::new(size.width as f32, size.height as f32));
// Run the app for some more frames...
harness.run();
let result = harness.try_wgpu_snapshot(&format!("demos/{name}"));
if let Err(err) = result {
errors.push(err);
}
}
assert!(errors.is_empty(), "Errors: {errors:#?}");
}
}

View File

@@ -112,3 +112,37 @@ impl crate::View for TextEditDemo {
});
}
}
#[cfg(test)]
mod tests {
use egui::{accesskit, CentralPanel};
use egui_kittest::kittest::{Key, Queryable};
use egui_kittest::Harness;
#[test]
pub fn should_type() {
let mut text = "Hello, world!".to_owned();
let mut harness = Harness::new(move |ctx| {
CentralPanel::default().show(ctx, |ui| {
ui.text_edit_singleline(&mut text);
});
});
harness.run();
let text_edit = harness.get_by_role(accesskit::Role::TextInput);
assert_eq!(text_edit.value().as_deref(), Some("Hello, world!"));
text_edit.key_combination(&[Key::Command, Key::A]);
text_edit.type_text("Hi ");
harness.run();
harness
.get_by_role(accesskit::Role::TextInput)
.type_text("there!");
harness.run();
let text_edit = harness.get_by_role(accesskit::Role::TextInput);
assert_eq!(text_edit.value().as_deref(), Some("Hi there!"));
}
}

View File

@@ -285,3 +285,31 @@ fn doc_link_label_with_crate<'a>(
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::View;
use egui::{CentralPanel, Context, Vec2};
use egui_kittest::Harness;
#[test]
pub fn should_match_screenshot() {
let mut demo = WidgetGallery {
// If we don't set a fixed date, the snapshot test will fail.
date: Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
..Default::default()
};
let app = |ctx: &Context| {
CentralPanel::default().show(ctx, |ui| {
demo.ui(ui);
});
};
let harness = Harness::builder()
.with_size(Vec2::new(380.0, 550.0))
.with_dpi(2.0)
.build(app);
harness.wgpu_snapshot("widget_gallery");
}
}

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4a725aa81433f301fda4ff8a28be869366332964995d1ae4ed996591596eb7e2
size 31461

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:36028d85f49ee77562250214237def2b676ecc9ed413d2fd8afc473d61289ca1
size 32761

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b8d4f004ee11ea68ae0f30657601b6e51403fcc3ca91fa5b8cdcb58585d8d40d
size 78318

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:684648bea4ef5ce138fc25dbe7576e3937a797e87f2244cb3656ff8b4c2777f5
size 11574

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ad38bff7cc5661be43e730e1b34c444b571b24b9f50791209496a1687610dd3d
size 20543

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ff78748f2571c49638d8fe8fdc859aaa5181758aad65498b7217551350fb9138
size 20672

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9dee66004cc47f5e27aaac34d137ff005eedf70cbfa3fbe43153dfd5c09d5e18
size 10610

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0d1086b789f1fe0a8085c86f5b6a5ae7ecb53020f385b84775d6812ebc9d74a3
size 132349

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08be378c01e376aab6e99ba3158519bbd7b301e815dc3447b57c9abab558977f
size 24237

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:53097b2c26ebcba8b8ad657ed8e52ca40261155e96dbbfca1e8eb01fce25d290
size 17586

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d9c8395e6b4287b92d85a52ca2d47750f67abeb0ad88c6b42264bfe2e62fd09d
size 22283

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:38d21b6f8c364f86ad759e88ea1068649c23c58ded5d2953ba8ff1c83b46112f
size 63884

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:83162f8c496a55230375dbc4cc636cfacf63049c913904bea9d06bdb56e63da6
size 36282

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2537c681d1ffceb5cf4bf19d11295891525c96aea0b1422ab28f133021185be0
size 17451

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:79ce1dbf7627579d4e10de6494e34d8fd9685506d7b35cb3c9148f90f8c01366
size 25144

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5068df8549ffc91028addfec6f851f12a4de80e208b50b39e4d44b6aa2c7240e
size 261946

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:be2ac005fd5aafa293e21b162c22a09078e46d2d45b6208ce0f7841eeb05314a
size 183934

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2e3436906f7ac459b7f4330a286937722e78ad885ae1e90f75be566e970a8ca7
size 116899

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df7dabf726620ab5205ce153f692d1ba02365848ead7b79c95b873d5121d52a6
size 25850

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ae6c2e3aad43cfad3322340ff7045ec50ba01d58feb7b8acc5ba062a8a5c9ab8
size 70230

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ec0c2efff75cb8d621f5a4ea59f9fa8d3076521ca34f4499e07fb9dc8681d7ba
size 65916

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c04aee0a3a77a3691bb601a93871117500be917e0896138fda43251454ec04c2
size 20988

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:814d863deaa4fa029044da1783db87744f0d82e874edd6cbab16e712ed8715aa
size 59881

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e682f5cb9ecb1bdf89281c2ba1612078e70e97f28c76facc64d717e4015ced6a
size 12977

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:15acfb041cc53ef9bd966d6edd53a6b692cdb645ae5cf34bc20e70d403371c30
size 34809

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5dc632962f8894c4f20a48c9b9e57d60470f3f83ef7f19d05854dba718610a2f
size 161820