pub struct WidgetHarness<Id> { /* private fields */ }Expand description
Drives a bare Interaction<Id> against a Headless backend: queues synthetic input,
steps a frame through a caller-supplied draw closure, and reads back the rendered view.
The sibling of retroglyph_core::testing::TestHarness for testing a widget (or a bare
Interaction<Id>) directly, without wrapping it in an App
first. Where TestHarness hands each frame to App::update, WidgetHarness hands each frame
a Ui<'_, '_, Id> built from its own Interaction<Id> and Headless surface, and
leaves whatever state a widget needs (a MenuState, a selection index, …) owned by the
caller’s closure instead of an App impl.
§Frame timing differs from TestHarness
TestHarness::step drains at most one queued Event per call, because it’s reproducing how
a real App sees input trickle in one Backend push at a time. WidgetHarness::step feeds
Interaction::handle_event directly, with no Backend queue in between, so nothing forces
that one-event-per-frame pacing: step drains its entire queue in one call,
between begin_frame and the draw closure – Interaction’s
own docs call this “the realistic call pattern” (see Interaction’s
press_and_release_in_separate_frames_still_resolves_a_click test), and it’s load-bearing for
Tab: FocusRing’s registration order only finalizes inside begin_frame, so a Tab fed before
it would still cycle through the previous frame’s stale order. One consequence: a pointer
gesture (click, hover) fed to step resolves on the following step call, not the one that
fed it, since the flags handle_event sets aren’t read until the next begin_frame; a key
press that moves focus (Tab) or a live-state field like
wants_pointer update immediately, within the same step. A
gesture that needs to be observed mid-flight (a drag, a hover that switches mid-scroll) still
needs its own mouse_move/step pair per intermediate
position, exactly like driving Interaction by hand.
§Examples
use retroglyph_core::grid::Rect;
use retroglyph_ui::testing::WidgetHarness;
use retroglyph_ui::widget::{Button, InteractiveWidget};
#[derive(Clone, Copy, PartialEq, Eq)]
enum Id {
Save,
}
let mut harness = WidgetHarness::<Id>::new(10, 3);
let area = Rect::new(0, 0, 7, 1);
let button = Button::new("Save");
// Frame 1: register the button's hit rect (nothing queued yet).
harness.step(|ui| ui.show(area, Id::Save, &button));
harness.click(2, 0);
// Frame 2: feeds the click; the flags it sets aren't read until the next `begin_frame`.
harness.step(|ui| ui.show(area, Id::Save, &button));
// Frame 3: resolves it.
let response = harness.step(|ui| ui.show(area, Id::Save, &button));
assert!(response.clicked());Implementations§
Source§impl<Id> WidgetHarness<Id>
impl<Id> WidgetHarness<Id>
Sourcepub fn new(width: u16, height: u16) -> Self
pub fn new(width: u16, height: u16) -> Self
Creates a harness with a width x height Headless backend and a fresh
Interaction<Id>.
Sourcepub fn push_event(&mut self, event: Event)
pub fn push_event(&mut self, event: Event)
Queues a synthetic event for the next step call.
Only queues: nothing sees it until a frame runs. Prefer the typed helpers
(click, key, mouse_move) unless the
Event variant needed isn’t one of them.
Sourcepub fn click(&mut self, x: u16, y: u16)
pub fn click(&mut self, x: u16, y: u16)
Queues a left-button click (press then release) at (x, y), with no modifiers.
Both events are drained by the same step call, but (see “Frame timing” on
WidgetHarness) that only sets the underlying flags: the click itself resolves on the
step after that one, once its begin_frame reads them.
Queues a click (press then release) with button at (x, y), with no modifiers.
Sourcepub fn mouse_move(&mut self, x: u16, y: u16)
pub fn mouse_move(&mut self, x: u16, y: u16)
Queues a pointer move to (x, y), with no buttons held.
Sourcepub fn key_with(&mut self, code: KeyCode, modifiers: KeyModifiers)
pub fn key_with(&mut self, code: KeyCode, modifiers: KeyModifiers)
Queues a key press of code with modifiers.
Sourcepub const fn interaction(&self) -> &Interaction<Id>
pub const fn interaction(&self) -> &Interaction<Id>
The underlying Interaction<Id>, for anything not wrapped directly (focus queries,
Interaction::hovered).
Sourcepub fn view(&self) -> String
pub fn view(&self) -> String
The rendered view as of the last step call (see
Headless::format_view).
Sourcepub fn readable_view(&self) -> String
pub fn readable_view(&self) -> String
view, with Headless::SPACE_GLYPH converted back to a literal space.
See retroglyph_core::testing::TestHarness::readable_view for why this exists: view
renders a blank cell as · so layout is visible in text diffs, which defeats an ordinary
plain-text assertion like view.contains("Save").
Source§impl<Id: Copy + PartialEq> WidgetHarness<Id>
impl<Id: Copy + PartialEq> WidgetHarness<Id>
Sourcepub fn step<R>(&mut self, f: impl FnOnce(&mut Ui<'_, '_, Id>) -> R) -> R
pub fn step<R>(&mut self, f: impl FnOnce(&mut Ui<'_, '_, Id>) -> R) -> R
Runs exactly one frame: drains every currently queued event into
Interaction::handle_event, then runs f inside
Interaction::begin_frame/end_frame and presents.
See “Frame timing” on WidgetHarness for why the whole queue drains at once rather than
one event per call.