use crossterm::event::{
Event as CtEvent, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers,
};
use rdom_core::{ListenerOptions, NodeId};
use std::cell::Cell;
use std::rc::Rc;
use crate::TuiDom;
use crate::render::{Terminal, TestBackend};
use crate::runtime::app::App;
use crate::style::Stylesheet;
fn test_app(dom: TuiDom) -> App<TestBackend> {
let backend = TestBackend::new(20, 5);
let terminal = Terminal::new(backend).unwrap();
App::with_backend(dom, Stylesheet::bare(), terminal).unwrap()
}
fn button_focused() -> (App<TestBackend>, NodeId) {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let btn = dom.create_element("button");
dom.append_child(root, btn).unwrap();
dom.set_focused(Some(btn));
(test_app(dom), btn)
}
fn key_press(code: KeyCode, modifiers: KeyModifiers) -> CtEvent {
CtEvent::Key(KeyEvent {
code,
modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::empty(),
})
}
fn record_click_count(app: &mut App<TestBackend>, btn: NodeId) -> Rc<Cell<u32>> {
let count = Rc::new(Cell::new(0));
let c = count.clone();
app.dom_mut()
.add_event_listener(btn, "click", ListenerOptions::default(), move |_| {
c.set(c.get() + 1);
})
.unwrap();
count
}
#[test]
fn enter_on_focused_button_synthesizes_click() {
let (mut app, btn) = button_focused();
let count = record_click_count(&mut app, btn);
app.handle_event(key_press(KeyCode::Enter, KeyModifiers::empty()));
assert_eq!(count.get(), 1);
}
#[test]
fn space_on_focused_button_synthesizes_click() {
let (mut app, btn) = button_focused();
let count = record_click_count(&mut app, btn);
app.handle_event(key_press(KeyCode::Char(' '), KeyModifiers::empty()));
assert_eq!(count.get(), 1);
}
#[test]
fn enter_on_non_button_does_not_synthesize_click() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.set_attribute(div, "tabindex", "0").unwrap();
dom.append_child(root, div).unwrap();
dom.set_focused(Some(div));
let mut app = test_app(dom);
let count = Rc::new(Cell::new(0));
let c = count.clone();
app.dom_mut()
.add_event_listener(div, "click", ListenerOptions::default(), move |_| {
c.set(c.get() + 1);
})
.unwrap();
app.handle_event(key_press(KeyCode::Enter, KeyModifiers::empty()));
assert_eq!(count.get(), 0);
}
#[test]
fn ctrl_enter_is_not_activation() {
let (mut app, btn) = button_focused();
let count = record_click_count(&mut app, btn);
app.handle_event(key_press(KeyCode::Enter, KeyModifiers::CONTROL));
assert_eq!(count.get(), 0);
}
#[test]
fn keydown_prevent_default_suppresses_click() {
let (mut app, btn) = button_focused();
app.dom_mut()
.add_event_listener(btn, "keydown", ListenerOptions::default(), |ctx| {
ctx.event.prevent_default();
})
.unwrap();
let count = record_click_count(&mut app, btn);
app.handle_event(key_press(KeyCode::Enter, KeyModifiers::empty()));
assert_eq!(count.get(), 0);
}
#[test]
fn synthesized_click_is_marked_synthetic() {
let (mut app, btn) = button_focused();
let saw_synthetic = Rc::new(Cell::new(false));
let s = saw_synthetic.clone();
app.dom_mut()
.add_event_listener(btn, "click", ListenerOptions::default(), move |ctx| {
s.set(ctx.event.is_synthetic());
})
.unwrap();
app.handle_event(key_press(KeyCode::Enter, KeyModifiers::empty()));
assert!(saw_synthetic.get());
}
#[test]
fn disabled_button_does_not_activate() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let btn = dom.create_element("button");
dom.set_attribute(btn, "disabled", "").unwrap();
dom.append_child(root, btn).unwrap();
dom.set_focused(Some(btn));
let mut app = test_app(dom);
let count = record_click_count(&mut app, btn);
app.handle_event(key_press(KeyCode::Enter, KeyModifiers::empty()));
assert_eq!(count.get(), 1);
}
fn input_button_focused(ty: &str) -> (App<TestBackend>, NodeId) {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let inp = dom.create_element("input");
dom.set_attribute(inp, "type", ty).unwrap();
dom.append_child(root, inp).unwrap();
dom.set_focused(Some(inp));
(test_app(dom), inp)
}
#[test]
fn enter_on_input_type_submit_synthesizes_click() {
let (mut app, btn) = input_button_focused("submit");
let count = record_click_count(&mut app, btn);
app.handle_event(key_press(KeyCode::Enter, KeyModifiers::empty()));
assert_eq!(count.get(), 1);
}
#[test]
fn space_on_input_type_reset_synthesizes_click() {
let (mut app, btn) = input_button_focused("reset");
let count = record_click_count(&mut app, btn);
app.handle_event(key_press(KeyCode::Char(' '), KeyModifiers::empty()));
assert_eq!(count.get(), 1);
}
#[test]
fn enter_on_input_type_button_synthesizes_click() {
let (mut app, btn) = input_button_focused("button");
let count = record_click_count(&mut app, btn);
app.handle_event(key_press(KeyCode::Enter, KeyModifiers::empty()));
assert_eq!(count.get(), 1);
}
#[test]
fn enter_on_text_input_does_not_synthesize_click() {
let (mut app, btn) = input_button_focused("text");
let count = record_click_count(&mut app, btn);
app.handle_event(key_press(KeyCode::Enter, KeyModifiers::empty()));
assert_eq!(count.get(), 0);
}
fn input_button(ty: &str, value: Option<&str>) -> (TuiDom, NodeId) {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let inp = dom.create_element("input");
dom.set_attribute(inp, "type", ty).unwrap();
if let Some(v) = value {
dom.set_attribute(inp, "value", v).unwrap();
}
dom.append_child(root, inp).unwrap();
(dom, inp)
}
fn painted_row(dom: &mut TuiDom) -> String {
use crate::prelude::*;
let viewport = Rect::new(0, 0, 20, 1);
dom.cascade(&Stylesheet::new());
dom.layout_dom(viewport);
let mut buf = Buffer::empty(viewport);
dom.paint_dom(&mut buf, viewport);
let mut s = String::new();
for x in 0..viewport.width {
if let Some(c) = buf.cell(x, 0)
&& !c.is_spacer()
{
s.push_str(c.symbol());
}
}
s.trim_end().to_string()
}
#[test]
fn submit_input_paints_its_value_as_the_label() {
let (mut dom, _) = input_button("submit", Some("Go"));
assert_eq!(painted_row(&mut dom), "[ Go ]");
}
#[test]
fn value_less_submit_input_paints_the_default_submit_label() {
let (mut dom, _) = input_button("submit", None);
assert_eq!(painted_row(&mut dom), "[ Submit ]");
}
#[test]
fn value_less_reset_input_paints_the_default_reset_label() {
let (mut dom, _) = input_button("reset", None);
assert_eq!(painted_row(&mut dom), "[ Reset ]");
}
#[test]
fn reset_input_paints_its_value_as_the_label() {
let (mut dom, _) = input_button("reset", Some("Clear"));
assert_eq!(painted_row(&mut dom), "[ Clear ]");
}
#[test]
fn button_input_paints_its_value_as_the_label() {
let (mut dom, _) = input_button("button", Some("Open"));
assert_eq!(painted_row(&mut dom), "[ Open ]");
}
#[test]
fn value_less_button_input_paints_an_empty_label() {
let (mut dom, _) = input_button("button", None);
assert_eq!(painted_row(&mut dom), "[ ]");
}
#[test]
fn empty_value_submit_input_paints_an_empty_label() {
let (mut dom, _) = input_button("submit", Some(""));
assert_eq!(painted_row(&mut dom), "[ ]");
}
#[test]
fn submit_input_label_follows_a_later_value_change() {
let (mut dom, inp) = input_button("submit", Some("Go"));
assert_eq!(painted_row(&mut dom), "[ Go ]");
dom.set_attribute(inp, "value", "Send").unwrap();
assert_eq!(painted_row(&mut dom), "[ Send ]");
}
#[test]
fn submit_input_box_is_as_wide_as_its_label() {
use crate::node::TuiNodeExt;
let (mut dom, inp) = input_button("submit", Some("Go"));
painted_row(&mut dom);
let rect = dom.node(inp).layout_rect().expect("laid out");
assert_eq!(rect.width, 6, "`[ Go ]` is 6 cells: {rect:?}");
}
#[test]
fn a_button_element_keeps_its_own_children_as_the_label() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let btn = dom.create_element("button");
dom.set_attribute(btn, "value", "v").unwrap();
let t = dom.create_text_node("Save");
dom.append_child(btn, t).unwrap();
dom.append_child(root, btn).unwrap();
assert_eq!(painted_row(&mut dom), "[ Save ]");
}
#[test]
fn a_click_on_the_label_activates_the_submit_input() {
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
let (dom, inp) = input_button("submit", Some("Go"));
let terminal = Terminal::new(TestBackend::new(20, 5)).unwrap();
let mut app = App::with_backend(dom, Stylesheet::new(), terminal).unwrap();
let count = record_click_count(&mut app, inp);
app.draw_if_dirty().unwrap();
for x in 0..6 {
for kind in [
MouseEventKind::Down(MouseButton::Left),
MouseEventKind::Up(MouseButton::Left),
] {
app.handle_event(CtEvent::Mouse(MouseEvent {
kind,
column: x,
row: 0,
modifiers: KeyModifiers::empty(),
}));
}
assert_eq!(count.get(), u32::from(x) + 1, "click at x={x}");
}
for kind in [
MouseEventKind::Down(MouseButton::Left),
MouseEventKind::Up(MouseButton::Left),
] {
app.handle_event(CtEvent::Mouse(MouseEvent {
kind,
column: 6,
row: 0,
modifiers: KeyModifiers::empty(),
}));
}
assert_eq!(count.get(), 6, "click at x=6 is past the box");
}
#[test]
fn image_input_paints_its_alt_text_as_the_label() {
let (mut dom, inp) = input_button("image", Some("ignored"));
dom.set_attribute(inp, "alt", "Go").unwrap();
assert_eq!(painted_row(&mut dom), "[ Go ]");
}
#[test]
fn alt_less_image_input_paints_the_default_submit_label() {
let (mut dom, _) = input_button("image", None);
assert_eq!(painted_row(&mut dom), "[ Submit ]");
}