use std::sync::Arc;
use crate::input_profile::{set_input_profile, InputProfile};
use crate::text::Font;
use crate::widgets::on_screen_keyboard::{
self, drain_synthetic_keys, is_enabled, is_visible, set_enabled, set_text_input_focused,
test_hook, KeyboardInputMode,
};
use crate::widgets::{ScrollView, TextField};
use crate::{App, FlexColumn, Modifiers, MouseButton, Size, SizedBox};
use super::TEST_FONT;
fn fresh_state() {
on_screen_keyboard::dismiss();
test_hook::reset();
crate::widgets::on_screen_keyboard::events::clear();
crate::widget::keyboard_scroll::reset_lift_for_test();
crate::ux_scale::set_ux_scale(1.0);
}
fn set_mobile_profile_for_test() {
set_input_profile(InputProfile::MobileIOS);
}
#[test]
fn keyboard_disabled_by_default() {
fresh_state();
assert!(
!is_enabled(),
"keyboard must start disabled so desktop apps see no behavior change"
);
assert!(!is_visible());
}
#[test]
fn enabling_keyboard_does_not_make_it_visible_alone() {
fresh_state();
set_enabled(true);
assert!(is_enabled());
assert!(!is_visible());
}
#[test]
fn focusing_text_input_raises_keyboard() {
fresh_state();
set_enabled(true);
set_text_input_focused(true, Some(""), KeyboardInputMode::Text);
test_hook::force_visible();
assert!(
is_visible(),
"keyboard should be visible after force_visible"
);
}
#[test]
fn auto_cap_on_empty_field() {
fresh_state();
set_enabled(true);
set_text_input_focused(true, Some(""), KeyboardInputMode::Text);
use crate::widgets::on_screen_keyboard::layouts::Layer;
use crate::widgets::on_screen_keyboard::state::with_state_ref;
assert_eq!(
with_state_ref(|s| s.current_layer),
Layer::Shifted,
"empty field should auto-cap the first letter"
);
}
#[test]
fn auto_cap_after_sentence_terminator() {
fresh_state();
set_enabled(true);
set_text_input_focused(true, Some("Hello world."), KeyboardInputMode::Text);
use crate::widgets::on_screen_keyboard::layouts::Layer;
use crate::widgets::on_screen_keyboard::state::with_state_ref;
assert_eq!(
with_state_ref(|s| s.current_layer),
Layer::Shifted,
"field ending in '.' should auto-cap the next letter"
);
}
#[test]
fn double_tap_shift_engages_caps_lock() {
fresh_state();
set_enabled(true);
set_text_input_focused(true, Some("hello"), KeyboardInputMode::Text);
use crate::widgets::on_screen_keyboard::layouts::Layer;
test_hook::simulate_shift_tap();
test_hook::simulate_shift_tap();
assert!(test_hook::caps_lock(), "double-tap should engage caps lock");
assert_eq!(test_hook::current_layer(), Layer::Shifted);
test_hook::simulate_shift_tap();
assert!(
!test_hook::caps_lock(),
"third tap should release caps lock"
);
assert_eq!(test_hook::current_layer(), Layer::Letters);
}
#[test]
fn single_shift_tap_does_not_engage_caps_lock() {
fresh_state();
set_enabled(true);
set_text_input_focused(true, Some("hello"), KeyboardInputMode::Text);
test_hook::simulate_shift_tap();
assert!(!test_hook::caps_lock(), "one tap is one-shot shift");
}
#[test]
fn no_auto_cap_mid_sentence() {
fresh_state();
set_enabled(true);
set_text_input_focused(true, Some("Hello world"), KeyboardInputMode::Text);
use crate::widgets::on_screen_keyboard::layouts::Layer;
use crate::widgets::on_screen_keyboard::state::with_state_ref;
assert_eq!(
with_state_ref(|s| s.current_layer),
Layer::Letters,
"field with mid-sentence text should not auto-cap"
);
}
#[test]
fn dismiss_lowers_keyboard() {
fresh_state();
set_enabled(true);
test_hook::force_visible();
assert!(is_visible());
on_screen_keyboard::dismiss();
test_hook::reset();
assert!(!is_visible());
}
#[test]
fn dismiss_clears_text_field_focus_and_drops_lift() {
fresh_state();
set_mobile_profile_for_test();
set_enabled(true);
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let field = TextField::new(Arc::clone(&font)).with_text("hi");
let mut app = App::new(Box::new(field));
app.layout(Size::new(400.0, 800.0));
app.on_mouse_down(50.0, 50.0, MouseButton::Left, Modifiers::default());
app.on_mouse_up(50.0, 50.0, MouseButton::Left, Modifiers::default());
assert!(app.has_focus(), "TextField should be focused after tap");
assert!(
crate::widgets::on_screen_keyboard::state::with_state_ref(|s| s.text_input_focused),
"keyboard should be tracking a focused text input",
);
assert!(
crate::widget::keyboard_scroll::lift_target_for_test() > 0.0,
"lift target should be positive (focused field is below the keyboard panel)",
);
on_screen_keyboard::dismiss();
app.drain_keyboard_events_for_test();
assert!(
!app.has_focus(),
"dismiss must clear focus on the text field so the next focus event lands cleanly",
);
assert_eq!(
crate::widget::keyboard_scroll::lift_target_for_test(),
0.0,
"dismiss must retarget the screen lift back to 0 so the tree comes back down",
);
}
#[test]
fn pointer_outside_panel_falls_through() {
fresh_state();
set_enabled(true);
test_hook::force_visible();
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let field = TextField::new(Arc::clone(&font)).with_text("hi");
let mut app = App::new(Box::new(field));
app.layout(Size::new(400.0, 800.0));
app.on_mouse_down(50.0, 5.0, MouseButton::Left, Modifiers::default());
assert!(
app.has_focus(),
"mouse-down above the keyboard should still reach the text field"
);
}
#[test]
fn numeric_mode_opens_on_numbers_layer() {
fresh_state();
set_enabled(true);
set_text_input_focused(true, Some(""), KeyboardInputMode::Numeric);
use crate::widgets::on_screen_keyboard::layouts::Layer;
use crate::widgets::on_screen_keyboard::state::with_state_ref;
assert_eq!(
with_state_ref(|s| s.current_layer),
Layer::Numbers,
"Numeric mode must open the keyboard on the digit layer, not Letters/Shifted"
);
}
#[test]
fn numeric_mode_clears_residual_caps_lock() {
fresh_state();
set_enabled(true);
set_text_input_focused(true, Some(""), KeyboardInputMode::Text);
test_hook::simulate_shift_tap();
test_hook::simulate_shift_tap();
assert!(test_hook::caps_lock(), "precondition: caps-lock engaged");
set_text_input_focused(true, Some(""), KeyboardInputMode::Numeric);
assert!(
!test_hook::caps_lock(),
"switching focus into a Numeric field must drop the stale caps-lock state"
);
}
#[test]
fn focusing_field_below_keyboard_scrolls_parent_view() {
fresh_state();
set_mobile_profile_for_test();
set_enabled(true);
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let mut col = FlexColumn::new();
col.push(Box::new(SizedBox::new().with_height(600.0)), 0.0);
let field = TextField::new(Arc::clone(&font)).with_placeholder("type here");
col.push(Box::new(field), 0.0);
let offset_cell = std::rc::Rc::new(std::cell::Cell::new(0.0_f64));
let scroll = ScrollView::new(Box::new(col)).with_offset_cell(std::rc::Rc::clone(&offset_cell));
let mut app = App::new(Box::new(scroll));
app.layout(Size::new(400.0, 400.0));
let panel_h = on_screen_keyboard::target_panel_height(400.0);
assert!(
panel_h > 0.0,
"keyboard layout must report a positive panel height"
);
let scroll_before = offset_cell.get();
app.on_key_down(crate::Key::Tab, Modifiers::default());
assert!(app.has_focus(), "TextField should be focused after Tab");
assert_eq!(app.focused_widget_type_name(), Some("TextField"));
app.layout(Size::new(400.0, 400.0));
let scroll_after = offset_cell.get();
assert!(
scroll_after > scroll_before + 1.0,
"scroll offset must increase to lift the focused field above the keyboard \
(before={:.1}, after={:.1}, panel_h={:.1})",
scroll_before,
scroll_after,
panel_h,
);
}
#[test]
fn focusing_field_with_no_scrollable_ancestor_requests_global_lift() {
fresh_state();
set_mobile_profile_for_test();
set_enabled(true);
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let mut col = FlexColumn::new();
col.push(Box::new(SizedBox::new().with_height(360.0)), 0.0);
let field = TextField::new(Arc::clone(&font)).with_placeholder("at the bottom");
col.push(Box::new(field), 0.0);
let mut app = App::new(Box::new(col));
app.layout(Size::new(400.0, 400.0));
let lift_before = crate::widget::keyboard_scroll::current_lift();
app.on_key_down(crate::Key::Tab, Modifiers::default());
assert!(app.has_focus());
assert!(
crate::widget::keyboard_scroll::is_lift_animating()
|| crate::widget::keyboard_scroll::current_lift() > lift_before + 0.5,
"auto-scroll must engage the global lift when no ScrollView can absorb the deficit",
);
}
#[test]
fn focusing_already_visible_field_does_not_scroll() {
fresh_state();
set_mobile_profile_for_test();
set_enabled(true);
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let mut col = FlexColumn::new();
let field = TextField::new(Arc::clone(&font)).with_placeholder("type here");
col.push(Box::new(field), 0.0);
col.push(Box::new(SizedBox::new().with_height(600.0)), 0.0);
let offset_cell = std::rc::Rc::new(std::cell::Cell::new(0.0_f64));
let scroll = ScrollView::new(Box::new(col)).with_offset_cell(std::rc::Rc::clone(&offset_cell));
let mut app = App::new(Box::new(scroll));
app.layout(Size::new(400.0, 400.0));
let scroll_before = offset_cell.get();
app.on_key_down(crate::Key::Tab, Modifiers::default());
assert!(app.has_focus());
app.layout(Size::new(400.0, 400.0));
let scroll_after = offset_cell.get();
assert!(
(scroll_after - scroll_before).abs() < 0.5,
"scroll offset must be unchanged when the focused field is already visible \
(before={:.1}, after={:.1})",
scroll_before,
scroll_after,
);
}
#[test]
fn text_field_with_keyboard_mode_propagates_through_focus() {
fresh_state();
set_mobile_profile_for_test();
set_enabled(true);
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let field = TextField::new(Arc::clone(&font)).with_keyboard_mode(KeyboardInputMode::Numeric);
let mut app = App::new(Box::new(field));
app.layout(Size::new(400.0, 800.0));
app.on_key_down(crate::Key::Tab, Modifiers::default());
assert!(app.has_focus(), "TextField should be focused after Tab");
use crate::widgets::on_screen_keyboard::layouts::Layer;
use crate::widgets::on_screen_keyboard::state::with_state_ref;
assert_eq!(
with_state_ref(|s| s.current_layer),
Layer::Numbers,
"focusing a Numeric TextField must raise the keyboard on the digit layer"
);
}
#[test]
fn synthetic_key_queue_drains_to_focused_field() {
fresh_state();
set_mobile_profile_for_test();
set_enabled(true);
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let field = TextField::new(Arc::clone(&font));
let mut app = App::new(Box::new(field));
app.layout(Size::new(400.0, 800.0));
app.on_key_down(crate::Key::Tab, Modifiers::default());
assert!(app.has_focus(), "TextField should be focused after Tab");
crate::widgets::on_screen_keyboard::events::push_synthetic_key(
crate::Key::Char('h'),
Modifiers::default(),
);
crate::widgets::on_screen_keyboard::events::push_synthetic_key(
crate::Key::Char('i'),
Modifiers::default(),
);
let pending = drain_synthetic_keys();
assert_eq!(pending.len(), 2);
for (key, mods) in pending {
app.on_key_down(key, mods);
}
assert_eq!(app.focused_widget_type_name(), Some("TextField"));
}