use plushie::WidgetEvent;
use plushie::prelude::*;
use plushie::test::{TestSession, WidgetTestSession};
use plushie::widget::{EventResult, Widget, WidgetRegistrar, WidgetView};
use serde_json::{Value, json};
#[derive(Default)]
struct NoState;
struct Consumer;
impl Widget for Consumer {
type State = NoState;
type Props = UntypedProps;
fn view(id: &str, _props: &UntypedProps, _state: &NoState) -> View {
column().id(id).child(button("inner", "press")).into()
}
fn handle_event(_event: &Event, _state: &mut NoState) -> EventResult {
EventResult::Consumed
}
}
#[derive(WidgetEvent)]
enum EmitterEvent {
Picked(u64),
}
struct Emitter;
impl Widget for Emitter {
type State = NoState;
type Props = UntypedProps;
fn view(id: &str, _props: &UntypedProps, _state: &NoState) -> View {
column().id(id).child(button("inner", "pick")).into()
}
fn handle_event(_event: &Event, _state: &mut NoState) -> EventResult {
EventResult::emit_event(EmitterEvent::Picked(42))
}
}
struct Passthrough;
impl Widget for Passthrough {
type State = NoState;
type Props = UntypedProps;
fn view(id: &str, _props: &UntypedProps, _state: &NoState) -> View {
column().id(id).child(button("inner", "through")).into()
}
fn handle_event(_event: &Event, _state: &mut NoState) -> EventResult {
EventResult::Ignored
}
}
#[test]
fn consumed_stops_event_before_update() {
let mut session = WidgetTestSession::<Consumer>::start("consume");
session.click("inner");
assert!(
session.events().is_empty(),
"Consumer must suppress the click; harness saw: {:?}",
session.events()
);
}
#[test]
fn emit_rewrites_event_family_and_reaches_update() {
let mut session = WidgetTestSession::<Emitter>::start("emit");
session.click("inner");
let (family, value) = session
.last_event()
.expect("update must see the emitted event");
assert_eq!(family, "picked", "family must match the emitted variant");
assert_eq!(value, &json!(42), "payload must match emit_event's value");
}
#[test]
fn ignored_falls_through_to_update_unchanged() {
let mut session = WidgetTestSession::<Passthrough>::start("through");
session.click("inner");
let (family, value) = session
.last_event()
.expect("update must see the original click");
assert_eq!(
family, "click",
"Ignored must forward the event's original family"
);
assert_eq!(value, &Value::Null, "click events carry a null payload");
}
#[derive(WidgetEvent)]
enum OuterEvent {
Payment(u64),
}
struct Inner;
impl Widget for Inner {
type State = NoState;
type Props = UntypedProps;
fn view(id: &str, _props: &UntypedProps, _state: &NoState) -> View {
column().id(id).child(button("pay-btn", "Pay")).into()
}
fn handle_event(_event: &Event, _state: &mut NoState) -> EventResult {
EventResult::Ignored
}
}
struct Outer;
impl Widget for Outer {
type State = NoState;
type Props = UntypedProps;
fn view(id: &str, _props: &UntypedProps, _state: &NoState) -> View {
let inner_placeholder = WidgetView::<Inner>::new("inner").placeholder();
column().id(id).child(inner_placeholder).into()
}
fn handle_event(event: &Event, _state: &mut NoState) -> EventResult {
match event.widget_match() {
Some(Click(_)) => EventResult::emit_event(OuterEvent::Payment(42)),
_ => EventResult::Ignored,
}
}
}
struct NestedApp {
events: Vec<Event>,
}
impl App for NestedApp {
type Model = Self;
fn init() -> (Self, Command) {
(Self { events: Vec::new() }, Command::None)
}
fn update(model: &mut Self, event: Event) -> Command {
model.events.push(event);
Command::None
}
fn view(_model: &Self, widgets: &mut WidgetRegistrar) -> ViewList {
let outer = WidgetView::<Outer>::new("outer").register(widgets);
let _inner_register = WidgetView::<Inner>::new("inner");
let _ = _inner_register.register(widgets);
window("main")
.child(column().id("root").child(outer))
.into()
}
}
#[test]
fn nested_outer_intercepts_inner_click_and_emits_with_outer_identity() {
let mut session = TestSession::<NestedApp>::start();
session.click("pay-btn");
let widget_events: Vec<&Event> = session
.model()
.events
.iter()
.filter(|e| matches!(e, Event::Widget(_)))
.collect();
assert_eq!(
widget_events.len(),
1,
"expected exactly one widget event (the emitted Payment), got {:?}",
session.model().events
);
let widget = widget_events[0].as_widget().expect("Widget event");
assert_eq!(
widget.event_type.as_family(),
"payment",
"emitted family must be the outer's transformed name, not the inner click's"
);
assert_eq!(
widget.value,
json!(42),
"emitted payload must be Outer's transformed value"
);
assert_eq!(
widget.scoped_id.id, "outer",
"emitted event's local id must be the interceptor's, not the inner target's"
);
assert_eq!(
widget.scoped_id.window_id.as_deref(),
Some("main"),
"window_id must survive the Emit re-emit"
);
assert!(
!widget.scoped_id.scope.iter().any(|s| s == "inner"),
"emitted event's scope must not include Inner; found {:?}",
widget.scoped_id.scope
);
}