diff --git a/Cargo.lock b/Cargo.lock index 9a441cb3..abd6aca0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2422,7 +2422,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kittest" version = "0.1.0" -source = "git+https://github.com/rerun-io/kittest?branch=main#679f9ade828021295c5f86f38275d9271d001004" +source = "git+https://github.com/rerun-io/kittest?branch=main#91bf0fd98b5afe04427bb3aea4c68c6e0034b4bd" dependencies = [ "accesskit", "accesskit_consumer", diff --git a/crates/egui_demo_app/tests/snapshots/clock.png b/crates/egui_demo_app/tests/snapshots/clock.png index 2b0bfc6c..31bb331e 100644 --- a/crates/egui_demo_app/tests/snapshots/clock.png +++ b/crates/egui_demo_app/tests/snapshots/clock.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61db3807f755ac832ba069e1adaf8aeb550c88737b4907748667a271ae29863d -size 334792 +oid sha256:0bd688ff74f9a096edab545fbcbf61b61a464183da066ae4a120ce1e2abf3e7b +size 334969 diff --git a/crates/egui_demo_app/tests/snapshots/custom3d.png b/crates/egui_demo_app/tests/snapshots/custom3d.png index ce19412f..2458cd8b 100644 --- a/crates/egui_demo_app/tests/snapshots/custom3d.png +++ b/crates/egui_demo_app/tests/snapshots/custom3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21e0a6cdf175606a513ddf410ae1b873a9817305ecad403116fad3c6ff795fa3 -size 92185 +oid sha256:c80c4ae4c2bfbc5c91e9cd94213a4f87646fe910b4a7c747531a1efcf23def47 +size 92364 diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png index 7666b658..08f5fc98 100644 --- a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png +++ b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e6a383dca7e91d07df4bf501e2de13d046f04546a08d026efe3f82fc96b6e29 -size 178887 +oid sha256:8cf6d0b20f127f22d49daefed27fc2d0ca43d645fe1486cf7f6fcbb676bdec82 +size 179065 diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index d5bde1f9..a13af2e7 100644 --- a/crates/egui_demo_app/tests/snapshots/imageviewer.png +++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2fae780123389ca0affa762a5c031b84abcdd31c7a830d485c907c8c370b006 -size 100780 +oid sha256:0e37b3ce49c9ccc1a64beb58b176e23ab6c1fa2d897f676b0de85e510e6bfa85 +size 100845 diff --git a/crates/egui_demo_app/tests/test_demo_app.rs b/crates/egui_demo_app/tests/test_demo_app.rs index 8b0b272f..65afff10 100644 --- a/crates/egui_demo_app/tests/test_demo_app.rs +++ b/crates/egui_demo_app/tests/test_demo_app.rs @@ -55,7 +55,7 @@ fn test_demo_app() { harness .get_by_role_and_label(Role::TextInput, "URI:") .focus(); - harness.press_key_modifiers(egui::Modifiers::COMMAND, egui::Key::A); + harness.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A); harness .get_by_role_and_label(Role::TextInput, "URI:") diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index cc43645c..6e9a92ef 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -371,8 +371,8 @@ fn file_menu_button(ui: &mut Ui) { #[cfg(test)] mod tests { use crate::{demo::demo_app_windows::DemoGroups, Demo as _}; - use egui::Vec2; - use egui_kittest::kittest::Queryable as _; + + use egui_kittest::kittest::{NodeT as _, Queryable as _}; use egui_kittest::{Harness, SnapshotOptions, SnapshotResults}; #[test] @@ -399,12 +399,12 @@ mod tests { demo.show(ctx, &mut true); }); - let window = harness.node().children().next().unwrap(); + let window = harness.queryable_node().children().next().unwrap(); // TODO(lucasmerlin): Windows should probably have a label? //let window = harness.get_by_label(name); - let size = window.raw_bounds().expect("window bounds").size(); - harness.set_size(Vec2::new(size.width as f32, size.height as f32)); + let size = window.rect().size(); + harness.set_size(size); // Run the app for some more frames... harness.run_ok(); diff --git a/crates/egui_demo_lib/src/demo/modals.rs b/crates/egui_demo_lib/src/demo/modals.rs index fcb33f0b..5fb1548e 100644 --- a/crates/egui_demo_lib/src/demo/modals.rs +++ b/crates/egui_demo_lib/src/demo/modals.rs @@ -190,7 +190,7 @@ mod tests { assert!(harness.ctx.memory(|mem| mem.any_popup_open())); assert!(harness.state().user_modal_open); - harness.press_key(Key::Escape); + harness.key_press(Key::Escape); harness.run_ok(); assert!(!harness.ctx.memory(|mem| mem.any_popup_open())); assert!(harness.state().user_modal_open); @@ -214,7 +214,7 @@ mod tests { assert!(harness.state().user_modal_open); assert!(harness.state().save_modal_open); - harness.press_key(Key::Escape); + harness.key_press(Key::Escape); harness.run(); assert!(harness.state().user_modal_open); @@ -267,7 +267,7 @@ mod tests { harness.run_ok(); - harness.get_by_label("Yes Please").simulate_click(); + harness.get_by_label("Yes Please").click(); harness.run_ok(); diff --git a/crates/egui_demo_lib/src/demo/text_edit.rs b/crates/egui_demo_lib/src/demo/text_edit.rs index 685a9c38..4ef34a51 100644 --- a/crates/egui_demo_lib/src/demo/text_edit.rs +++ b/crates/egui_demo_lib/src/demo/text_edit.rs @@ -113,8 +113,8 @@ impl crate::View for TextEditDemo { #[cfg(test)] mod tests { - use egui::{accesskit, CentralPanel}; - use egui_kittest::kittest::{Key, Queryable as _}; + use egui::{accesskit, CentralPanel, Key, Modifiers}; + use egui_kittest::kittest::Queryable as _; use egui_kittest::Harness; #[test] @@ -133,8 +133,9 @@ mod tests { let text_edit = harness.get_by_role(accesskit::Role::TextInput); assert_eq!(text_edit.value().as_deref(), Some("Hello, world!")); + text_edit.focus(); - text_edit.key_combination(&[Key::Command, Key::A]); + harness.key_press_modifiers(Modifiers::COMMAND, Key::A); text_edit.type_text("Hi "); harness.run(); diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index 5f0e91bc..32e51a82 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -737,8 +737,9 @@ mod tests { }); { - // Expand color-test collapsing header - harness.get_by_label("Color test").click(); + // Expand color-test collapsing header. We accesskit-click since collapsing header + // might not be on screen at this point. + harness.get_by_label("Color test").click_accesskit(); harness.run(); } diff --git a/crates/egui_kittest/README.md b/crates/egui_kittest/README.md index ff071c9f..31a55f61 100644 --- a/crates/egui_kittest/README.md +++ b/crates/egui_kittest/README.md @@ -10,7 +10,7 @@ Ui testing library for egui, based on [kittest](https://github.com/rerun-io/kitt ## Example usage ```rust use egui::accesskit::Toggled; -use egui_kittest::{Harness, kittest::Queryable}; +use egui_kittest::{Harness, kittest::{Queryable, NodeT}}; fn main() { let mut checked = false; @@ -21,13 +21,13 @@ fn main() { let mut harness = Harness::new_ui(app); let checkbox = harness.get_by_label("Check me!"); - assert_eq!(checkbox.toggled(), Some(Toggled::False)); + assert_eq!(checkbox.accesskit_node().toggled(), Some(Toggled::False)); checkbox.click(); harness.run(); let checkbox = harness.get_by_label("Check me!"); - assert_eq!(checkbox.toggled(), Some(Toggled::True)); + assert_eq!(checkbox.accesskit_node().toggled(), Some(Toggled::True)); // Shrink the window size to the smallest size possible harness.fit_contents(); diff --git a/crates/egui_kittest/src/event.rs b/crates/egui_kittest/src/event.rs deleted file mode 100644 index e756d4dc..00000000 --- a/crates/egui_kittest/src/event.rs +++ /dev/null @@ -1,194 +0,0 @@ -use egui::Event::PointerButton; -use egui::{Event, Modifiers, Pos2}; -use kittest::{ElementState, MouseButton, SimulatedEvent}; - -#[derive(Default)] -pub(crate) struct EventState { - last_mouse_pos: Pos2, -} - -impl EventState { - /// Map the kittest event to an egui event, add it to the input and update the modifiers. - /// This function accesses `egui::RawInput::modifiers`. Make sure it is not reset after each - /// frame (Since we use [`egui::RawInput::take`], this should be fine). - pub fn update(&mut self, event: kittest::Event, input: &mut egui::RawInput) { - if let Some(event) = self.kittest_event_to_egui(&mut input.modifiers, event) { - input.events.push(event); - } - } - - fn kittest_event_to_egui( - &mut self, - modifiers: &mut Modifiers, - event: kittest::Event, - ) -> Option { - match event { - kittest::Event::ActionRequest(e) => Some(Event::AccessKitActionRequest(e)), - kittest::Event::Simulated(e) => match e { - SimulatedEvent::CursorMoved { position } => { - self.last_mouse_pos = Pos2::new(position.x as f32, position.y as f32); - Some(Event::PointerMoved(Pos2::new( - position.x as f32, - position.y as f32, - ))) - } - SimulatedEvent::MouseInput { state, button } => { - pointer_button_to_egui(button).map(|button| PointerButton { - button, - modifiers: *modifiers, - pos: self.last_mouse_pos, - pressed: matches!(state, ElementState::Pressed), - }) - } - SimulatedEvent::Ime(text) => Some(Event::Text(text)), - SimulatedEvent::KeyInput { state, key } => { - match key { - kittest::Key::Alt => { - modifiers.alt = matches!(state, ElementState::Pressed); - } - kittest::Key::Command => { - modifiers.command = matches!(state, ElementState::Pressed); - } - kittest::Key::Control => { - modifiers.ctrl = matches!(state, ElementState::Pressed); - } - kittest::Key::Shift => { - modifiers.shift = matches!(state, ElementState::Pressed); - } - _ => {} - } - kittest_key_to_egui(key).map(|key| Event::Key { - key, - modifiers: *modifiers, - pressed: matches!(state, ElementState::Pressed), - repeat: false, - physical_key: None, - }) - } - }, - } - } -} - -fn kittest_key_to_egui(value: kittest::Key) -> Option { - use egui::Key as EKey; - use kittest::Key; - match value { - Key::ArrowDown => Some(EKey::ArrowDown), - Key::ArrowLeft => Some(EKey::ArrowLeft), - Key::ArrowRight => Some(EKey::ArrowRight), - Key::ArrowUp => Some(EKey::ArrowUp), - Key::Escape => Some(EKey::Escape), - Key::Tab => Some(EKey::Tab), - Key::Backspace => Some(EKey::Backspace), - Key::Enter => Some(EKey::Enter), - Key::Space => Some(EKey::Space), - Key::Insert => Some(EKey::Insert), - Key::Delete => Some(EKey::Delete), - Key::Home => Some(EKey::Home), - Key::End => Some(EKey::End), - Key::PageUp => Some(EKey::PageUp), - Key::PageDown => Some(EKey::PageDown), - Key::Copy => Some(EKey::Copy), - Key::Cut => Some(EKey::Cut), - Key::Paste => Some(EKey::Paste), - Key::Colon => Some(EKey::Colon), - Key::Comma => Some(EKey::Comma), - Key::Backslash => Some(EKey::Backslash), - Key::Slash => Some(EKey::Slash), - Key::Pipe => Some(EKey::Pipe), - Key::Questionmark => Some(EKey::Questionmark), - Key::OpenBracket => Some(EKey::OpenBracket), - Key::CloseBracket => Some(EKey::CloseBracket), - Key::Backtick => Some(EKey::Backtick), - Key::Minus => Some(EKey::Minus), - Key::Period => Some(EKey::Period), - Key::Plus => Some(EKey::Plus), - Key::Equals => Some(EKey::Equals), - Key::Semicolon => Some(EKey::Semicolon), - Key::Quote => Some(EKey::Quote), - Key::Num0 => Some(EKey::Num0), - Key::Num1 => Some(EKey::Num1), - Key::Num2 => Some(EKey::Num2), - Key::Num3 => Some(EKey::Num3), - Key::Num4 => Some(EKey::Num4), - Key::Num5 => Some(EKey::Num5), - Key::Num6 => Some(EKey::Num6), - Key::Num7 => Some(EKey::Num7), - Key::Num8 => Some(EKey::Num8), - Key::Num9 => Some(EKey::Num9), - Key::A => Some(EKey::A), - Key::B => Some(EKey::B), - Key::C => Some(EKey::C), - Key::D => Some(EKey::D), - Key::E => Some(EKey::E), - Key::F => Some(EKey::F), - Key::G => Some(EKey::G), - Key::H => Some(EKey::H), - Key::I => Some(EKey::I), - Key::J => Some(EKey::J), - Key::K => Some(EKey::K), - Key::L => Some(EKey::L), - Key::M => Some(EKey::M), - Key::N => Some(EKey::N), - Key::O => Some(EKey::O), - Key::P => Some(EKey::P), - Key::Q => Some(EKey::Q), - Key::R => Some(EKey::R), - Key::S => Some(EKey::S), - Key::T => Some(EKey::T), - Key::U => Some(EKey::U), - Key::V => Some(EKey::V), - Key::W => Some(EKey::W), - Key::X => Some(EKey::X), - Key::Y => Some(EKey::Y), - Key::Z => Some(EKey::Z), - Key::F1 => Some(EKey::F1), - Key::F2 => Some(EKey::F2), - Key::F3 => Some(EKey::F3), - Key::F4 => Some(EKey::F4), - Key::F5 => Some(EKey::F5), - Key::F6 => Some(EKey::F6), - Key::F7 => Some(EKey::F7), - Key::F8 => Some(EKey::F8), - Key::F9 => Some(EKey::F9), - Key::F10 => Some(EKey::F10), - Key::F11 => Some(EKey::F11), - Key::F12 => Some(EKey::F12), - Key::F13 => Some(EKey::F13), - Key::F14 => Some(EKey::F14), - Key::F15 => Some(EKey::F15), - Key::F16 => Some(EKey::F16), - Key::F17 => Some(EKey::F17), - Key::F18 => Some(EKey::F18), - Key::F19 => Some(EKey::F19), - Key::F20 => Some(EKey::F20), - Key::F21 => Some(EKey::F21), - Key::F22 => Some(EKey::F22), - Key::F23 => Some(EKey::F23), - Key::F24 => Some(EKey::F24), - Key::F25 => Some(EKey::F25), - Key::F26 => Some(EKey::F26), - Key::F27 => Some(EKey::F27), - Key::F28 => Some(EKey::F28), - Key::F29 => Some(EKey::F29), - Key::F30 => Some(EKey::F30), - Key::F31 => Some(EKey::F31), - Key::F32 => Some(EKey::F32), - Key::F33 => Some(EKey::F33), - Key::F34 => Some(EKey::F34), - Key::F35 => Some(EKey::F35), - _ => None, - } -} - -fn pointer_button_to_egui(value: MouseButton) -> Option { - match value { - MouseButton::Left => Some(egui::PointerButton::Primary), - MouseButton::Right => Some(egui::PointerButton::Secondary), - MouseButton::Middle => Some(egui::PointerButton::Middle), - MouseButton::Back => Some(egui::PointerButton::Extra1), - MouseButton::Forward => Some(egui::PointerButton::Extra2), - MouseButton::Other(_) => None, - } -} diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 0963a949..ac67c8da 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -4,7 +4,6 @@ #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] mod builder; -mod event; #[cfg(feature = "snapshot")] mod snapshot; @@ -14,6 +13,7 @@ use std::fmt::{Debug, Display, Formatter}; use std::time::Duration; mod app_kind; +mod node; mod renderer; #[cfg(feature = "wgpu")] mod texture_to_image; @@ -23,13 +23,13 @@ pub mod wgpu; pub use kittest; use crate::app_kind::AppKind; -use crate::event::EventState; pub use builder::*; +pub use node::*; pub use renderer::*; -use egui::{Modifiers, Pos2, Rect, RepaintCause, Vec2, ViewportId}; -use kittest::{Node, Queryable}; +use egui::{Key, Modifiers, Pos2, Rect, RepaintCause, Vec2, ViewportId}; +use kittest::Queryable; #[derive(Debug, Clone)] pub struct ExceededMaxStepsError { @@ -61,13 +61,13 @@ pub struct Harness<'a, State = ()> { kittest: kittest::State, output: egui::FullOutput, app: AppKind<'a, State>, - event_state: EventState, response: Option, state: State, renderer: Box, max_steps: u64, step_dt: f32, wait_for_pending_images: bool, + queued_events: EventQueue, } impl Debug for Harness<'_, State> { @@ -126,12 +126,12 @@ impl<'a, State> Harness<'a, State> { ), output, response, - event_state: EventState::default(), state, renderer, max_steps, step_dt, wait_for_pending_images, + queued_events: Default::default(), }; // Run the harness until it is stable, ensuring that all Areas are shown and animations are done harness.run_ok(); @@ -227,12 +227,19 @@ impl<'a, State> Harness<'a, State> { /// This will call the app closure with each queued event and /// update the Harness. pub fn step(&mut self) { - let events = self.kittest.take_events(); + let events = std::mem::take(&mut *self.queued_events.lock()); if events.is_empty() { self._step(false); } for event in events { - self.event_state.update(event, &mut self.input); + match event { + EventType::Event(event) => { + self.input.events.push(event); + } + EventType::Modifiers(modifiers) => { + self.input.modifiers = modifiers; + } + } self._step(false); } } @@ -414,52 +421,128 @@ impl<'a, State> Harness<'a, State> { &mut self.state } - /// Press a key. - /// This will create a key down event and a key up event. - pub fn press_key(&mut self, key: egui::Key) { - self.input.events.push(egui::Event::Key { + fn event(&self, event: egui::Event) { + self.queued_events.lock().push(EventType::Event(event)); + } + + fn event_modifiers(&self, event: egui::Event, modifiers: Modifiers) { + let mut queue = self.queued_events.lock(); + queue.push(EventType::Modifiers(modifiers)); + queue.push(EventType::Event(event)); + queue.push(EventType::Modifiers(Modifiers::default())); + } + + fn modifiers(&self, modifiers: Modifiers) { + self.queued_events + .lock() + .push(EventType::Modifiers(modifiers)); + } + + pub fn key_down(&self, key: egui::Key) { + self.event(egui::Event::Key { key, pressed: true, - modifiers: self.input.modifiers, - repeat: false, - physical_key: None, - }); - self.input.events.push(egui::Event::Key { - key, - pressed: false, - modifiers: self.input.modifiers, + modifiers: Modifiers::default(), repeat: false, physical_key: None, }); } - /// Press a key with modifiers. - /// This will create a key-down event, a key-up event, and update the modifiers. - /// - /// NOTE: In contrast to the event fns on [`Node`], this will call [`Harness::step`], in - /// order to properly update modifiers. - pub fn press_key_modifiers(&mut self, modifiers: Modifiers, key: egui::Key) { - // Combine the modifiers with the current modifiers - let previous_modifiers = self.input.modifiers; - self.input.modifiers |= modifiers; - - self.input.events.push(egui::Event::Key { - key, - pressed: true, + pub fn key_down_modifiers(&self, modifiers: Modifiers, key: egui::Key) { + self.event_modifiers( + egui::Event::Key { + key, + pressed: true, + modifiers, + repeat: false, + physical_key: None, + }, modifiers, - repeat: false, - physical_key: None, - }); - self.step(); - self.input.events.push(egui::Event::Key { + ); + } + + pub fn key_up(&self, key: egui::Key) { + self.event(egui::Event::Key { key, pressed: false, - modifiers, + modifiers: Modifiers::default(), repeat: false, physical_key: None, }); + } - self.input.modifiers = previous_modifiers; + pub fn key_up_modifiers(&self, modifiers: Modifiers, key: egui::Key) { + self.event_modifiers( + egui::Event::Key { + key, + pressed: false, + modifiers, + repeat: false, + physical_key: None, + }, + modifiers, + ); + } + + /// Press the given keys in combination. + /// + /// For e.g. [`Key::A`] + [`Key::B`] this would generate: + /// - Press [`Key::A`] + /// - Press [`Key::B`] + /// - Release [`Key::B`] + /// - Release [`Key::A`] + pub fn key_combination(&self, keys: &[Key]) { + for key in keys { + self.key_down(*key); + } + for key in keys.iter().rev() { + self.key_up(*key); + } + } + + /// Press the given keys in combination, with modifiers. + /// + /// For e.g. [`Modifiers::COMMAND`] + [`Key::A`] + [`Key::B`] this would generate: + /// - Press [`Modifiers::COMMAND`] + /// - Press [`Key::A`] + /// - Press [`Key::B`] + /// - Release [`Key::B`] + /// - Release [`Key::A`] + /// - Release [`Modifiers::COMMAND`] + pub fn key_combination_modifiers(&self, modifiers: Modifiers, keys: &[Key]) { + self.modifiers(modifiers); + + for pressed in [true, false] { + for key in keys { + self.event(egui::Event::Key { + key: *key, + pressed, + modifiers, + repeat: false, + physical_key: None, + }); + } + } + + self.modifiers(Modifiers::default()); + } + + /// Press a key. + /// + /// This will create a key down event and a key up event. + pub fn key_press(&self, key: egui::Key) { + self.key_combination(&[key]); + } + + /// Press a key with modifiers. + /// + /// This will + /// - set the modifiers + /// - create a key down event + /// - create a key up event + /// - reset the modifiers + pub fn key_press_modifiers(&self, modifiers: Modifiers, key: egui::Key) { + self.key_combination_modifiers(modifiers, &[key]); } /// Render the last output to an image. @@ -478,6 +561,18 @@ impl<'a, State> Harness<'a, State> { .get(&ViewportId::ROOT) .expect("Missing root viewport") } + + fn root(&self) -> Node<'_> { + Node { + accesskit_node: self.kittest.root(), + queue: &self.queued_events, + } + } + + #[deprecated = "Use `Harness::root` instead."] + pub fn node(&self) -> Node<'_> { + self.root() + } } /// Utilities for stateless harnesses. @@ -526,11 +621,11 @@ impl<'a> Harness<'a> { } } -impl<'t, 'n, State> Queryable<'t, 'n> for Harness<'_, State> +impl<'tree, 'node, State> Queryable<'tree, 'node, Node<'tree>> for Harness<'_, State> where - 'n: 't, + 'node: 'tree, { - fn node(&'n self) -> Node<'t> { - self.kittest_state().node() + fn queryable_node(&'node self) -> Node<'tree> { + self.root() } } diff --git a/crates/egui_kittest/src/node.rs b/crates/egui_kittest/src/node.rs new file mode 100644 index 00000000..384a5dbd --- /dev/null +++ b/crates/egui_kittest/src/node.rs @@ -0,0 +1,162 @@ +use egui::accesskit::ActionRequest; +use egui::mutex::Mutex; +use egui::{accesskit, Modifiers, PointerButton, Pos2}; +use kittest::{debug_fmt_node, AccessKitNode, NodeT}; +use std::fmt::{Debug, Formatter}; + +pub(crate) enum EventType { + Event(egui::Event), + Modifiers(Modifiers), +} + +pub(crate) type EventQueue = Mutex>; + +#[derive(Clone, Copy)] +pub struct Node<'tree> { + pub(crate) accesskit_node: AccessKitNode<'tree>, + pub(crate) queue: &'tree EventQueue, +} + +impl Debug for Node<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + debug_fmt_node(self, f) + } +} + +impl<'tree> NodeT<'tree> for Node<'tree> { + fn accesskit_node(&self) -> AccessKitNode<'tree> { + self.accesskit_node + } + + fn new_related(&self, child_node: AccessKitNode<'tree>) -> Self { + Self { + queue: self.queue, + accesskit_node: child_node, + } + } +} + +impl Node<'_> { + fn event(&self, event: egui::Event) { + self.queue.lock().push(EventType::Event(event)); + } + + fn modifiers(&self, modifiers: Modifiers) { + self.queue.lock().push(EventType::Modifiers(modifiers)); + } + + pub fn hover(&self) { + self.event(egui::Event::PointerMoved(self.rect().center())); + } + + /// Click at the node center with the primary button. + pub fn click(&self) { + self.click_button(PointerButton::Primary); + } + + #[deprecated = "Use `click()` instead."] + pub fn simulate_click(&self) { + self.click(); + } + + pub fn click_secondary(&self) { + self.click_button(PointerButton::Secondary); + } + + pub fn click_button(&self, button: PointerButton) { + self.hover(); + for pressed in [true, false] { + self.event(egui::Event::PointerButton { + pos: self.rect().center(), + button, + pressed, + modifiers: Modifiers::default(), + }); + } + } + + pub fn click_modifiers(&self, modifiers: Modifiers) { + self.click_button_modifiers(PointerButton::Primary, modifiers); + } + + pub fn click_button_modifiers(&self, button: PointerButton, modifiers: Modifiers) { + self.hover(); + self.modifiers(modifiers); + for pressed in [true, false] { + self.event(egui::Event::PointerButton { + pos: self.rect().center(), + button, + pressed, + modifiers, + }); + } + self.modifiers(Modifiers::default()); + } + + /// Click the node via accesskit. + /// + /// This will trigger a [`accesskit::Action::Click`] action. + /// In contrast to `click()`, this can also click widgets that are not currently visible. + pub fn click_accesskit(&self) { + self.event(egui::Event::AccessKitActionRequest( + accesskit::ActionRequest { + target: self.accesskit_node.id(), + action: accesskit::Action::Click, + data: None, + }, + )); + } + + pub fn rect(&self) -> egui::Rect { + let rect = self + .accesskit_node + .bounding_box() + .expect("Every egui node should have a rect"); + egui::Rect { + min: Pos2::new(rect.x0 as f32, rect.y0 as f32), + max: Pos2::new(rect.x1 as f32, rect.y1 as f32), + } + } + + pub fn focus(&self) { + self.event(egui::Event::AccessKitActionRequest(ActionRequest { + action: accesskit::Action::Focus, + target: self.accesskit_node.id(), + data: None, + })); + } + + #[deprecated = "Use `Harness::key_down` instead."] + pub fn key_down(&self, key: egui::Key) { + self.event(egui::Event::Key { + key, + pressed: true, + modifiers: Modifiers::default(), + repeat: false, + physical_key: None, + }); + } + + #[deprecated = "Use `Harness::key_up` instead."] + pub fn key_up(&self, key: egui::Key) { + self.event(egui::Event::Key { + key, + pressed: false, + modifiers: Modifiers::default(), + repeat: false, + physical_key: None, + }); + } + + pub fn type_text(&self, text: &str) { + self.event(egui::Event::Text(text.to_owned())); + } + + pub fn value(&self) -> Option { + self.accesskit_node.value() + } + + pub fn is_focused(&self) -> bool { + self.accesskit_node.is_focused() + } +} diff --git a/crates/egui_kittest/tests/menu.rs b/crates/egui_kittest/tests/menu.rs index fc19e804..48f348a2 100644 --- a/crates/egui_kittest/tests/menu.rs +++ b/crates/egui_kittest/tests/menu.rs @@ -95,7 +95,7 @@ fn menu_close_on_click_outside() { TestMenu::new(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClick)) .into_harness(); - harness.get_by_label("Menu A").simulate_click(); + harness.get_by_label("Menu A").click(); harness.run(); harness @@ -106,9 +106,7 @@ fn menu_close_on_click_outside() { // We should be able to check the checkbox without closing the menu // Click a couple of times, just in case for expect_checked in [true, false, true, false] { - harness - .get_by_label("Checkbox in Submenu C") - .simulate_click(); + harness.get_by_label("Checkbox in Submenu C").click(); harness.run(); assert_eq!(expect_checked, harness.state().checked); } @@ -119,7 +117,7 @@ fn menu_close_on_click_outside() { assert!(harness.query_by_label("Checkbox in Submenu C").is_some()); // Clicking outside should close the menu - harness.get_by_label("Some other label").simulate_click(); + harness.get_by_label("Some other label").click(); harness.run(); assert!(harness.query_by_label("Checkbox in Submenu C").is_none()); } @@ -130,14 +128,14 @@ fn menu_close_on_click() { TestMenu::new(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClick)) .into_harness(); - harness.get_by_label("Menu A").simulate_click(); + harness.get_by_label("Menu A").click(); harness.run(); harness.get_by_label_contains("Submenu B with icon").hover(); harness.run(); // Clicking the button should close the menu (even if ui.close() is not called by the button) - harness.get_by_label("Button in Submenu B").simulate_click(); + harness.get_by_label("Button in Submenu B").click(); harness.run(); assert!(harness.query_by_label("Button in Submenu B").is_none()); } @@ -145,21 +143,19 @@ fn menu_close_on_click() { #[test] fn clicking_submenu_button_should_never_close_menu() { // We test for this since otherwise the menu wouldn't work on touch devices - // The other tests use .hover to open submenus, but this test explicitly uses .simulate_click + // The other tests use .hover to open submenus, but this test explicitly uses .click let mut harness = TestMenu::new(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClick)) .into_harness(); - harness.get_by_label("Menu A").simulate_click(); + harness.get_by_label("Menu A").click(); harness.run(); // Clicking the submenu button should not close the menu - harness - .get_by_label_contains("Submenu B with icon") - .simulate_click(); + harness.get_by_label_contains("Submenu B with icon").click(); harness.run(); - harness.get_by_label("Button in Submenu B").simulate_click(); + harness.get_by_label("Button in Submenu B").click(); harness.run(); assert!(harness.query_by_label("Button in Submenu B").is_none()); } @@ -174,7 +170,7 @@ fn menu_snapshots() { harness.run(); results.add(harness.try_snapshot("menu/closed_hovered")); - harness.get_by_label("Menu A").simulate_click(); + harness.get_by_label("Menu A").click(); harness.run(); results.add(harness.try_snapshot("menu/opened")); diff --git a/crates/egui_kittest/tests/popup.rs b/crates/egui_kittest/tests/popup.rs index 368e8de7..a8d3bcc9 100644 --- a/crates/egui_kittest/tests/popup.rs +++ b/crates/egui_kittest/tests/popup.rs @@ -23,7 +23,7 @@ fn test_interactive_tooltip() { harness.run(); harness.get_by_label("link").hover(); harness.run(); - harness.get_by_label("link").simulate_click(); + harness.get_by_label("link").click(); harness.run(); diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index 50ed1095..5107799f 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -10,19 +10,19 @@ pub fn focus_should_skip_over_disabled_buttons() { ui.add(Button::new("Button 3")); }); - harness.press_key(egui::Key::Tab); + harness.key_press(egui::Key::Tab); harness.run(); let button_1 = harness.get_by_label("Button 1"); assert!(button_1.is_focused()); - harness.press_key(egui::Key::Tab); + harness.key_press(egui::Key::Tab); harness.run(); let button_3 = harness.get_by_label("Button 3"); assert!(button_3.is_focused()); - harness.press_key(egui::Key::Tab); + harness.key_press(egui::Key::Tab); harness.run(); let button_1 = harness.get_by_label("Button 1"); @@ -41,13 +41,13 @@ pub fn focus_should_skip_over_disabled_drag_values() { ui.add(egui::DragValue::new(&mut value_3)); }); - harness.press_key(egui::Key::Tab); + harness.key_press(egui::Key::Tab); harness.run(); let drag_value_1 = harness.get_by(|node| node.numeric_value() == Some(1.0)); assert!(drag_value_1.is_focused()); - harness.press_key(egui::Key::Tab); + harness.key_press(egui::Key::Tab); harness.run(); let drag_value_3 = harness.get_by(|node| node.numeric_value() == Some(3.0)); @@ -103,8 +103,7 @@ fn test_combobox() { results.add(harness.try_snapshot("combobox_opened")); let item_2 = harness.get_by_role_and_label(Role::Button, "Item 2"); - // Node::click doesn't close the popup, so we use simulate_click - item_2.simulate_click(); + item_2.click(); harness.run(); diff --git a/crates/egui_kittest/tests/snapshots/readme_example.png b/crates/egui_kittest/tests/snapshots/readme_example.png index 4b828832..a3d4ca79 100644 --- a/crates/egui_kittest/tests/snapshots/readme_example.png +++ b/crates/egui_kittest/tests/snapshots/readme_example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1d172484712e3e12038f8ff427db8c0073aba124aa1b6be17edcc7dccb12f74 -size 1656 +oid sha256:341658df1dfe665e79180d4540965a986a21de09c9cbc1a8744bdcff1a7c2086 +size 1892 diff --git a/crates/egui_kittest/tests/tests.rs b/crates/egui_kittest/tests/tests.rs index 2b223f45..8c92f431 100644 --- a/crates/egui_kittest/tests/tests.rs +++ b/crates/egui_kittest/tests/tests.rs @@ -1,6 +1,6 @@ use egui::{include_image, Modifiers, Vec2}; use egui_kittest::Harness; -use kittest::{Key, Queryable as _}; +use kittest::Queryable as _; #[test] fn test_shrink() { @@ -39,17 +39,15 @@ fn test_modifiers() { State::default(), ); - harness.get_by_label("Click me").key_down(Key::Command); - // This run isn't necessary, but allows us to test whether modifiers are remembered between frames - harness.run(); - harness.get_by_label("Click me").click(); - harness.get_by_label("Click me").key_up(Key::Command); + harness + .get_by_label("Click me") + .click_modifiers(Modifiers::COMMAND); harness.run(); - harness.press_key_modifiers(Modifiers::COMMAND, egui::Key::Z); + harness.key_press_modifiers(Modifiers::COMMAND, egui::Key::Z); harness.run(); - harness.node().key_combination(&[Key::Command, Key::Y]); + harness.key_combination_modifiers(Modifiers::COMMAND, &[egui::Key::Y]); harness.run(); let state = harness.state(); diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 110eff81..e7082d47 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -1,11 +1,11 @@ use egui::load::SizedTexture; use egui::{ include_image, Align, AtomExt as _, AtomLayout, Button, Color32, ColorImage, Direction, - DragValue, Event, Grid, IntoAtoms as _, Layout, PointerButton, Pos2, Response, Slider, Stroke, + DragValue, Event, Grid, IntoAtoms as _, Layout, PointerButton, Response, Slider, Stroke, StrokeKind, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, }; -use egui_kittest::kittest::{by, Node, Queryable as _}; -use egui_kittest::{Harness, SnapshotResult, SnapshotResults}; +use egui_kittest::kittest::{by, Queryable as _}; +use egui_kittest::{Harness, Node, SnapshotResult, SnapshotResults}; #[test] fn widget_tests() { @@ -278,14 +278,10 @@ impl<'a> VisualTests<'a> { }); self.add("pressed", |harness| { harness.get_next().hover(); - let rect = harness.get_next().bounding_box().unwrap(); - let pos = Pos2::new( - ((rect.x0 + rect.x1) / 2.0) as f32, - ((rect.y0 + rect.y1) / 2.0) as f32, - ); + let rect = harness.get_next().rect(); harness.input_mut().events.push(Event::PointerButton { button: PointerButton::Primary, - pos, + pos: rect.center(), pressed: true, modifiers: Default::default(), });