Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tweak TestHarness API and docs #790

Merged
merged 2 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 74 additions & 56 deletions masonry/src/testing/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,6 @@ use crate::tracing_backend::try_init_test_tracing;
use crate::widget::{WidgetMut, WidgetRef};
use crate::{Color, Handled, Point, Size, Vec2, Widget, WidgetId};

/// Default canvas size for tests.
pub const HARNESS_DEFAULT_SIZE: Size = Size::new(400., 400.);

/// Default background color for tests.
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 a [`RenderRoot`] for testing.
Expand Down Expand Up @@ -118,6 +112,11 @@ pub struct TestHarness {
title: String,
}

pub struct TestHarnessParams {
pub window_size: Size,
pub background_color: Color,
}

/// Assert a snapshot of a rendered frame of your app.
///
/// This macro takes a test harness and a name, renders the current state of the app,
Expand All @@ -141,33 +140,50 @@ macro_rules! assert_render_snapshot {
};
}

impl TestHarnessParams {
/// Default canvas size for tests.
pub const DEFAULT_SIZE: Size = Size::new(400., 400.);

/// Default background color for tests.
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::rgb8(0x29, 0x29, 0x29);
}

impl Default for TestHarnessParams {
fn default() -> Self {
Self {
window_size: Self::DEFAULT_SIZE,
background_color: Self::DEFAULT_BACKGROUND_COLOR,
}
}
}

impl TestHarness {
/// Builds harness with given root widget.
///
/// Window size will be [`HARNESS_DEFAULT_SIZE`].
/// Background color will be [`HARNESS_DEFAULT_BACKGROUND_COLOR`].
/// Window size will be [`Self::DEFAULT_SIZE`].
/// Background color will be [`Self::DEFAULT_BACKGROUND_COLOR`].
pub fn create(root_widget: impl Widget) -> Self {
Self::create_with(
root_widget,
HARNESS_DEFAULT_SIZE,
HARNESS_DEFAULT_BACKGROUND_COLOR,
)
Self::create_with(root_widget, TestHarnessParams::default())
}

// TODO - Remove
/// Builds harness with given root widget and window size.
pub fn create_with_size(root_widget: impl Widget, window_size: Size) -> Self {
Self::create_with(root_widget, window_size, HARNESS_DEFAULT_BACKGROUND_COLOR)
Self::create_with(
root_widget,
TestHarnessParams {
window_size,
..Default::default()
},
)
}

/// Builds harness with given root widget, canvas size and background color.
pub fn create_with(
root_widget: impl Widget,
window_size: Size,
background_color: Color,
) -> Self {
pub fn create_with(root_widget: impl Widget, params: TestHarnessParams) -> Self {
let mouse_state = PointerState::empty();
let window_size = PhysicalSize::new(window_size.width as _, window_size.height as _);
let window_size = PhysicalSize::new(
params.window_size.width as _,
params.window_size.height as _,
);

// If there is no default tracing subscriber, we set our own. If one has
// already been set, we get an error which we swallow.
Expand All @@ -194,7 +210,7 @@ impl TestHarness {
),
mouse_state,
window_size,
background_color,
background_color: params.background_color,
action_queue: VecDeque::new(),
has_ime_session: false,
ime_rect: Default::default(),
Expand Down Expand Up @@ -306,7 +322,6 @@ impl TestHarness {
// TODO - fix window_size
let (width, height) = (self.window_size.width, self.window_size.height);
let render_params = vello::RenderParams {
// TODO - Parameterize
base_color: self.background_color,
width,
height,
Expand Down Expand Up @@ -417,22 +432,50 @@ impl TestHarness {
/// Send events that lead to a given widget being clicked.
///
/// Combines [`mouse_move`](Self::mouse_move), [`mouse_button_press`](Self::mouse_button_press), and [`mouse_button_release`](Self::mouse_button_release).
///
/// ## Panics
///
/// - If the widget is not found in the tree.
/// - If the widget is stashed.
/// - If the widget doesn't accept pointer events.
/// - If the widget is scrolled out of view.
#[track_caller]
pub fn mouse_click_on(&mut self, id: WidgetId) {
let widget_rect = self.get_widget(id).ctx().window_layout_rect();
let widget_center = widget_rect.center();

self.mouse_move(widget_center);
self.mouse_move_to(id);
self.mouse_button_press(PointerButton::Primary);
self.mouse_button_release(PointerButton::Primary);
}

/// Use [`mouse_move`](Self::mouse_move) to set the internal mouse pos to the center of the given widget.
///
/// ## Panics
///
/// - If the widget is not found in the tree.
/// - If the widget is stashed.
/// - If the widget doesn't accept pointer events.
/// - If the widget is scrolled out of view.
#[track_caller]
pub fn mouse_move_to(&mut self, id: WidgetId) {
// FIXME - handle case where the widget isn't visible
// FIXME - assert that the widget correctly receives the event otherwise?
let widget_rect = self.get_widget(id).ctx().window_layout_rect();
let widget = self.get_widget(id);
let widget_rect = widget.ctx().window_layout_rect();
let widget_center = widget_rect.center();

if !widget.ctx().accepts_pointer_interaction() {
panic!("Widget {id} doesn't accept pointer events");
}
if widget.ctx().is_disabled() {
panic!("Widget {id} is disabled");
}
if self
.render_root
.get_root_widget()
.find_widget_at_pos(widget_center)
.map(|w| w.id())
!= Some(id)
{
panic!("Widget {id} is not visible");
}

self.mouse_move(widget_center);
}

Expand Down Expand Up @@ -472,36 +515,13 @@ impl TestHarness {
self.process_signals();
}

// TODO - Fold into move_timers_forward
/// 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();
self.process_signals();
}

#[cfg(FALSE)]
/// Simulate the passage of time.
///
/// If you create any timer in a widget, this method is the only way to trigger
/// them in unit tests. The testing model assumes that everything else executes
/// instantly, and timers are never triggered "spontaneously".
///
/// **(TODO - Doesn't move animations forward.)**
pub fn move_timers_forward(&mut self, duration: Duration) {
// TODO - handle animations
let tokens = self
.mock_app
.window
.mock_timer_queue
.as_mut()
.unwrap()
.move_forward(duration);
for token in tokens {
self.process_event(Event::Timer(token));
}
}

// --- MARK: GETTERS ---

/// Return a [`WidgetRef`] to the root widget.
Expand Down Expand Up @@ -582,9 +602,7 @@ impl TestHarness {
self.render_root.edit_widget(id, f)
}

/// Pop the next action from the queue.
///
/// **Note:** Actions are still a WIP feature.
/// Pop the oldest [`Action`] emitted by the widget tree.
pub fn pop_action(&mut self) -> Option<(Action, WidgetId)> {
self.action_queue.pop_front()
}
Expand Down
4 changes: 2 additions & 2 deletions masonry/src/testing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ mod screenshots;
#[cfg(not(tarpaulin_include))]
mod snapshot_utils;

pub use harness::{TestHarness, HARNESS_DEFAULT_BACKGROUND_COLOR, HARNESS_DEFAULT_SIZE};
pub use harness::{TestHarness, TestHarnessParams};
pub use helper_widgets::{ModularWidget, Record, Recorder, Recording, ReplaceChild, TestWidgetExt};

use crate::WidgetId;

/// Convenience function to return an arrays of unique widget ids.
/// Convenience function to return an array of unique widget ids.
pub fn widget_ids<const N: usize>() -> [WidgetId; N] {
std::array::from_fn(|_| WidgetId::next())
}
Loading