use super::*;
use crate::base::{Point, Size};
use crate::theme::default_theme;
use crate::ui::{Key, Mods, UiEvent, UiTree};
use crate::widgets::itest_util::{key, key_mod, mount_widget, render, type_str};
fn focused_input(size: Size) -> (crate::reactive::RootScope, UiTree, Signal<String>) {
let t = &default_theme().tokens;
let holder: Rc<RefCell<Option<Signal<String>>>> = Rc::new(RefCell::new(None));
let h = holder.clone();
let (root, mut tree) = mount_widget(size, move |cx| {
let value = cx.signal(String::new());
*h.borrow_mut() = Some(value);
TextInput::new()
.value(value)
.placeholder("type here")
.element(cx, t)
.build()
});
key(&mut tree, Key::Tab); let sig = holder.borrow().expect("value signal");
(root, tree, sig)
}
#[test]
fn callbacks_may_dispose_the_inputs_scope() {
let t = &default_theme().tokens;
let mut tree = UiTree::new(Size::new(16, 1));
let (root, ()) = crate::reactive::create_root(|cx| {
let modal_cx = cx.child();
let view = TextInput::new()
.on_submit(move |_| modal_cx.dispose())
.element(modal_cx, t)
.build();
tree.mount(modal_cx, view);
});
tree.layout();
key(&mut tree, Key::Tab); type_str(&mut tree, "hi");
key(&mut tree, Key::Enter); assert_eq!(tree.instance_count(), 0, "subtree unmounted by dispose");
root.dispose();
let mut tree = UiTree::new(Size::new(16, 1));
let (root, ()) = crate::reactive::create_root(|cx| {
let modal_cx = cx.child();
let view = TextInput::new()
.on_change(move |_| modal_cx.dispose())
.element(modal_cx, t)
.build();
tree.mount(modal_cx, view);
});
tree.layout();
key(&mut tree, Key::Tab);
key(&mut tree, Key::Char('q')); assert_eq!(tree.instance_count(), 0, "subtree unmounted by dispose");
root.dispose();
}
#[test]
fn typing_inserts_and_renders_inside_the_frame() {
let theme = default_theme();
let size = Size::new(16, 1);
let (_root, mut tree, value) = focused_input(size);
type_str(&mut tree, "hello");
assert_eq!(value.get_untracked(), "hello");
let canvas = render(&mut tree, size);
assert_eq!(
canvas.cell(Point::new(1, 0)).unwrap().0,
'h',
"text starts after the stroke"
);
assert!(canvas.row_text(0).contains("hello"));
assert_eq!(
canvas.cell(Point::new(0, 0)).unwrap().1,
theme.tokens.border_focus
);
}
#[test]
fn backspace_delete_home_end_word_jump() {
let size = Size::new(20, 1);
let (_root, mut tree, value) = focused_input(size);
type_str(&mut tree, "one two three");
key(&mut tree, Key::Backspace); assert_eq!(value.get_untracked(), "one two thre");
key_mod(&mut tree, Key::Left, Mods::ALT); key(&mut tree, Key::Delete); assert_eq!(value.get_untracked(), "one two hre");
key(&mut tree, Key::Home);
key(&mut tree, Key::Delete);
assert_eq!(value.get_untracked(), "ne two hre");
key(&mut tree, Key::End);
type_str(&mut tree, "!");
assert_eq!(value.get_untracked(), "ne two hre!");
}
#[test]
fn selection_replaces_on_type_and_renders_selected_style() {
let theme = default_theme();
let t = &theme.tokens;
let size = Size::new(20, 1);
let (_root, mut tree, value) = focused_input(size);
type_str(&mut tree, "abcdef");
key(&mut tree, Key::Home);
key_mod(&mut tree, Key::Right, Mods::SHIFT);
key_mod(&mut tree, Key::Right, Mods::SHIFT); let canvas = render(&mut tree, size);
assert_eq!(canvas.cell(Point::new(1, 0)).unwrap().2, t.selection_bg);
type_str(&mut tree, "X"); assert_eq!(value.get_untracked(), "Xcdef");
}
#[test]
fn paste_goes_in_whole_and_scrolls_to_cursor() {
let size = Size::new(8, 1);
let (_root, mut tree, value) = focused_input(size);
tree.dispatch(&UiEvent::Paste("pasted line\nwith break".into()));
assert_eq!(value.get_untracked(), "pasted line with break");
let canvas = render(&mut tree, size);
assert_ne!(
canvas.cell(Point::new(1, 0)).unwrap().0,
'p',
"scrolled: {:?}",
canvas.row_text(0)
);
}
#[test]
fn submit_fires_with_current_value() {
let t = &default_theme().tokens;
let submitted: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
let s2 = submitted.clone();
let (_root, mut tree) = mount_widget(Size::new(16, 1), move |cx| {
TextInput::new()
.on_submit(move |v| s2.borrow_mut().push(v.to_string()))
.element(cx, t)
.build()
});
key(&mut tree, Key::Tab);
type_str(&mut tree, "ok");
key(&mut tree, Key::Enter);
assert_eq!(*submitted.borrow(), vec!["ok".to_string()]);
}
#[test]
fn placeholder_shows_only_unfocused_empty() {
let size = Size::new(16, 1);
let t = &default_theme().tokens;
let (_root, mut tree) = mount_widget(size, |cx| {
TextInput::new()
.placeholder("type here")
.element(cx, t)
.build()
});
let theme = default_theme();
let canvas = render(&mut tree, size);
assert!(canvas.row_text(0).contains("type here"));
assert_eq!(
canvas.cell(Point::new(1, 0)).unwrap().1,
theme.tokens.text_faint,
"placeholder ink is text_faint (§3)"
);
key(&mut tree, Key::Tab); let canvas = render(&mut tree, size);
assert!(!canvas.row_text(0).contains("type here"));
}
#[test]
fn placeholder_while_focused_paints_beside_the_caret() {
let size = Size::new(16, 1);
let t = &default_theme().tokens;
let theme = default_theme();
let (_root, mut tree) = mount_widget(size, |cx| {
TextInput::new()
.placeholder("type here")
.placeholder_while_focused(true)
.element(cx, t)
.build()
});
key(&mut tree, Key::Tab); let canvas = render(&mut tree, size);
let caret = canvas.cell(Point::new(1, 0)).unwrap();
assert_eq!(caret.0, ' ', "caret cell is a styled blank");
assert_eq!(caret.2, theme.tokens.cursor, "caret bg is the cursor token");
assert_eq!(canvas.cell(Point::new(2, 0)).unwrap().0, 't');
assert_eq!(
canvas.cell(Point::new(2, 0)).unwrap().1,
theme.tokens.text_faint,
"focused placeholder keeps the text_faint ink"
);
assert!(canvas.row_text(0).contains("type here"));
type_str(&mut tree, "x");
let canvas = render(&mut tree, size);
assert!(
!canvas.row_text(0).contains("type here"),
"typing hides the focused placeholder: {:?}",
canvas.row_text(0)
);
assert_eq!(canvas.cell(Point::new(1, 0)).unwrap().0, 'x');
assert_eq!(
canvas.cell(Point::new(2, 0)).unwrap().2,
theme.tokens.cursor,
"caret advanced past the typed glyph"
);
}
#[test]
fn placeholder_clips_to_the_interior_at_narrow_widths() {
let size = Size::new(16, 1);
let t = &default_theme().tokens;
let (_root, mut tree) = mount_widget(size, |cx| {
crate::ui::Element::new()
.style(LayoutStyle::column())
.child(
TextInput::new()
.placeholder("a very long hint")
.placeholder_while_focused(true)
.layout(LayoutStyle::default().width(Dimension::Cells(8)))
.element(cx, t)
.build(),
)
.build()
});
let canvas = render(&mut tree, size);
assert_eq!(canvas.cell(Point::new(6, 0)).unwrap().0, '…');
assert_eq!(
canvas.cell(Point::new(7, 0)).unwrap().0,
'▌',
"right stroke survives the hint"
);
assert!(
canvas.row_text(0).chars().skip(8).all(|c| c == ' '),
"hint escaped the widget rect: {:?}",
canvas.row_text(0)
);
key(&mut tree, Key::Tab);
let canvas = render(&mut tree, size);
assert_eq!(canvas.cell(Point::new(6, 0)).unwrap().0, '…');
assert_eq!(canvas.cell(Point::new(7, 0)).unwrap().0, '▌');
assert!(
canvas.row_text(0).chars().skip(8).all(|c| c == ' '),
"focused hint escaped the widget rect: {:?}",
canvas.row_text(0)
);
}
#[test]
fn placeholder_width_three_degrades_to_a_bare_ellipsis() {
let size = Size::new(8, 1);
let t = &default_theme().tokens;
let (_root, mut tree) = mount_widget(size, |cx| {
crate::ui::Element::new()
.style(LayoutStyle::column())
.child(
TextInput::new()
.placeholder("hello")
.layout(LayoutStyle::default().width(Dimension::Cells(3)))
.element(cx, t)
.build(),
)
.build()
});
let canvas = render(&mut tree, size);
assert_eq!(canvas.cell(Point::new(0, 0)).unwrap().0, '▐');
assert_eq!(canvas.cell(Point::new(1, 0)).unwrap().0, '…');
assert_eq!(canvas.cell(Point::new(2, 0)).unwrap().0, '▌');
assert!(canvas.row_text(0).chars().skip(3).all(|c| c == ' '));
}
fn masked_input(size: Size) -> (crate::reactive::RootScope, UiTree, Signal<String>) {
let t = &default_theme().tokens;
let holder: Rc<RefCell<Option<Signal<String>>>> = Rc::new(RefCell::new(None));
let h = holder.clone();
let (root, mut tree) = mount_widget(size, move |cx| {
let value = cx.signal(String::new());
*h.borrow_mut() = Some(value);
TextInput::new()
.value(value)
.placeholder("api key")
.masked(true)
.element(cx, t)
.build()
});
key(&mut tree, Key::Tab); let sig = holder.borrow().expect("value signal");
(root, tree, sig)
}
#[test]
fn masked_field_leaks_no_plaintext_through_screen_or_semantic_tree() {
let size = Size::new(24, 1);
let (_root, mut tree, value) = masked_input(size);
tree.dispatch(&UiEvent::Paste("secret\u{1F9D1}\u{200D}\u{1F680}".into()));
assert_eq!(value.get_untracked(), "secret\u{1F9D1}\u{200D}\u{1F680}");
let canvas = render(&mut tree, size);
let row = canvas.row_text(0);
assert!(!row.contains("secret"), "plaintext on screen: {row:?}");
assert!(!row.contains('\u{1F9D1}'), "emoji on screen: {row:?}");
assert_eq!(
row.matches('\u{2022}').count(),
7,
"one bullet per grapheme cluster: {row:?}"
);
let a11y = tree.accessibility_tree_text();
assert!(
!a11y.contains("secret"),
"plaintext in the semantic tree: {a11y}"
);
assert!(
a11y.contains(&"\u{2022}".repeat(7)),
"access_value exports cluster-count bullets: {a11y}"
);
assert!(a11y.contains("api key"));
}
#[test]
fn masked_word_jump_treats_whole_value_as_one_word() {
let size = Size::new(24, 1);
let (_root, mut tree, value) = masked_input(size);
tree.dispatch(&UiEvent::Paste("one two three".into()));
key_mod(&mut tree, Key::Left, Mods::ALT);
type_str(&mut tree, "X");
assert_eq!(value.get_untracked(), "Xone two three", "cursor was at 0");
key_mod(&mut tree, Key::Right, Mods::ALT);
type_str(&mut tree, "Y");
assert_eq!(
value.get_untracked(),
"Xone two threeY",
"cursor was at len"
);
key_mod(&mut tree, Key::Left, Mods::SHIFT | Mods::ALT);
type_str(&mut tree, "Z");
assert_eq!(value.get_untracked(), "Z", "whole-field selection replaced");
let (_r2, mut plain_tree, plain_value) = focused_input(size);
plain_tree.dispatch(&UiEvent::Paste("one two three".into()));
key_mod(&mut plain_tree, Key::Left, Mods::ALT); type_str(&mut plain_tree, "X");
assert_eq!(plain_value.get_untracked(), "one two Xthree");
}
#[test]
fn masked_editing_stays_cluster_atomic_and_value_stays_real() {
let size = Size::new(24, 1);
let (_root, mut tree, value) = masked_input(size);
tree.dispatch(&UiEvent::Paste("ab\u{1F9D1}\u{200D}\u{1F680}cd".into()));
key(&mut tree, Key::Backspace); assert_eq!(value.get_untracked(), "ab\u{1F9D1}\u{200D}\u{1F680}c");
key(&mut tree, Key::Backspace); key(&mut tree, Key::Backspace); assert_eq!(value.get_untracked(), "ab");
type_str(&mut tree, "XY");
assert_eq!(value.get_untracked(), "abXY");
let canvas = render(&mut tree, size);
assert_eq!(canvas.row_text(0).matches('\u{2022}').count(), 4);
let t = &default_theme().tokens;
let (_r2, mut plain_tree) = mount_widget(size, move |cx| {
let v = cx.signal(String::from("visible"));
TextInput::new().value(v).element(cx, t).build()
});
assert!(plain_tree.accessibility_tree_text().contains("visible"));
}