use crossterm::event::{
Event as CtEvent, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, MouseButton,
MouseEvent as CtMouseEvent, MouseEventKind,
};
use rdom_core::{ListenerOptions, Position, Selection};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use crate::TuiDom;
use crate::layout::Size;
use crate::render::{Terminal, TestBackend};
use crate::runtime::app::App;
use crate::runtime::builtins::form;
use crate::style::{Stylesheet, TuiStyle};
fn key(code: KeyCode) -> CtEvent {
CtEvent::Key(KeyEvent {
code,
modifiers: KeyModifiers::empty(),
kind: KeyEventKind::Press,
state: KeyEventState::empty(),
})
}
fn click(x: u16, y: u16) -> Vec<CtEvent> {
vec![
CtEvent::Mouse(CtMouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: x,
row: y,
modifiers: KeyModifiers::empty(),
}),
CtEvent::Mouse(CtMouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: x,
row: y,
modifiers: KeyModifiers::empty(),
}),
]
}
fn test_app(dom: TuiDom, sheet: Stylesheet) -> App<TestBackend> {
let backend = TestBackend::new(40, 5);
let terminal = Terminal::new(backend).unwrap();
App::with_backend(dom, sheet, terminal).unwrap()
}
#[test]
fn click_on_input_type_submit_fires_submit_event() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let btn = dom.create_element("input");
dom.set_attribute(btn, "type", "submit").unwrap();
dom.append_child(form, btn).unwrap();
dom.append_child(root, form).unwrap();
let sheet = Stylesheet::new().rule_unchecked(
"input[type=submit]",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(1)),
);
let mut app = test_app(dom, sheet);
let fired = Rc::new(Cell::new(0u32));
let f = fired.clone();
app.dom_mut()
.add_event_listener(form, "submit", ListenerOptions::default(), move |_| {
f.set(f.get() + 1);
})
.unwrap();
app.draw_if_dirty().unwrap();
for ev in click(1, 0) {
app.handle_event(ev);
}
assert_eq!(fired.get(), 1);
}
#[test]
fn click_on_button_with_no_type_submits_form() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let btn = dom.create_element("button");
let label = dom.create_text_node("Go");
dom.append_child(btn, label).unwrap();
dom.append_child(form, btn).unwrap();
dom.append_child(root, form).unwrap();
let sheet = Stylesheet::new().rule_unchecked(
"button",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(1)),
);
let mut app = test_app(dom, sheet);
let fired = Rc::new(Cell::new(false));
let f = fired.clone();
app.dom_mut()
.add_event_listener(form, "submit", ListenerOptions::default(), move |_| {
f.set(true);
})
.unwrap();
app.draw_if_dirty().unwrap();
for ev in click(1, 0) {
app.handle_event(ev);
}
assert!(fired.get());
}
#[test]
fn click_on_button_type_button_does_not_submit() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let btn = dom.create_element("button");
dom.set_attribute(btn, "type", "button").unwrap();
dom.append_child(form, btn).unwrap();
dom.append_child(root, form).unwrap();
let sheet = Stylesheet::new().rule_unchecked(
"button",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(1)),
);
let mut app = test_app(dom, sheet);
let fired = Rc::new(Cell::new(false));
let f = fired.clone();
app.dom_mut()
.add_event_listener(form, "submit", ListenerOptions::default(), move |_| {
f.set(true);
})
.unwrap();
app.draw_if_dirty().unwrap();
for ev in click(1, 0) {
app.handle_event(ev);
}
assert!(!fired.get());
}
#[test]
fn submit_event_is_cancelable() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let btn = dom.create_element("input");
dom.set_attribute(btn, "type", "submit").unwrap();
dom.append_child(form, btn).unwrap();
dom.append_child(root, form).unwrap();
let sheet = Stylesheet::new().rule_unchecked(
"input[type=submit]",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(1)),
);
let mut app = test_app(dom, sheet);
let saw_after_prevent = Rc::new(Cell::new(false));
let s = saw_after_prevent.clone();
app.dom_mut()
.add_event_listener(form, "submit", ListenerOptions::default(), |ctx| {
ctx.event.prevent_default();
})
.unwrap();
app.dom_mut()
.add_event_listener(form, "submit", ListenerOptions::default(), move |ctx| {
assert!(ctx.event.default_prevented());
s.set(true);
})
.unwrap();
app.draw_if_dirty().unwrap();
for ev in click(1, 0) {
app.handle_event(ev);
}
assert!(saw_after_prevent.get());
}
#[test]
fn disabled_submit_button_does_not_fire_submit() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let btn = dom.create_element("input");
dom.set_attribute(btn, "type", "submit").unwrap();
dom.set_attribute(btn, "disabled", "").unwrap();
dom.append_child(form, btn).unwrap();
dom.append_child(root, form).unwrap();
let sheet = Stylesheet::new().rule_unchecked(
"input[type=submit]",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(1)),
);
let mut app = test_app(dom, sheet);
let fired = Rc::new(Cell::new(false));
let f = fired.clone();
app.dom_mut()
.add_event_listener(form, "submit", ListenerOptions::default(), move |_| {
f.set(true);
})
.unwrap();
app.draw_if_dirty().unwrap();
for ev in click(1, 0) {
app.handle_event(ev);
}
assert!(!fired.get());
}
#[test]
fn enter_in_lone_text_input_submits_form() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let inp = dom.create_element("input");
dom.set_attribute(inp, "type", "text").unwrap();
dom.append_child(form, inp).unwrap();
dom.append_child(root, form).unwrap();
let mut app = test_app(dom, Stylesheet::new());
let fired = Rc::new(Cell::new(false));
let f = fired.clone();
app.dom_mut()
.add_event_listener(form, "submit", ListenerOptions::default(), move |_| {
f.set(true);
})
.unwrap();
app.dom_mut().set_focused(Some(inp));
let t = app
.dom()
.node(inp)
.child_nodes()
.next()
.map(|c| c.id())
.unwrap();
app.dom_mut()
.set_selection(Some(Selection::caret(Position::new(t, 0))));
app.handle_event(key(KeyCode::Enter));
assert!(fired.get());
}
#[test]
fn enter_in_form_with_multiple_text_inputs_does_not_submit() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let i1 = dom.create_element("input");
dom.set_attribute(i1, "type", "text").unwrap();
let i2 = dom.create_element("input");
dom.set_attribute(i2, "type", "text").unwrap();
dom.append_child(form, i1).unwrap();
dom.append_child(form, i2).unwrap();
dom.append_child(root, form).unwrap();
let mut app = test_app(dom, Stylesheet::new());
let fired = Rc::new(Cell::new(false));
let f = fired.clone();
app.dom_mut()
.add_event_listener(form, "submit", ListenerOptions::default(), move |_| {
f.set(true);
})
.unwrap();
app.dom_mut().set_focused(Some(i1));
let t = app
.dom()
.node(i1)
.child_nodes()
.next()
.map(|c| c.id())
.unwrap();
app.dom_mut()
.set_selection(Some(Selection::caret(Position::new(t, 0))));
app.handle_event(key(KeyCode::Enter));
assert!(!fired.get());
}
#[test]
fn enter_outside_form_does_not_submit_anything() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let inp = dom.create_element("input");
dom.set_attribute(inp, "type", "text").unwrap();
dom.append_child(root, inp).unwrap();
let mut app = test_app(dom, Stylesheet::new());
let fired = Rc::new(Cell::new(false));
let f = fired.clone();
app.dom_mut()
.add_event_listener(root, "submit", ListenerOptions::default(), move |_| {
f.set(true);
})
.unwrap();
app.dom_mut().set_focused(Some(inp));
let t = app
.dom()
.node(inp)
.child_nodes()
.next()
.map(|c| c.id())
.unwrap();
app.dom_mut()
.set_selection(Some(Selection::caret(Position::new(t, 0))));
app.handle_event(key(KeyCode::Enter));
assert!(!fired.get());
}
#[test]
fn click_on_reset_button_fires_reset_event() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let btn = dom.create_element("input");
dom.set_attribute(btn, "type", "reset").unwrap();
dom.append_child(form, btn).unwrap();
dom.append_child(root, form).unwrap();
let sheet = Stylesheet::new().rule_unchecked(
"input[type=reset]",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(1)),
);
let mut app = test_app(dom, sheet);
let fired = Rc::new(Cell::new(false));
let f = fired.clone();
app.dom_mut()
.add_event_listener(form, "reset", ListenerOptions::default(), move |_| {
f.set(true);
})
.unwrap();
app.draw_if_dirty().unwrap();
for ev in click(1, 0) {
app.handle_event(ev);
}
assert!(fired.get());
}
#[test]
fn collect_returns_text_input_value() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let inp = dom.create_element("input");
dom.set_attribute(inp, "type", "text").unwrap();
dom.set_attribute(inp, "name", "user").unwrap();
dom.set_attribute(inp, "value", "alice").unwrap();
dom.append_child(form, inp).unwrap();
dom.append_child(root, form).unwrap();
let app = test_app(dom, Stylesheet::new());
let collected = form::collect(app.dom(), form);
assert_eq!(collected, vec![("user".to_string(), "alice".to_string())]);
}
#[test]
fn collect_skips_inputs_without_name() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let inp = dom.create_element("input");
dom.set_attribute(inp, "type", "text").unwrap();
dom.set_attribute(inp, "value", "secret").unwrap();
dom.append_child(form, inp).unwrap();
dom.append_child(root, form).unwrap();
let app = test_app(dom, Stylesheet::new());
assert!(form::collect(app.dom(), form).is_empty());
}
#[test]
fn collect_skips_disabled_inputs() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let inp = dom.create_element("input");
dom.set_attribute(inp, "type", "text").unwrap();
dom.set_attribute(inp, "name", "x").unwrap();
dom.set_attribute(inp, "value", "v").unwrap();
dom.set_attribute(inp, "disabled", "").unwrap();
dom.append_child(form, inp).unwrap();
dom.append_child(root, form).unwrap();
let app = test_app(dom, Stylesheet::new());
assert!(form::collect(app.dom(), form).is_empty());
}
#[test]
fn collect_includes_checked_checkboxes_only() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let cb1 = dom.create_element("input");
dom.set_attribute(cb1, "type", "checkbox").unwrap();
dom.set_attribute(cb1, "name", "a").unwrap();
dom.set_attribute(cb1, "value", "1").unwrap();
dom.set_attribute(cb1, "checked", "").unwrap();
let cb2 = dom.create_element("input");
dom.set_attribute(cb2, "type", "checkbox").unwrap();
dom.set_attribute(cb2, "name", "b").unwrap();
dom.set_attribute(cb2, "value", "2").unwrap();
dom.append_child(form, cb1).unwrap();
dom.append_child(form, cb2).unwrap();
dom.append_child(root, form).unwrap();
let app = test_app(dom, Stylesheet::new());
assert_eq!(
form::collect(app.dom(), form),
vec![("a".to_string(), "1".to_string())]
);
}
#[test]
fn collect_default_checkbox_value_is_on() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let cb = dom.create_element("input");
dom.set_attribute(cb, "type", "checkbox").unwrap();
dom.set_attribute(cb, "name", "agree").unwrap();
dom.set_attribute(cb, "checked", "").unwrap();
dom.append_child(form, cb).unwrap();
dom.append_child(root, form).unwrap();
let app = test_app(dom, Stylesheet::new());
assert_eq!(
form::collect(app.dom(), form),
vec![("agree".to_string(), "on".to_string())]
);
}
#[test]
fn collect_includes_textarea_text_content() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let ta = dom.create_element("textarea");
dom.set_attribute(ta, "name", "comments").unwrap();
let t = dom.create_text_node("hello\nworld");
dom.append_child(ta, t).unwrap();
dom.append_child(form, ta).unwrap();
dom.append_child(root, form).unwrap();
let app = test_app(dom, Stylesheet::new());
assert_eq!(
form::collect(app.dom(), form),
vec![("comments".to_string(), "hello\nworld".to_string())]
);
}
#[test]
fn submit_handler_can_read_form_data_via_collect() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let inp = dom.create_element("input");
dom.set_attribute(inp, "type", "text").unwrap();
dom.set_attribute(inp, "name", "q").unwrap();
dom.set_attribute(inp, "value", "rust").unwrap();
let btn = dom.create_element("input");
dom.set_attribute(btn, "type", "submit").unwrap();
dom.append_child(form, inp).unwrap();
dom.append_child(form, btn).unwrap();
dom.append_child(root, form).unwrap();
let sheet = Stylesheet::new()
.rule_unchecked(
"input[type=submit]",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(1)),
)
.rule_unchecked(
"input[type=text]",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(1)),
);
let mut app = test_app(dom, sheet);
let captured: Rc<RefCell<Vec<(String, String)>>> = Rc::new(RefCell::new(Vec::new()));
let c = captured.clone();
app.dom_mut()
.add_event_listener(form, "submit", ListenerOptions::default(), move |ctx| {
ctx.event.prevent_default();
*c.borrow_mut() = form::collect(ctx.dom, form);
})
.unwrap();
app.draw_if_dirty().unwrap();
for ev in click(1, 1) {
app.handle_event(ev);
}
assert_eq!(
*captured.borrow(),
vec![("q".to_string(), "rust".to_string())]
);
}
#[test]
fn submit_event_carries_submitter_on_button_click() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let btn = dom.create_element("input");
dom.set_attribute(btn, "type", "submit").unwrap();
dom.append_child(form, btn).unwrap();
dom.append_child(root, form).unwrap();
let sheet = Stylesheet::new().rule_unchecked(
"input[type=submit]",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(1)),
);
let mut app = test_app(dom, sheet);
let captured: Rc<Cell<Option<Option<rdom_core::NodeId>>>> = Rc::new(Cell::new(None));
{
let captured = captured.clone();
app.dom_mut()
.add_event_listener(form, "submit", ListenerOptions::default(), move |ctx| {
let detail = ctx
.event
.detail
.as_submit()
.expect("submit must carry EventDetail::Submit");
captured.set(Some(detail.submitter));
})
.unwrap();
}
app.draw_if_dirty().unwrap();
for ev in click(1, 0) {
app.handle_event(ev);
}
let seen = captured.get().expect("submit listener fired");
assert_eq!(seen, Some(btn));
}
#[test]
fn implicit_enter_submit_has_no_submitter() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let inp = dom.create_element("input");
dom.set_attribute(inp, "type", "text").unwrap();
dom.append_child(form, inp).unwrap();
dom.append_child(root, form).unwrap();
let mut app = test_app(dom, Stylesheet::new());
let captured: Rc<Cell<Option<Option<rdom_core::NodeId>>>> = Rc::new(Cell::new(None));
{
let captured = captured.clone();
app.dom_mut()
.add_event_listener(form, "submit", ListenerOptions::default(), move |ctx| {
let detail = ctx
.event
.detail
.as_submit()
.expect("submit must carry EventDetail::Submit");
captured.set(Some(detail.submitter));
})
.unwrap();
}
app.dom_mut().set_focused(Some(inp));
let t = app
.dom()
.node(inp)
.child_nodes()
.next()
.map(|c| c.id())
.unwrap();
app.dom_mut()
.set_selection(Some(Selection::caret(Position::new(t, 0))));
app.handle_event(key(KeyCode::Enter));
let seen = captured.get().expect("submit listener fired");
assert!(
seen.is_none(),
"implicit Enter submit must have submitter=None; got {seen:?}"
);
}
#[test]
fn reset_restores_default_value_and_default_checked() {
use crate::accessors::TuiAccessors;
use crate::runtime::builtins::input;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
let text = dom.create_element("input");
dom.set_attribute(text, "value", "a").unwrap();
let cb = dom.create_element("input");
dom.set_attribute(cb, "type", "checkbox").unwrap();
let ra = dom.create_element("input");
dom.set_attribute(ra, "type", "radio").unwrap();
dom.set_attribute(ra, "name", "g").unwrap();
dom.set_attribute(ra, "checked", "").unwrap();
let rb = dom.create_element("input");
dom.set_attribute(rb, "type", "radio").unwrap();
dom.set_attribute(rb, "name", "g").unwrap();
let reset = dom.create_element("input");
dom.set_attribute(reset, "type", "reset").unwrap();
for c in [text, cb, ra, rb, reset] {
dom.append_child(form, c).unwrap();
}
dom.append_child(root, form).unwrap();
let mut sheet = Stylesheet::new().rule_unchecked(
"form",
TuiStyle::new().direction(crate::layout::Direction::Column),
);
for sel in [
"input",
"input[type=checkbox]",
"input[type=radio]",
"input[type=reset]",
] {
sheet = sheet.rule_unchecked(
sel,
TuiStyle::new()
.display(crate::layout::Display::Block)
.width(Size::Fixed(10))
.height(Size::Fixed(1)),
);
}
let mut app = test_app(dom, sheet);
app.draw_if_dirty().unwrap();
input::set_value(app.dom_mut(), text, "ax");
for ev in click(1, 1) {
app.handle_event(ev); }
for ev in click(1, 3) {
app.handle_event(ev); }
app.draw_if_dirty().unwrap();
assert_eq!(app.dom().node(text).value(), Some("ax".into()));
assert!(app.dom().node(cb).checked());
assert!(!app.dom().node(ra).checked() && app.dom().node(rb).checked());
assert_eq!(app.dom().node(text).default_value(), Some("a".into()));
assert_eq!(app.dom().node(cb).default_checked(), Some(false));
assert_eq!(app.dom().node(ra).default_checked(), Some(true));
for ev in click(1, 4) {
app.handle_event(ev); }
app.draw_if_dirty().unwrap();
assert_eq!(app.dom().node(text).value(), Some("a".into()));
assert_eq!(app.dom().node(text).get_attribute("value"), Some("a"));
assert!(!app.dom().node(cb).checked());
assert!(app.dom().node(ra).checked() && !app.dom().node(rb).checked());
}
fn form_app(
build: impl FnOnce(&mut TuiDom, rdom_core::NodeId),
) -> (App<TestBackend>, rdom_core::NodeId) {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
dom.append_child(root, form).unwrap();
build(&mut dom, form);
let reset = dom.create_element("input");
dom.set_attribute(reset, "type", "reset").unwrap();
dom.append_child(form, reset).unwrap();
let mut app = test_app(dom, Stylesheet::new());
app.draw_if_dirty().unwrap();
(app, reset)
}
fn click_reset(app: &mut App<TestBackend>, reset: rdom_core::NodeId) {
use crate::accessors::TuiAccessorsMut;
app.dom_mut().node_mut(reset).click();
}
fn option(
dom: &mut TuiDom,
select: rdom_core::NodeId,
value: &str,
selected: bool,
) -> rdom_core::NodeId {
let o = dom.create_element("option");
dom.set_attribute(o, "value", value).unwrap();
let t = dom.create_text_node(value);
dom.append_child(o, t).unwrap();
if selected {
dom.set_attribute(o, "selected", "").unwrap();
}
dom.append_child(select, o).unwrap();
o
}
#[test]
fn reset_restores_select_options_to_default_selected() {
use crate::accessors::{TuiAccessors, TuiAccessorsMut};
let mut ids = Vec::new();
let (mut app, reset) = form_app(|dom, form| {
let single = dom.create_element("select");
dom.append_child(form, single).unwrap();
ids.push(single);
ids.push(option(dom, single, "a", false));
ids.push(option(dom, single, "b", true));
let multi = dom.create_element("select");
dom.set_attribute(multi, "multiple", "").unwrap();
dom.append_child(form, multi).unwrap();
ids.push(multi);
ids.push(option(dom, multi, "x", true));
ids.push(option(dom, multi, "y", false));
});
let [single, a, b, multi, x, y] = ids[..] else {
unreachable!()
};
app.dom_mut().node_mut(single).set_value("a").unwrap();
app.dom_mut().node_mut(multi).set_value("y").unwrap();
assert_eq!(app.dom().node(single).value(), Some("a".into()));
assert_eq!(app.dom().node(multi).value(), Some("y".into()));
click_reset(&mut app, reset);
assert!(!app.dom().node(a).has_attribute("selected"));
assert!(app.dom().node(b).has_attribute("selected"));
assert!(app.dom().node(x).has_attribute("selected"));
assert!(!app.dom().node(y).has_attribute("selected"));
assert_eq!(app.dom().node(single).value(), Some("b".into()));
}
#[test]
fn reset_restores_range_to_its_default_value() {
use crate::accessors::TuiAccessors;
use crate::runtime::builtins::range;
let mut ids = Vec::new();
let (mut app, reset) = form_app(|dom, form| {
for value in [Some("30"), None] {
let r = dom.create_element("input");
dom.set_attribute(r, "type", "range").unwrap();
if let Some(v) = value {
dom.set_attribute(r, "value", v).unwrap();
}
dom.append_child(form, r).unwrap();
ids.push(r);
}
});
let [r30, rmid] = ids[..] else { unreachable!() };
range::set_value(app.dom_mut(), r30, 70.0);
range::set_value(app.dom_mut(), rmid, 10.0);
assert_eq!(app.dom().node(r30).default_value(), Some("30".into()));
assert_eq!(app.dom().node(rmid).default_value(), Some("".into()));
click_reset(&mut app, reset);
assert_eq!(app.dom().node(r30).get_attribute("value"), Some("30"));
assert_eq!(app.dom().node(rmid).get_attribute("value"), None);
assert_eq!(range::value_of(app.dom(), rmid), 50.0);
}
#[test]
fn textarea_set_value_keeps_the_authored_default_value() {
use crate::accessors::{TuiAccessors, TuiAccessorsMut};
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let ta = dom.create_element("textarea");
let t = dom.create_text_node("orig");
dom.append_child(ta, t).unwrap();
dom.append_child(root, ta).unwrap();
dom.node_mut(ta).set_value("new").unwrap();
assert_eq!(dom.node(ta).value(), Some("new".into()));
assert_eq!(dom.node(ta).default_value(), Some("orig".into()));
}
#[test]
fn default_value_of_non_text_inputs_is_the_value_attribute() {
use crate::accessors::TuiAccessors;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let mut make = |ty: &str, value: &str| {
let i = dom.create_element("input");
dom.set_attribute(i, "type", ty).unwrap();
dom.set_attribute(i, "value", value).unwrap();
dom.append_child(root, i).unwrap();
i
};
let range = make("range", "30");
let hidden = make("hidden", "h");
let submit = make("submit", "Go");
let cb = make("checkbox", "yes");
assert_eq!(dom.node(range).default_value(), Some("30".into()));
assert_eq!(dom.node(hidden).default_value(), Some("h".into()));
assert_eq!(dom.node(submit).default_value(), Some("Go".into()));
assert_eq!(dom.node(cb).default_value(), Some("yes".into()));
}
#[test]
fn default_setters_change_what_reset_restores() {
use crate::accessors::{TuiAccessors, TuiAccessorsMut};
let mut ids = Vec::new();
let (mut app, reset) = form_app(|dom, form| {
let text = dom.create_element("input");
dom.set_attribute(text, "value", "a").unwrap();
let cb = dom.create_element("input");
dom.set_attribute(cb, "type", "checkbox").unwrap();
let ta = dom.create_element("textarea");
let hidden = dom.create_element("input");
dom.set_attribute(hidden, "type", "hidden").unwrap();
for c in [text, cb, ta, hidden] {
dom.append_child(form, c).unwrap();
ids.push(c);
}
});
let [text, cb, ta, hidden] = ids[..] else {
unreachable!()
};
let dom = app.dom_mut();
dom.node_mut(text).set_default_value("z").unwrap();
dom.node_mut(ta).set_default_value("body").unwrap();
dom.node_mut(cb).set_default_checked(true).unwrap();
dom.node_mut(hidden).set_default_value("q").unwrap();
assert_eq!(
dom.node(text).value(),
Some("a".into()),
"live value untouched"
);
assert!(!dom.node(cb).checked(), "live checkedness untouched");
assert_eq!(dom.node(text).default_value(), Some("z".into()));
assert_eq!(dom.node(cb).default_checked(), Some(true));
assert_eq!(dom.node(hidden).get_attribute("value"), Some("q"));
click_reset(&mut app, reset);
let dom = app.dom();
assert_eq!(dom.node(text).value(), Some("z".into()));
assert_eq!(dom.node(ta).value(), Some("body".into()));
assert!(dom.node(cb).checked());
}
#[test]
fn collect_skips_selected_options_that_are_disabled() {
let mut form_id = None;
let (app, _reset) = form_app(|dom, form| {
form_id = Some(form);
let multi = dom.create_element("select");
dom.set_attribute(multi, "name", "m").unwrap();
dom.set_attribute(multi, "multiple", "").unwrap();
dom.append_child(form, multi).unwrap();
option(dom, multi, "kept", true);
let own = option(dom, multi, "own-disabled", true);
dom.set_attribute(own, "disabled", "").unwrap();
let group = dom.create_element("optgroup");
dom.set_attribute(group, "disabled", "").unwrap();
dom.append_child(multi, group).unwrap();
option(dom, group, "group-disabled", true);
});
assert_eq!(
form::collect(app.dom(), form_id.unwrap()),
vec![("m".to_string(), "kept".to_string())]
);
}
#[test]
fn collect_single_select_with_disabled_selected_option_submits_nothing() {
let mut form_id = None;
let (app, _reset) = form_app(|dom, form| {
form_id = Some(form);
let single = dom.create_element("select");
dom.set_attribute(single, "name", "s").unwrap();
dom.append_child(form, single).unwrap();
option(dom, single, "a", false);
let b = option(dom, single, "b", true);
dom.set_attribute(b, "disabled", "").unwrap();
});
assert!(form::collect(app.dom(), form_id.unwrap()).is_empty());
}
fn press_enter_in(app: &mut App<TestBackend>, input: rdom_core::NodeId) {
app.dom_mut().set_focused(Some(input));
let t = app
.dom()
.node(input)
.child_nodes()
.next()
.map(|c| c.id())
.unwrap();
app.dom_mut()
.set_selection(Some(Selection::caret(Position::new(t, 0))));
app.handle_event(key(KeyCode::Enter));
}
fn named(
dom: &mut TuiDom,
form: rdom_core::NodeId,
tag: &str,
attrs: &[(&str, &str)],
) -> rdom_core::NodeId {
let el = dom.create_element(tag);
for (k, v) in attrs {
dom.set_attribute(el, k, v).unwrap();
}
dom.append_child(form, el).unwrap();
el
}
type Submissions = Rc<RefCell<Vec<(Option<rdom_core::NodeId>, Vec<(String, String)>)>>>;
fn record_submissions(app: &mut App<TestBackend>, form: rdom_core::NodeId) -> Submissions {
let log: Submissions = Rc::new(RefCell::new(Vec::new()));
let l = log.clone();
app.dom_mut()
.add_event_listener(form, "submit", ListenerOptions::default(), move |ctx| {
ctx.event.prevent_default();
let submitter = ctx
.event
.detail
.as_submit()
.expect("submit detail")
.submitter;
let entries = form::collect_with_submitter(ctx.dom, form, submitter);
l.borrow_mut().push((submitter, entries));
})
.unwrap();
log
}
fn pairs(list: &[(&str, &str)]) -> Vec<(String, String)> {
list.iter()
.map(|(n, v)| (n.to_string(), v.to_string()))
.collect()
}
#[test]
fn collect_includes_only_the_submitter_among_buttons() {
let mut ids = Vec::new();
let mut form_id = None;
let (app, _reset) = form_app(|dom, form| {
form_id = Some(form);
named(dom, form, "input", &[("name", "q"), ("value", "rust")]);
ids.push(named(
dom,
form,
"input",
&[("type", "submit"), ("name", "a"), ("value", "A")],
));
ids.push(named(dom, form, "button", &[("name", "b"), ("value", "B")]));
ids.push(named(
dom,
form,
"input",
&[("type", "reset"), ("name", "r"), ("value", "R")],
));
ids.push(named(
dom,
form,
"input",
&[("type", "button"), ("name", "x"), ("value", "X")],
));
ids.push(named(
dom,
form,
"button",
&[("type", "button"), ("name", "y"), ("value", "Y")],
));
});
let form = form_id.unwrap();
let dom = app.dom();
assert_eq!(form::collect(dom, form), pairs(&[("q", "rust")]));
assert_eq!(
form::collect_with_submitter(dom, form, Some(ids[0])),
pairs(&[("q", "rust"), ("a", "A")])
);
assert_eq!(
form::collect_with_submitter(dom, form, Some(ids[1])),
pairs(&[("q", "rust"), ("b", "B")])
);
for ¬_submit in &ids[2..] {
assert_eq!(
form::collect_with_submitter(dom, form, Some(not_submit)),
pairs(&[("q", "rust")]),
"a non-submit button never contributes"
);
}
}
#[test]
fn submitter_without_value_contributes_empty_string_and_without_name_nothing() {
let mut ids = Vec::new();
let mut form_id = None;
let (app, _reset) = form_app(|dom, form| {
form_id = Some(form);
ids.push(named(dom, form, "button", &[("name", "go")]));
ids.push(named(
dom,
form,
"input",
&[("type", "submit"), ("value", "unnamed")],
));
});
let form = form_id.unwrap();
assert_eq!(
form::collect_with_submitter(app.dom(), form, Some(ids[0])),
pairs(&[("go", "")])
);
assert!(form::collect_with_submitter(app.dom(), form, Some(ids[1])).is_empty());
}
#[test]
fn value_less_submit_input_submitter_contributes_empty_string() {
let mut ids = Vec::new();
let mut form_id = None;
let (app, _reset) = form_app(|dom, form| {
form_id = Some(form);
ids.push(named(
dom,
form,
"input",
&[("type", "submit"), ("name", "a")],
));
});
let form = form_id.unwrap();
assert_eq!(
form::collect_with_submitter(app.dom(), form, Some(ids[0])),
pairs(&[("a", "")])
);
}
#[test]
fn clicked_submit_button_is_the_submitter_and_the_only_button_entry() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
dom.append_child(root, form).unwrap();
named(
&mut dom,
form,
"input",
&[("type", "submit"), ("name", "act"), ("value", "save")],
);
let second = named(
&mut dom,
form,
"input",
&[("type", "submit"), ("name", "act"), ("value", "delete")],
);
let mut app = test_app(dom, Stylesheet::new());
let log = record_submissions(&mut app, form);
use crate::accessors::TuiAccessorsMut;
app.dom_mut().node_mut(second).click();
assert_eq!(
*log.borrow(),
vec![(Some(second), pairs(&[("act", "delete")]))]
);
}
#[test]
fn enter_with_a_default_button_clicks_it_and_submits_with_it() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
dom.append_child(root, form).unwrap();
let q = named(&mut dom, form, "input", &[("name", "q"), ("value", "rust")]);
named(&mut dom, form, "input", &[("name", "r"), ("value", "tui")]);
named(
&mut dom,
form,
"button",
&[("type", "button"), ("name", "n"), ("value", "N")],
);
named(&mut dom, form, "input", &[("type", "reset")]);
let default = named(
&mut dom,
form,
"button",
&[("type", "submit"), ("name", "go"), ("value", "1")],
);
named(
&mut dom,
form,
"input",
&[("type", "submit"), ("name", "later"), ("value", "2")],
);
let mut app = test_app(dom, Stylesheet::new());
let log = record_submissions(&mut app, form);
let clicks = Rc::new(RefCell::new(Vec::new()));
let c = clicks.clone();
app.dom_mut()
.add_event_listener(form, "click", ListenerOptions::default(), move |ctx| {
c.borrow_mut().push(ctx.event.target);
})
.unwrap();
press_enter_in(&mut app, q);
assert_eq!(*clicks.borrow(), vec![Some(default)]);
assert_eq!(
*log.borrow(),
vec![(
Some(default),
pairs(&[("q", "rust"), ("r", "tui"), ("go", "1")])
)]
);
}
#[test]
fn enter_with_a_disabled_default_button_does_not_submit() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
dom.append_child(root, form).unwrap();
let q = named(&mut dom, form, "input", &[("name", "q")]);
named(
&mut dom,
form,
"input",
&[("type", "submit"), ("disabled", "")],
);
named(&mut dom, form, "input", &[("type", "submit")]);
let mut app = test_app(dom, Stylesheet::new());
let log = record_submissions(&mut app, form);
press_enter_in(&mut app, q);
assert!(log.borrow().is_empty());
}
#[test]
fn button_with_an_invalid_type_is_a_submit_button() {
let mut ids = Vec::new();
let mut form_id = None;
let (app, _reset) = form_app(|dom, form| {
form_id = Some(form);
ids.push(named(
dom,
form,
"button",
&[("type", "bogus"), ("name", "a"), ("value", "A")],
));
ids.push(named(
dom,
form,
"button",
&[("type", "reset"), ("name", "b"), ("value", "B")],
));
});
let form = form_id.unwrap();
let dom = app.dom();
assert_eq!(
form::collect_with_submitter(dom, form, Some(ids[0])),
pairs(&[("a", "A")])
);
assert!(form::collect_with_submitter(dom, form, Some(ids[1])).is_empty());
}
#[test]
fn click_on_button_with_invalid_type_submits() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
dom.append_child(root, form).unwrap();
let btn = named(&mut dom, form, "button", &[("type", "bogus")]);
let mut app = test_app(dom, Stylesheet::new());
let log = record_submissions(&mut app, form);
use crate::accessors::TuiAccessorsMut;
app.dom_mut().node_mut(btn).click();
assert_eq!(*log.borrow(), vec![(Some(btn), vec![])]);
}
#[test]
fn collect_skips_controls_in_a_disabled_fieldset_but_not_its_first_legend() {
let mut form_id = None;
let (app, _reset) = form_app(|dom, form| {
form_id = Some(form);
named(dom, form, "input", &[("name", "a"), ("value", "A")]);
let fs = named(dom, form, "fieldset", &[("disabled", "")]);
let legend = named(dom, fs, "legend", &[]);
named(dom, legend, "input", &[("name", "l"), ("value", "L")]);
named(dom, fs, "input", &[("name", "b"), ("value", "B")]);
named(
dom,
fs,
"input",
&[("type", "checkbox"), ("name", "c"), ("checked", "")],
);
let inner = named(dom, fs, "fieldset", &[]);
named(dom, inner, "input", &[("name", "d"), ("value", "D")]);
});
assert_eq!(
form::collect(app.dom(), form_id.unwrap()),
pairs(&[("a", "A"), ("l", "L")])
);
}
#[test]
fn submit_button_in_a_disabled_fieldset_does_not_submit() {
let mut form_id = None;
let mut input_id = None;
let mut button_id = None;
let (mut app, _reset) = form_app(|dom, form| {
form_id = Some(form);
input_id = Some(named(dom, form, "input", &[("value", "q")]));
let fs = named(dom, form, "fieldset", &[("disabled", "")]);
button_id = Some(named(dom, fs, "button", &[("name", "go")]));
});
let log = record_submissions(&mut app, form_id.unwrap());
click_reset(&mut app, button_id.unwrap()); assert!(
log.borrow().is_empty(),
"a disabled-by-fieldset button does not submit"
);
press_enter_in(&mut app, input_id.unwrap());
assert!(
log.borrow().is_empty(),
"a disabled-by-fieldset default button blocks implicit submission"
);
}
fn owner_app(build: impl FnOnce(&mut TuiDom, rdom_core::NodeId)) -> App<TestBackend> {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
build(&mut dom, root);
let mut app = test_app(dom, Stylesheet::new());
app.draw_if_dirty().unwrap();
app
}
#[test]
fn control_outside_the_form_with_a_form_attribute_is_submitted_and_reset_with_it() {
let mut ids = Vec::new();
let mut app = owner_app(|dom, root| {
ids.push(named(
dom,
root,
"input",
&[("name", "before"), ("form", "f"), ("value", "1")],
));
let form = named(dom, root, "form", &[("id", "f")]);
ids.push(form);
ids.push(named(
dom,
form,
"input",
&[("name", "inside"), ("value", "2")],
));
ids.push(named(
dom,
root,
"input",
&[("name", "after"), ("form", "f"), ("value", "3")],
));
named(dom, root, "input", &[("name", "stray"), ("value", "4")]);
ids.push(named(
dom,
root,
"input",
&[("type", "reset"), ("form", "f")],
));
});
let (before, form, inside, after, reset) = (ids[0], ids[1], ids[2], ids[3], ids[4]);
assert_eq!(
form::elements(app.dom(), form),
vec![before, inside, after, reset]
);
{
use crate::accessors::TuiAccessors;
assert_eq!(app.dom().node(before).input_form(), Some(form));
assert_eq!(app.dom().node(reset).input_form(), Some(form));
}
assert_eq!(
form::collect(app.dom(), form),
pairs(&[("before", "1"), ("inside", "2"), ("after", "3")])
);
{
use crate::accessors::TuiAccessorsMut;
app.dom_mut().node_mut(before).set_value("changed").unwrap();
app.dom_mut().node_mut(after).set_value("changed").unwrap();
}
click_reset(&mut app, reset);
assert_eq!(
form::collect(app.dom(), form),
pairs(&[("before", "1"), ("inside", "2"), ("after", "3")]),
"the reset button outside the form resets its owner's controls"
);
}
#[test]
fn control_inside_one_form_can_belong_to_another() {
let mut ids = Vec::new();
let mut app = owner_app(|dom, root| {
let a = named(dom, root, "form", &[("id", "a")]);
let b = named(dom, root, "form", &[("id", "b")]);
named(dom, a, "input", &[("name", "for_a"), ("value", "A")]);
named(
dom,
a,
"input",
&[("name", "for_b"), ("form", "b"), ("value", "B")],
);
let go = named(dom, a, "button", &[("form", "b"), ("name", "go")]);
ids.extend([a, b, go]);
});
let (a, b, go) = (ids[0], ids[1], ids[2]);
assert_eq!(form::collect(app.dom(), a), pairs(&[("for_a", "A")]));
assert_eq!(form::collect(app.dom(), b), pairs(&[("for_b", "B")]));
let log_a = record_submissions(&mut app, a);
let log_b = record_submissions(&mut app, b);
click_reset(&mut app, go); assert!(log_a.borrow().is_empty(), "form A is not submitted");
assert_eq!(
*log_b.borrow(),
vec![(Some(go), pairs(&[("for_b", "B"), ("go", "")]))]
);
}
#[test]
fn unknown_form_attribute_means_no_owner_even_inside_a_form() {
let mut ids = Vec::new();
let mut app = owner_app(|dom, root| {
let f = named(dom, root, "form", &[("id", "f")]);
named(dom, f, "input", &[("name", "ok"), ("value", "1")]);
named(
dom,
f,
"input",
&[("name", "lost"), ("form", "nope"), ("value", "2")],
);
let go = named(dom, f, "button", &[("form", "nope")]);
ids.extend([f, go]);
});
let (f, go) = (ids[0], ids[1]);
assert_eq!(form::collect(app.dom(), f), pairs(&[("ok", "1")]));
let log = record_submissions(&mut app, f);
click_reset(&mut app, go);
assert!(
log.borrow().is_empty(),
"an unowned submit button submits nothing"
);
}
#[test]
fn implicit_submission_follows_the_form_attribute() {
let mut ids = Vec::new();
let mut app = owner_app(|dom, root| {
let f = named(dom, root, "form", &[("id", "f")]);
let input = named(
dom,
root,
"input",
&[("form", "f"), ("name", "q"), ("value", "x")],
);
let go = named(
dom,
root,
"button",
&[("form", "f"), ("name", "go"), ("value", "G")],
);
ids.extend([f, input, go]);
});
let (f, input, go) = (ids[0], ids[1], ids[2]);
let log = record_submissions(&mut app, f);
press_enter_in(&mut app, input);
assert_eq!(
*log.borrow(),
vec![(Some(go), pairs(&[("q", "x"), ("go", "G")]))]
);
}
#[test]
fn submit_event_reports_the_effective_submission_attributes() {
use rdom_core::{FormEnctype, FormMethod, SubmitDetail};
let mut ids = Vec::new();
let mut app = owner_app(|dom, root| {
let f = named(
dom,
root,
"form",
&[
("action", "/save"),
("method", "post"),
("enctype", "multipart/form-data"),
("target", "_blank"),
],
);
let plain = named(dom, f, "button", &[]);
let over = named(
dom,
f,
"input",
&[
("type", "submit"),
("formaction", "/other"),
("formmethod", "get"),
("formenctype", "text/plain"),
("formtarget", "_self"),
("formnovalidate", ""),
],
);
ids.extend([f, plain, over]);
});
let (f, plain, over) = (ids[0], ids[1], ids[2]);
let seen: Rc<RefCell<Vec<SubmitDetail>>> = Rc::new(RefCell::new(Vec::new()));
let s = seen.clone();
app.dom_mut()
.add_event_listener(f, "submit", ListenerOptions::default(), move |ctx| {
ctx.event.prevent_default();
s.borrow_mut()
.push(ctx.event.detail.as_submit().unwrap().clone());
})
.unwrap();
click_reset(&mut app, plain);
click_reset(&mut app, over);
let seen = seen.borrow();
assert_eq!(seen.len(), 2);
let d = &seen[0];
assert_eq!(
(
d.submitter,
d.action.as_str(),
d.method,
d.enctype,
d.target.as_str(),
d.no_validate
),
(
Some(plain),
"/save",
FormMethod::Post,
FormEnctype::MultipartFormData,
"_blank",
false
)
);
let d = &seen[1];
assert_eq!(
(
d.submitter,
d.action.as_str(),
d.method,
d.enctype,
d.target.as_str(),
d.no_validate
),
(
Some(over),
"/other",
FormMethod::Get,
FormEnctype::TextPlain,
"_self",
true
)
);
}
#[test]
fn button_and_input_type_keywords_are_case_insensitive() {
let mut ids = Vec::new();
let mut app = owner_app(|dom, root| {
let f = named(dom, root, "form", &[]);
let text = named(dom, f, "input", &[("type", "TEXT"), ("name", "t")]);
let reset = named(dom, f, "button", &[("type", "RESET")]);
let submit = named(dom, f, "input", &[("type", "Submit")]);
ids.extend([f, text, reset, submit]);
});
let (f, text, reset, submit) = (ids[0], ids[1], ids[2], ids[3]);
let log = record_submissions(&mut app, f);
let resets = Rc::new(Cell::new(0u32));
let r = resets.clone();
app.dom_mut()
.add_event_listener(f, "reset", ListenerOptions::default(), move |_| {
r.set(r.get() + 1);
})
.unwrap();
click_reset(&mut app, reset);
assert_eq!(resets.get(), 1, "type=RESET resets");
assert!(log.borrow().is_empty(), "type=RESET does not submit");
click_reset(&mut app, submit);
assert_eq!(log.borrow().len(), 1, "type=Submit submits");
assert_eq!(log.borrow()[0].0, Some(submit));
press_enter_in(&mut app, text);
assert_eq!(
log.borrow().len(),
2,
"Enter in type=TEXT submits implicitly"
);
assert_eq!(log.borrow()[1].0, Some(submit), "via the default button");
}
fn invalid_form_app(
form_attrs: &[(&str, &str)],
button_attrs: &[(&str, &str)],
) -> (App<TestBackend>, [rdom_core::NodeId; 4]) {
let mut ids = Vec::new();
let app = owner_app(|dom, root| {
let f = named(dom, root, "form", form_attrs);
let r = named(dom, f, "input", &[("name", "r"), ("required", "")]);
let t = named(dom, f, "input", &[("name", "t")]);
let b = named(dom, f, "button", button_attrs);
ids.extend([f, r, t, b]);
});
(app, [ids[0], ids[1], ids[2], ids[3]])
}
fn count_invalid(app: &mut App<TestBackend>, id: rdom_core::NodeId) -> Rc<Cell<u32>> {
let n = Rc::new(Cell::new(0u32));
let c = n.clone();
app.dom_mut()
.add_event_listener(id, "invalid", ListenerOptions::default(), move |_| {
c.set(c.get() + 1);
})
.unwrap();
n
}
#[test]
fn an_invalid_form_blocks_click_submission() {
let (mut app, [f, r, _, b]) = invalid_form_app(&[], &[]);
let log = record_submissions(&mut app, f);
let invalid = count_invalid(&mut app, r);
click_reset(&mut app, b);
assert!(log.borrow().is_empty(), "no submit while invalid");
assert_eq!(invalid.get(), 1);
assert_eq!(app.dom().focused(), Some(r));
use crate::accessors::TuiAccessorsMut;
app.dom_mut().node_mut(r).set_value("x").unwrap();
click_reset(&mut app, b);
assert_eq!(log.borrow().len(), 1);
}
#[test]
fn an_invalid_form_blocks_implicit_submission() {
let (mut app, [f, r, t, _]) = invalid_form_app(&[], &[]);
let log = record_submissions(&mut app, f);
press_enter_in(&mut app, t);
assert!(log.borrow().is_empty());
assert_eq!(app.dom().focused(), Some(r));
}
#[test]
fn a_canceled_invalid_event_still_blocks_submission_without_focusing() {
let (mut app, [f, r, _, b]) = invalid_form_app(&[], &[]);
let log = record_submissions(&mut app, f);
app.dom_mut()
.add_event_listener(r, "invalid", ListenerOptions::default(), |ctx| {
ctx.event.prevent_default();
})
.unwrap();
click_reset(&mut app, b);
assert!(log.borrow().is_empty());
assert_ne!(app.dom().focused(), Some(r));
}
#[test]
fn an_invalid_form_blocks_request_submit() {
use crate::SubmitOutcome;
use crate::accessors::TuiAccessorsMut;
let (mut app, [f, r, _, b]) = invalid_form_app(&[], &[]);
let log = record_submissions(&mut app, f);
let invalid = count_invalid(&mut app, r);
assert_eq!(
app.dom_mut().node_mut(f).form_request_submit(Some(b)),
Ok(SubmitOutcome::Invalid)
);
assert_eq!(
app.dom_mut().node_mut(f).form_request_submit(None),
Ok(SubmitOutcome::Invalid)
);
assert!(log.borrow().is_empty());
assert_eq!(invalid.get(), 2);
}
#[test]
fn novalidate_and_formnovalidate_skip_validation() {
use crate::SubmitOutcome;
use crate::accessors::TuiAccessorsMut;
let (mut app, [f, r, _, b]) = invalid_form_app(&[("novalidate", "")], &[]);
let log = record_submissions(&mut app, f);
let invalid = count_invalid(&mut app, r);
click_reset(&mut app, b);
assert_eq!(log.borrow().len(), 1, "form novalidate");
assert_eq!(invalid.get(), 0, "no validation ran");
let (mut app, [f, r, t, b]) = invalid_form_app(&[], &[("formnovalidate", "")]);
let log = record_submissions(&mut app, f);
click_reset(&mut app, b);
press_enter_in(&mut app, t);
assert_eq!(
app.dom_mut().node_mut(f).form_request_submit(Some(b)),
Ok(SubmitOutcome::Canceled),
"the recorder cancels, so the submit fired"
);
assert_eq!(
log.borrow().len(),
3,
"click, Enter (default button), requestSubmit"
);
assert_eq!(
app.dom_mut().node_mut(f).form_request_submit(None),
Ok(SubmitOutcome::Invalid),
"without the submitter, the form validates"
);
let _ = r;
}
fn reentry_app() -> (App<TestBackend>, [rdom_core::NodeId; 2], Rc<Cell<u32>>) {
let mut ids = Vec::new();
let mut app = owner_app(|dom, root| {
let f = named(dom, root, "form", &[]);
named(dom, f, "input", &[("name", "t")]);
let b = named(dom, f, "button", &[]);
ids.extend([f, b]);
});
let (f, b) = (ids[0], ids[1]);
let n = Rc::new(Cell::new(0u32));
let c = n.clone();
app.dom_mut()
.add_event_listener(f, "submit", ListenerOptions::default(), move |_| {
c.set(c.get() + 1);
})
.unwrap();
(app, [f, b], n)
}
#[test]
fn request_submit_from_a_submit_listener_does_not_resubmit() {
use crate::SubmitOutcome;
use crate::accessors::TuiAccessorsMut;
let (mut app, [f, b], submits) = reentry_app();
let inner: Rc<RefCell<Vec<SubmitOutcome>>> = Rc::default();
let log = inner.clone();
app.dom_mut()
.add_event_listener(f, "submit", ListenerOptions::default(), move |ctx| {
log.borrow_mut()
.push(ctx.dom.node_mut(f).form_request_submit(None).unwrap());
log.borrow_mut()
.push(ctx.dom.node_mut(f).form_request_submit(Some(b)).unwrap());
})
.unwrap();
assert_eq!(
app.dom_mut().node_mut(f).form_request_submit(Some(b)),
Ok(SubmitOutcome::Submitted)
);
assert_eq!(submits.get(), 1, "submit fires exactly once");
assert_eq!(
*inner.borrow(),
vec![SubmitOutcome::AlreadySubmitting; 2],
"the nested requestSubmit calls return early"
);
app.dom_mut().node_mut(f).form_request_submit(None).unwrap();
assert_eq!(submits.get(), 2);
}
#[test]
fn clicking_the_submitter_from_a_submit_listener_does_not_resubmit() {
use crate::accessors::TuiAccessorsMut;
let (mut app, [f, b], submits) = reentry_app();
app.dom_mut()
.add_event_listener(f, "submit", ListenerOptions::default(), move |ctx| {
ctx.dom.node_mut(b).click();
})
.unwrap();
app.dom_mut().node_mut(b).click();
assert_eq!(submits.get(), 1, "submit fires exactly once");
}
#[test]
fn request_submit_from_an_invalid_listener_does_not_revalidate() {
use crate::SubmitOutcome;
use crate::accessors::TuiAccessorsMut;
let (mut app, [f, r, _, b]) = invalid_form_app(&[], &[]);
let invalid = count_invalid(&mut app, r);
let inner: Rc<RefCell<Vec<SubmitOutcome>>> = Rc::default();
let log = inner.clone();
app.dom_mut()
.add_event_listener(r, "invalid", ListenerOptions::default(), move |ctx| {
log.borrow_mut()
.push(ctx.dom.node_mut(f).form_request_submit(None).unwrap());
})
.unwrap();
assert_eq!(
app.dom_mut().node_mut(f).form_request_submit(Some(b)),
Ok(SubmitOutcome::Invalid)
);
assert_eq!(invalid.get(), 1);
assert_eq!(*inner.borrow(), vec![SubmitOutcome::AlreadySubmitting]);
}
#[test]
fn a_panicking_submit_listener_does_not_leave_the_form_submitting() {
use crate::SubmitOutcome;
use crate::accessors::TuiAccessorsMut;
use std::panic::{AssertUnwindSafe, catch_unwind};
let (mut app, [f, _], submits) = reentry_app();
let armed = Rc::new(Cell::new(true));
let a = armed.clone();
app.dom_mut()
.add_event_listener(f, "submit", ListenerOptions::default(), move |_| {
if a.replace(false) {
panic!("submit listener bomb");
}
})
.unwrap();
let first = catch_unwind(AssertUnwindSafe(|| {
app.dom_mut().node_mut(f).form_request_submit(None)
}));
assert!(first.is_err(), "the bomb must fire");
assert_eq!(
app.dom_mut().node_mut(f).form_request_submit(None),
Ok(SubmitOutcome::Submitted),
"the form submits again after the panic"
);
assert_eq!(submits.get(), 2);
}
#[test]
fn a_disconnected_form_does_not_submit() {
use crate::SubmitOutcome;
use crate::accessors::TuiAccessorsMut;
let (mut app, [f, b], submits) = reentry_app();
let root = app.dom().root();
app.dom_mut().remove_child(root, f).unwrap();
assert_eq!(
app.dom_mut().node_mut(f).form_request_submit(Some(b)),
Ok(SubmitOutcome::Disconnected)
);
app.dom_mut().node_mut(b).click();
assert_eq!(submits.get(), 0);
}
#[test]
fn a_submit_listener_that_disconnects_the_form_stops_the_default_action() {
use crate::SubmitOutcome;
use crate::accessors::TuiAccessorsMut;
let mut ids = Vec::new();
let mut app = owner_app(|dom, root| {
let d = named(dom, root, "dialog", &[("open", "")]);
let f = named(dom, d, "form", &[("method", "dialog")]);
let b = named(dom, f, "button", &[("value", "ok")]);
ids.extend([d, f, b]);
});
let (d, f, b) = (ids[0], ids[1], ids[2]);
app.dom_mut()
.add_event_listener(f, "submit", ListenerOptions::default(), move |ctx| {
ctx.dom.remove_child(d, f).unwrap();
})
.unwrap();
assert_eq!(
app.dom_mut().node_mut(f).form_request_submit(Some(b)),
Ok(SubmitOutcome::Disconnected)
);
assert!(app.dom().node(d).has_attribute("open"), "dialog stays open");
}
#[test]
fn a_submit_listener_that_drops_the_form_reports_disconnected() {
use crate::SubmitOutcome;
use crate::accessors::TuiAccessorsMut;
let mut ids = Vec::new();
let mut app = owner_app(|dom, root| {
let d = named(dom, root, "dialog", &[("open", "")]);
let f = named(dom, d, "form", &[("method", "dialog")]);
let b = named(dom, f, "button", &[("value", "ok")]);
ids.extend([d, f, b]);
});
let (d, f, b) = (ids[0], ids[1], ids[2]);
app.dom_mut()
.add_event_listener(f, "submit", ListenerOptions::default(), move |ctx| {
ctx.dom.drop_subtree(f).unwrap();
})
.unwrap();
assert_eq!(
app.dom_mut().node_mut(f).form_request_submit(Some(b)),
Ok(SubmitOutcome::Disconnected)
);
assert!(!app.dom().contains(f));
assert!(app.dom().node(d).has_attribute("open"), "dialog stays open");
}
#[test]
fn clicking_an_image_button_submits_its_coordinates() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
dom.append_child(root, form).unwrap();
named(&mut dom, form, "input", &[("name", "q"), ("value", "rust")]);
let image = named(
&mut dom,
form,
"input",
&[
("type", "image"),
("name", "go"),
("alt", "Go"),
("value", "ignored"),
],
);
let mut app = test_app(dom, Stylesheet::new());
let log = record_submissions(&mut app, form);
use crate::accessors::TuiAccessorsMut;
app.dom_mut().node_mut(image).click();
assert_eq!(
*log.borrow(),
vec![(
Some(image),
pairs(&[("q", "rust"), ("go.x", "0"), ("go.y", "0")])
)]
);
}
#[test]
fn an_unnamed_image_button_submits_x_and_y_and_only_as_the_submitter() {
let mut ids = Vec::new();
let mut form_id = None;
let (app, _reset) = form_app(|dom, form| {
form_id = Some(form);
ids.push(named(dom, form, "input", &[("type", "image")]));
ids.push(named(
dom,
form,
"input",
&[("type", "submit"), ("name", "s")],
));
});
let form = form_id.unwrap();
let dom = app.dom();
assert_eq!(
form::collect_with_submitter(dom, form, Some(ids[0])),
pairs(&[("x", "0"), ("y", "0")])
);
assert_eq!(
form::collect_with_submitter(dom, form, Some(ids[1])),
pairs(&[("s", "")])
);
assert_eq!(form::collect(dom, form), pairs(&[]));
}
#[test]
fn enter_submits_with_a_default_image_button() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let form = dom.create_element("form");
dom.append_child(root, form).unwrap();
let q = named(&mut dom, form, "input", &[("name", "q"), ("value", "rust")]);
let image = named(
&mut dom,
form,
"input",
&[("type", "image"), ("name", "go")],
);
let mut app = test_app(dom, Stylesheet::new());
let log = record_submissions(&mut app, form);
press_enter_in(&mut app, q);
assert_eq!(
*log.borrow(),
vec![(
Some(image),
pairs(&[("q", "rust"), ("go.x", "0"), ("go.y", "0")])
)]
);
}
#[test]
fn a_form_cloned_inside_its_own_submit_listener_submits_normally() {
use crate::SubmitOutcome;
use crate::accessors::TuiAccessorsMut;
let (mut app, [f, _], _) = reentry_app();
let clone: Rc<Cell<Option<rdom_core::NodeId>>> = Rc::default();
let slot = clone.clone();
app.dom_mut()
.add_event_listener(f, "submit", ListenerOptions::default(), move |ctx| {
slot.set(Some(ctx.dom.clone_node(f, true)));
})
.unwrap();
app.dom_mut().node_mut(f).form_request_submit(None).unwrap();
let copy = clone.get().expect("the listener cloned the form");
let root = app.dom().root();
app.dom_mut().append_child(root, copy).unwrap();
assert_eq!(
app.dom_mut().node_mut(copy).form_request_submit(None),
Ok(SubmitOutcome::Submitted)
);
}
#[test]
fn a_cloned_input_has_no_custom_validity() {
use crate::accessors::{TuiAccessors, TuiAccessorsMut};
let mut dom: TuiDom = TuiDom::new();
let input = dom.create_element("input");
dom.append_child(dom.root(), input).unwrap();
dom.node_mut(input).set_custom_validity("taken").unwrap();
assert_eq!(
dom.node(input).validation_message().as_deref(),
Some("taken")
);
let copy = dom.clone_node(input, false);
dom.append_child(dom.root(), copy).unwrap();
assert_eq!(dom.node(copy).validation_message().as_deref(), Some(""));
}