Update test-related documentation (#698)

This commit is contained in:
Olivier FAURE 2024-10-21 12:05:32 +02:00 committed by GitHub
parent 38415d65ca
commit 7fef215fb0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 112 additions and 45 deletions

View File

@ -29,8 +29,6 @@ use crate::tracing_backend::try_init_test_tracing;
use crate::widget::{WidgetMut, WidgetRef};
use crate::{Color, Handled, Point, Size, Vec2, Widget, WidgetId};
// TODO - Get shorter names
// TODO - Make them associated consts
/// Default canvas size for tests.
pub const HARNESS_DEFAULT_SIZE: Size = Size::new(400., 400.);
@ -39,37 +37,32 @@ pub const HARNESS_DEFAULT_BACKGROUND_COLOR: Color = Color::rgb8(0x29, 0x29, 0x29
/// A safe headless environment to test widgets in.
///
/// `TestHarness` is a type that simulates an [`AppRoot`](crate::AppRoot)
/// with a single window.
/// `TestHarness` is a type that simulates a [`RenderRoot`] for testing.
///
/// ## Workflow
///
/// One of the main goals of masonry is to provide primitives that allow application
/// developers to test their app in a convenient and intuitive way. The basic testing
/// workflow is as follows:
/// One of the main goals of Masonry is to provide primitives that allow application
/// developers to test their app in a convenient and intuitive way.
/// The basic testing workflow is as follows:
///
/// - Create a harness with some widget.
/// - Send events to the widget as if you were a user interacting with a window.
/// (rewrite passes are handled automatically.)
/// (Rewrite passes are handled automatically.)
/// - Check that the state of the widget graph matches what you expect.
///
/// You can do that last part in a few different ways. You can get a [`WidgetRef`] to
/// a specific widget through methods like [`try_get_widget`](Self::try_get_widget). [`WidgetRef`] implements
/// `Debug`, so you can check the state of an entire tree with something like the `insta`
/// crate.
/// You can do that last part in a few different ways.
/// You can get a [`WidgetRef`] to a specific widget through methods like [`try_get_widget`](Self::try_get_widget).
/// [`WidgetRef`] implements `Debug`, so you can check the state of an entire tree with something like the [`insta`] crate.
///
/// You can also render the widget tree directly with the [`render`](Self::render) method. Masonry also
/// provides the [`assert_render_snapshot`] macro, which performs snapshot testing on the
/// You can also render the widget tree directly with the [`render`](Self::render) method.
/// Masonry also provides the [`assert_render_snapshot`] macro, which performs snapshot testing on the
/// rendered widget tree automatically.
///
/// ## Fidelity
///
/// `TestHarness` tries to act like the normal masonry environment. It will run the same passes as the normal app after every user event and animation.
///
/// The passage of time is simulated with the [`move_timers_forward`](Self::move_timers_forward) methods. **(TODO -
/// Doesn't move animations forward.)**
///
/// **(TODO - Painting invalidation might not be accurate.)**
/// Animations can be simulated with the [`animate_ms`](Self::animate_ms) method.
///
/// One minor difference is that paint only happens when the user explicitly calls rendering
/// methods, whereas in a normal applications you could reasonably expect multiple paint calls
@ -132,6 +125,7 @@ pub struct TestHarness {
///
/// If a screenshot already exists, the rendered value is compared against this screenshot.
/// The assert passes if both are equal; otherwise, a diff file is created.
/// If the test is run again and the new rendered value matches the old screenshot, the diff file is deleted.
///
/// If a screenshot doesn't exist, the assert will fail; the new screenshot is stored as
/// `./screenshots/<test_name>.new.png`, and must be renamed before the assert will pass.
@ -214,33 +208,30 @@ impl TestHarness {
// --- MARK: PROCESS EVENTS ---
// FIXME - The docs for these three functions are copy-pasted. Rewrite them.
/// Send an event to the widget.
/// Send a [`WindowEvent`] to the simulated window.
///
/// If this event triggers update events, they will also be dispatched,
/// as will any resulting commands. Commands created as a result of this event
/// will also be dispatched.
/// If this event triggers rewrite passes, they will also run as normal.
// TODO - Link to tutorial about rewrite passes - See #632
pub fn process_window_event(&mut self, event: WindowEvent) -> Handled {
let handled = self.render_root.handle_window_event(event);
self.process_signals();
handled
}
/// Send an event to the widget.
/// Send a [`PointerEvent`] to the simulated window.
///
/// If this event triggers update events, they will also be dispatched,
/// as will any resulting commands. Commands created as a result of this event
/// will also be dispatched.
/// If this event triggers rewrite passes, they will also run as normal.
// TODO - Link to tutorial about rewrite passes - See #632
pub fn process_pointer_event(&mut self, event: PointerEvent) -> Handled {
let handled = self.render_root.handle_pointer_event(event);
self.process_signals();
handled
}
/// Send an event to the widget.
/// Send a [`TextEvent`] to the simulated window.
///
/// If this event triggers update events, they will also be dispatched,
/// as will any resulting commands. Commands created as a result of this event
/// will also be dispatched.
/// If this event triggers rewrite passes, they will also run as normal.
// TODO - Link to tutorial about rewrite passes - See #632
pub fn process_text_event(&mut self, event: TextEvent) -> Handled {
let handled = self.render_root.handle_text_event(event);
self.process_signals();
@ -441,6 +432,7 @@ impl TestHarness {
// TODO - Handle complicated IME
// TODO - Mock Winit keyboard events
/// Send a [`TextEvent`] for each character in the given string.
pub fn keyboard_type_chars(&mut self, text: &str) {
// For each character
for c in text.split("").filter(|s| !s.is_empty()) {
@ -449,13 +441,33 @@ impl TestHarness {
}
}
/// Sets the focused widget.
///
/// ## Panics
///
/// If the widget is not found in the tree or can't be focused.
// TODO - Link to focus definition in tutorial
#[track_caller]
pub fn focus_on(&mut self, id: Option<WidgetId>) {
if let Some(id) = id {
let arena = &self.render_root.widget_arena;
let Some(state) = arena.widget_states.find(id.to_raw()) else {
panic!("Cannot focus widget {id}: widget not found in tree");
};
if state.item.is_stashed {
panic!("Cannot focus widget {id}: widget is stashed");
}
if state.item.is_disabled {
panic!("Cannot focus widget {id}: widget is disabled");
}
}
self.render_root.global_state.next_focused_widget = id;
self.render_root.run_rewrite_passes();
self.process_signals();
}
// TODO - Fold into move_timers_forward
/// Send animation events to the widget tree
/// Run an animation pass on the widget tree.
pub fn animate_ms(&mut self, ms: u64) {
run_update_anim_pass(&mut self.render_root, ms * 1_000_000);
self.render_root.run_rewrite_passes();
@ -486,12 +498,12 @@ impl TestHarness {
// --- MARK: GETTERS ---
/// Return the root widget.
/// Return a [`WidgetRef`] to the root widget.
pub fn root_widget(&self) -> WidgetRef<'_, dyn Widget> {
self.render_root.get_root_widget()
}
/// Return the widget with the given id.
/// Return a [`WidgetRef`] to the widget with the given id.
///
/// ## Panics
///
@ -503,24 +515,26 @@ impl TestHarness {
.unwrap_or_else(|| panic!("could not find widget {}", id))
}
/// Try to return the widget with the given id.
/// Try to return a [`WidgetRef`] to the widget with the given id.
pub fn try_get_widget(&self, id: WidgetId) -> Option<WidgetRef<'_, dyn Widget>> {
self.render_root.get_widget(id)
}
// TODO - link to focus documentation.
/// Return the widget that receives keyboard events.
// TODO - Link to focus definition in tutorial
/// Return a [`WidgetRef`] to the widget that receives keyboard events.
pub fn focused_widget(&self) -> Option<WidgetRef<'_, dyn Widget>> {
self.root_widget()
.find_widget_by_id(self.render_root.global_state.focused_widget?)
}
// TODO - Multiple pointers
/// Return a [`WidgetRef`] to the widget which captures pointer events.
// TODO - Link to pointer capture definition in tutorial
pub fn pointer_capture_target(&self) -> Option<WidgetRef<'_, dyn Widget>> {
self.render_root
.get_widget(self.render_root.global_state.pointer_capture_target?)
}
/// Return the id of the widget which captures pointer events.
// TODO - This is kinda redundant with the above
pub fn pointer_capture_target_id(&self) -> Option<WidgetId> {
self.render_root.global_state.pointer_capture_target
@ -562,29 +576,41 @@ impl TestHarness {
self.render_root.edit_widget(id, f)
}
/// Pop next action from the queue
/// Pop the next action from the queue.
///
/// Note: Actions are still a WIP feature.
/// **Note:** Actions are still a WIP feature.
pub fn pop_action(&mut self) -> Option<(Action, WidgetId)> {
self.action_queue.pop_front()
}
/// Return the app's current cursor icon.
///
/// The cursor icon is the icon that would be displayed to indicate the mouse
/// position in a visual environment.
pub fn cursor_icon(&self) -> CursorIcon {
self.render_root.cursor_icon()
}
/// Return whether the app has an IME session in progress.
///
/// This usually means that a widget which [accepts text input](Widget::accepts_text_input) is focused.
pub fn has_ime_session(&self) -> bool {
self.has_ime_session
}
/// Return the rectangle of the IME session.
///
/// This is usually the layout rectangle of the focused widget.
pub fn ime_rect(&self) -> (LogicalPosition<f64>, LogicalSize<f64>) {
self.ime_rect
}
/// Return the size of the simulated window.
pub fn window_size(&self) -> PhysicalSize<u32> {
self.window_size
}
/// Return the title of the simulated window.
pub fn title(&self) -> std::string::String {
self.title.clone()
}
@ -600,6 +626,7 @@ impl TestHarness {
/// * `test_file_path`: file path the current test is in.
/// * `test_module_path`: import path of the module the current test is in.
/// * `test_name`: arbitrary name; second argument of [`assert_render_snapshot`].
#[doc(hidden)]
#[track_caller]
pub fn check_render_snapshot(
&mut self,

View File

@ -8,15 +8,11 @@
//! Note: Some of these types are undocumented. They're meant to help maintainers of
//! Masonry, not to be user-facing.
#![allow(missing_docs)]
#![allow(unused)]
use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
use accesskit::{NodeBuilder, Role};
use accesskit_winit::Event;
use smallvec::SmallVec;
use tracing::trace_span;
use vello::Scene;
@ -68,10 +64,14 @@ pub struct ModularWidget<S> {
/// A widget that can replace its child on command
pub struct ReplaceChild {
child: WidgetPod<Box<dyn Widget>>,
#[allow(dead_code)]
// reason: This is probably bit-rotted code. Next version will SizedBox with WidgetMut instead.
replacer: Box<dyn Fn() -> WidgetPod<Box<dyn Widget>>>,
}
/// A widget that records each time one of its methods is called.
/// A wrapper widget that records each time one of its methods is called.
///
/// Its intent is to let you observe the methods called on a widget in a test.
///
/// Make one like this:
///
@ -94,6 +94,8 @@ pub struct Recorder<W> {
}
/// A recording of widget method calls.
///
/// Internally stores a queue of [`Records`](Record).
#[derive(Debug, Clone, Default)]
pub struct Recording(Rc<RefCell<VecDeque<Record>>>);
@ -114,7 +116,9 @@ pub enum Record {
Access,
}
/// like `WidgetExt` but just for this one thing
/// External trait implemented for all widgets.
///
/// Implements helper methods useful for unit testing.
pub trait TestWidgetExt: Widget + Sized + 'static {
fn record(self, recording: &Recording) -> Recorder<Self> {
Recorder {
@ -131,6 +135,10 @@ pub trait TestWidgetExt: Widget + Sized + 'static {
impl<W: Widget + 'static> TestWidgetExt for W {}
impl<S> ModularWidget<S> {
/// Create a new `ModularWidget`.
///
/// By default none of its methods do anything, and its layout method returns
/// a static 100x100 size.
pub fn new(state: S) -> Self {
ModularWidget {
state,
@ -151,22 +159,36 @@ impl<S> ModularWidget<S> {
children: None,
}
}
}
/// Builder methods.
///
/// Each method takes a flag which is then returned by the matching Widget method.
impl<S> ModularWidget<S> {
/// See [`Widget::accepts_pointer_interaction`]
pub fn accepts_pointer_interaction(mut self, flag: bool) -> Self {
self.accepts_pointer_interaction = flag;
self
}
/// See [`Widget::accepts_focus`]
pub fn accepts_focus(mut self, flag: bool) -> Self {
self.accepts_focus = flag;
self
}
/// See [`Widget::accepts_text_input`]
pub fn accepts_text_input(mut self, flag: bool) -> Self {
self.accepts_text_input = flag;
self
}
}
/// Builder methods.
///
/// Each method takes a callback that matches the behavior of the matching Widget method.
impl<S> ModularWidget<S> {
/// See [`Widget::on_pointer_event`]
pub fn pointer_event_fn(
mut self,
f: impl FnMut(&mut S, &mut EventCtx, &PointerEvent) + 'static,
@ -175,6 +197,7 @@ impl<S> ModularWidget<S> {
self
}
/// See [`Widget::on_text_event`]
pub fn text_event_fn(
mut self,
f: impl FnMut(&mut S, &mut EventCtx, &TextEvent) + 'static,
@ -183,6 +206,7 @@ impl<S> ModularWidget<S> {
self
}
/// See [`Widget::on_access_event`]
pub fn access_event_fn(
mut self,
f: impl FnMut(&mut S, &mut EventCtx, &AccessEvent) + 'static,
@ -191,11 +215,13 @@ impl<S> ModularWidget<S> {
self
}
/// See [`Widget::on_anim_frame`]
pub fn anim_frame_fn(mut self, f: impl FnMut(&mut S, &mut UpdateCtx, u64) + 'static) -> Self {
self.on_anim_frame = Some(Box::new(f));
self
}
/// See [`Widget::register_children`]
pub fn register_children_fn(
mut self,
f: impl FnMut(&mut S, &mut RegisterCtx) + 'static,
@ -204,11 +230,13 @@ impl<S> ModularWidget<S> {
self
}
/// See [`Widget::update`]
pub fn update_fn(mut self, f: impl FnMut(&mut S, &mut UpdateCtx, &Update) + 'static) -> Self {
self.update = Some(Box::new(f));
self
}
/// See [`Widget::layout`]
pub fn layout_fn(
mut self,
f: impl FnMut(&mut S, &mut LayoutCtx, &BoxConstraints) -> Size + 'static,
@ -217,21 +245,25 @@ impl<S> ModularWidget<S> {
self
}
/// See [`Widget::compose`]
pub fn compose_fn(mut self, f: impl FnMut(&mut S, &mut ComposeCtx) + 'static) -> Self {
self.compose = Some(Box::new(f));
self
}
/// See [`Widget::paint`]
pub fn paint_fn(mut self, f: impl FnMut(&mut S, &mut PaintCtx, &mut Scene) + 'static) -> Self {
self.paint = Some(Box::new(f));
self
}
/// See [`Widget::accessibility_role`]
pub fn role_fn(mut self, f: impl Fn(&S) -> Role + 'static) -> Self {
self.role = Some(Box::new(f));
self
}
/// See [`Widget::accessibility`]
pub fn access_fn(
mut self,
f: impl FnMut(&mut S, &mut AccessCtx, &mut NodeBuilder) + 'static,
@ -240,6 +272,7 @@ impl<S> ModularWidget<S> {
self
}
/// See [`Widget::children_ids`]
pub fn children_fn(
mut self,
children: impl Fn(&S) -> SmallVec<[WidgetId; 16]> + 'static,
@ -383,6 +416,10 @@ impl<S: 'static> Widget for ModularWidget<S> {
}
impl ReplaceChild {
/// Create a new `ReplaceChild` widget.
///
/// The `child` is the initial child widget, and `f` is a function that
/// returns a new widget to replace it with.
pub fn new<W: Widget + 'static>(child: impl Widget, f: impl Fn() -> W + 'static) -> Self {
let child = WidgetPod::new(child).boxed();
let replacer = Box::new(move || WidgetPod::new(f()).boxed());
@ -436,14 +473,17 @@ impl Widget for ReplaceChild {
}
impl Recording {
/// True if no events have been recorded.
pub fn is_empty(&self) -> bool {
self.0.borrow().is_empty()
}
/// The number of events in the recording.
pub fn len(&self) -> usize {
self.0.borrow().len()
}
/// Clear recorded events.
pub fn clear(&self) {
self.0.borrow_mut().clear();
}