use super::*;
use proptest::prelude::*;
#[test]
fn test_new_tooltip_state_with_auto_show_true() {
let state = TooltipState::new(true);
assert!(state.enabled);
assert!(state.current_function.is_none());
}
#[test]
fn test_new_tooltip_state_with_auto_show_false() {
let state = TooltipState::new(false);
assert!(!state.enabled);
assert!(state.current_function.is_none());
}
#[test]
fn test_default_creates_enabled_state() {
let state = TooltipState::default();
assert!(state.enabled);
}
#[test]
fn test_toggle() {
let mut state = TooltipState::new(true);
assert!(state.enabled);
state.toggle();
assert!(!state.enabled);
state.toggle();
assert!(state.enabled);
}
#[test]
fn test_set_current_function() {
let mut state = TooltipState::new(true);
state.set_current_function(Some("select".to_string()));
assert_eq!(state.current_function, Some("select".to_string()));
state.set_current_function(None);
assert!(state.current_function.is_none());
}
#[test]
fn test_should_show() {
let mut state = TooltipState::new(true);
assert!(!state.should_show());
state.set_current_function(Some("map".to_string()));
assert!(state.should_show());
state.toggle();
assert!(!state.should_show());
state.set_current_function(None);
assert!(!state.should_show());
}
#[test]
fn test_new_tooltip_state_has_no_operator() {
let state = TooltipState::new(true);
assert!(state.current_operator.is_none());
}
#[test]
fn test_set_current_operator() {
let mut state = TooltipState::new(true);
state.set_current_operator(Some("//".to_string()));
assert_eq!(state.current_operator, Some("//".to_string()));
state.set_current_operator(None);
assert!(state.current_operator.is_none());
}
#[test]
fn test_should_show_with_operator() {
let mut state = TooltipState::new(true);
assert!(!state.should_show());
state.set_current_operator(Some("//".to_string()));
assert!(state.should_show());
state.toggle();
assert!(!state.should_show());
state.set_current_operator(None);
assert!(!state.should_show());
}
#[test]
fn test_should_show_with_function_or_operator() {
let mut state = TooltipState::new(true);
state.set_current_function(Some("map".to_string()));
assert!(state.should_show());
state.set_current_operator(Some("//".to_string()));
assert!(state.should_show());
state.set_current_function(None);
assert!(state.should_show());
state.set_current_operator(None);
assert!(!state.should_show());
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_toggle_round_trip(initial_enabled: bool, has_function: bool, function_name in "[a-z_]+") {
let mut state = TooltipState::new(initial_enabled);
if has_function {
state.set_current_function(Some(function_name.clone()));
}
let original_enabled = state.enabled;
let original_function = state.current_function.clone();
state.toggle();
state.toggle();
prop_assert_eq!(state.enabled, original_enabled, "Toggle round-trip should preserve enabled state");
prop_assert_eq!(state.current_function, original_function, "Toggle should not affect current_function");
}
#[test]
fn prop_toggle_disabled_hides_tooltip(function_name in "[a-z_]+") {
let mut state = TooltipState::new(true);
state.set_current_function(Some(function_name));
prop_assert!(state.should_show(), "Should show when enabled with function");
state.toggle();
prop_assert!(!state.should_show(), "Should not show when disabled");
prop_assert!(!state.enabled, "Should be disabled after toggle");
}
#[test]
fn prop_toggle_enabled_with_function_shows_tooltip(function_name in "[a-z_]+") {
let mut state = TooltipState::new(false);
state.set_current_function(Some(function_name));
prop_assert!(!state.should_show(), "Should not show when disabled");
state.toggle();
prop_assert!(state.should_show(), "Should show when enabled with function");
prop_assert!(state.enabled, "Should be enabled after toggle");
}
#[test]
fn prop_hint_visibility_matches_state(
enabled: bool,
has_function: bool,
function_name in "[a-z_]+"
) {
let mut state = TooltipState::new(enabled);
if has_function {
state.set_current_function(Some(function_name));
} else {
state.set_current_function(None);
}
let should_show_input_hint = !state.enabled && state.current_function.is_some();
let should_show_tooltip = state.should_show();
if !state.enabled && state.current_function.is_some() {
prop_assert!(should_show_input_hint, "Input hint should show when disabled + on function");
prop_assert!(!should_show_tooltip, "Tooltip should not show when disabled");
}
if !state.enabled && state.current_function.is_none() {
prop_assert!(!should_show_input_hint, "Input hint should not show when disabled + no function");
prop_assert!(!should_show_tooltip, "Tooltip should not show when disabled");
}
if state.enabled && state.current_function.is_some() {
prop_assert!(!should_show_input_hint, "Input hint should not show when enabled");
prop_assert!(should_show_tooltip, "Tooltip should show when enabled + on function");
}
if state.enabled && state.current_function.is_none() {
prop_assert!(!should_show_input_hint, "Input hint should not show when enabled");
prop_assert!(!should_show_tooltip, "Tooltip should not show when no function");
}
}
}
use crate::test_utils::test_helpers::test_app;
#[test]
fn test_update_tooltip_detects_function() {
let json = r#"{"name": "test"}"#;
let mut app = test_app(json);
app.input.textarea.insert_str("select(.name)");
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Head);
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Forward);
update_tooltip_from_app(&mut app);
assert_eq!(app.tooltip.current_function, Some("select".to_string()));
assert!(app.tooltip.should_show());
}
#[test]
fn test_update_tooltip_inside_function_parens() {
let json = r#"{"name": "test"}"#;
let mut app = test_app(json);
app.input.textarea.insert_str("select(.name)");
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Head);
for _ in 0..8 {
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Forward);
}
update_tooltip_from_app(&mut app);
assert_eq!(app.tooltip.current_function, Some("select".to_string()));
assert!(app.tooltip.should_show());
}
#[test]
fn test_update_tooltip_no_function() {
let json = r#"{"name": "test"}"#;
let mut app = test_app(json);
app.input.textarea.insert_str(".name");
update_tooltip_from_app(&mut app);
assert!(app.tooltip.current_function.is_none());
assert!(!app.tooltip.should_show());
}
#[test]
fn test_tooltip_disabled_does_not_show() {
let json = r#"{"name": "test"}"#;
let mut app = test_app(json);
app.input.textarea.insert_str("select(.name)");
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Head);
app.tooltip.toggle();
assert!(!app.tooltip.enabled);
update_tooltip_from_app(&mut app);
assert_eq!(app.tooltip.current_function, Some("select".to_string()));
assert!(!app.tooltip.should_show());
}
#[test]
fn test_update_tooltip_detects_operator_double_slash() {
let json = r#"{"name": "test"}"#;
let mut app = test_app(json);
app.input.textarea.insert_str(".name // \"default\"");
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Head);
for _ in 0..6 {
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Forward);
}
update_tooltip_from_app(&mut app);
assert!(app.tooltip.current_function.is_none());
assert_eq!(app.tooltip.current_operator, Some("//".to_string()));
assert!(app.tooltip.should_show());
}
#[test]
fn test_update_tooltip_detects_operator_pipe_equals() {
let json = r#"{"name": "test"}"#;
let mut app = test_app(json);
app.input.textarea.insert_str(".name |= ascii_upcase");
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Head);
for _ in 0..6 {
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Forward);
}
update_tooltip_from_app(&mut app);
assert!(app.tooltip.current_function.is_none());
assert_eq!(app.tooltip.current_operator, Some("|=".to_string()));
assert!(app.tooltip.should_show());
}
#[test]
fn test_update_tooltip_detects_operator_alternative_assignment() {
let json = r#"{"name": "test"}"#;
let mut app = test_app(json);
app.input.textarea.insert_str(".count //= 0");
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Head);
for _ in 0..7 {
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Forward);
}
update_tooltip_from_app(&mut app);
assert!(app.tooltip.current_function.is_none());
assert_eq!(app.tooltip.current_operator, Some("//=".to_string()));
assert!(app.tooltip.should_show());
}
#[test]
fn test_update_tooltip_detects_operator_recursive_descent() {
let json = r#"{"name": "test"}"#;
let mut app = test_app(json);
app.input.textarea.insert_str(".. | numbers");
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Head);
update_tooltip_from_app(&mut app);
assert!(app.tooltip.current_function.is_none());
assert_eq!(app.tooltip.current_operator, Some("..".to_string()));
assert!(app.tooltip.should_show());
}
#[test]
fn test_update_tooltip_no_operator_on_single_pipe() {
let json = r#"{"name": "test"}"#;
let mut app = test_app(json);
app.input.textarea.insert_str(".name | length");
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Head);
for _ in 0..6 {
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Forward);
}
update_tooltip_from_app(&mut app);
assert!(app.tooltip.current_function.is_none());
assert!(app.tooltip.current_operator.is_none());
assert!(!app.tooltip.should_show());
}
#[test]
fn test_update_tooltip_cursor_after_operator_detects_it() {
let json = r#"{"name": "test"}"#;
let mut app = test_app(json);
app.input.textarea.insert_str("//");
let cursor_pos = app.input.textarea.cursor().1;
assert_eq!(
cursor_pos, 2,
"Cursor should be at position 2 after typing //"
);
update_tooltip_from_app(&mut app);
assert_eq!(app.tooltip.current_operator, Some("//".to_string()));
assert!(app.tooltip.should_show());
}
#[test]
fn test_update_tooltip_cursor_on_operator_detects_it() {
let json = r#"{"name": "test"}"#;
let mut app = test_app(json);
app.input.textarea.insert_str("//");
app.input
.textarea
.move_cursor(tui_textarea::CursorMove::Back);
update_tooltip_from_app(&mut app);
assert_eq!(app.tooltip.current_operator, Some("//".to_string()));
assert!(app.tooltip.should_show());
}
#[test]
fn test_update_tooltip_cursor_after_pipe_equals() {
let json = r#"{"name": "test"}"#;
let mut app = test_app(json);
app.input.textarea.insert_str("|=");
update_tooltip_from_app(&mut app);
assert_eq!(app.tooltip.current_operator, Some("|=".to_string()));
assert!(app.tooltip.should_show());
}
#[test]
fn test_update_tooltip_cursor_after_alternative_assignment() {
let json = r#"{"name": "test"}"#;
let mut app = test_app(json);
app.input.textarea.insert_str("//=");
update_tooltip_from_app(&mut app);
assert_eq!(app.tooltip.current_operator, Some("//=".to_string()));
assert!(app.tooltip.should_show());
}
#[test]
fn test_update_tooltip_cursor_after_double_dot() {
let json = r#"{"name": "test"}"#;
let mut app = test_app(json);
app.input.textarea.insert_str("..");
update_tooltip_from_app(&mut app);
assert_eq!(app.tooltip.current_operator, Some("..".to_string()));
assert!(app.tooltip.should_show());
}
mod app_property_tests {
use super::*;
use crate::autocomplete::jq_functions::JQ_FUNCTION_METADATA;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_tooltip_visibility_on_function_name(
func_index in 0usize..JQ_FUNCTION_METADATA.len(),
prefix in "[.| ]{0,3}",
suffix in "[()| ]{0,5}"
) {
let func = &JQ_FUNCTION_METADATA[func_index];
let func_name = func.name;
let query = format!("{}{}{}", prefix, func_name, suffix);
let func_start = prefix.len();
let json = r#"{"test": true}"#;
let mut app = test_app(json);
app.input.textarea.insert_str(&query);
app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
for _ in 0..func_start {
app.input.textarea.move_cursor(tui_textarea::CursorMove::Forward);
}
update_tooltip_from_app(&mut app);
prop_assert!(
app.tooltip.should_show(),
"Tooltip should show when cursor is on function '{}' in query '{}'",
func_name,
query
);
prop_assert_eq!(
app.tooltip.current_function.as_deref(),
Some(func_name),
"Current function should be '{}' when cursor is on it",
func_name
);
}
#[test]
fn prop_tooltip_visibility_inside_function_parens(
func_index in 0usize..JQ_FUNCTION_METADATA.len(),
inner_content in "[.a-z0-9]{1,8}"
) {
let func = &JQ_FUNCTION_METADATA[func_index];
if !func.needs_parens {
return Ok(());
}
let func_name = func.name;
let query = format!("{}({})", func_name, inner_content);
let content_start = func_name.len() + 1;
let json = r#"{"test": true}"#;
let mut app = test_app(json);
app.input.textarea.insert_str(&query);
app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
for _ in 0..content_start {
app.input.textarea.move_cursor(tui_textarea::CursorMove::Forward);
}
update_tooltip_from_app(&mut app);
prop_assert!(
app.tooltip.should_show(),
"Tooltip should show when cursor is inside '{}' parens in query '{}'",
func_name,
query
);
prop_assert_eq!(
app.tooltip.current_function.as_deref(),
Some(func_name),
"Current function should be '{}' when cursor is inside its parens",
func_name
);
}
#[test]
fn prop_tooltip_not_visible_outside_function_context(
field_name in "[a-z]{1,8}"
) {
if JQ_FUNCTION_METADATA.iter().any(|f| f.name == field_name) {
return Ok(());
}
let query = format!(".{}", field_name);
let json = r#"{"test": true}"#;
let mut app = test_app(json);
app.input.textarea.insert_str(&query);
update_tooltip_from_app(&mut app);
prop_assert!(
!app.tooltip.should_show(),
"Tooltip should not show when cursor is outside function context in query '{}'",
query
);
prop_assert!(
app.tooltip.current_function.is_none(),
"Current function should be None when cursor is outside function context"
);
}
#[test]
fn prop_tooltip_disabled_never_shows(
func_index in 0usize..JQ_FUNCTION_METADATA.len()
) {
let func = &JQ_FUNCTION_METADATA[func_index];
let func_name = func.name;
let query = format!("{}(.x)", func_name);
let json = r#"{"test": true}"#;
let mut app = test_app(json);
app.input.textarea.insert_str(&query);
app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
app.tooltip.toggle();
update_tooltip_from_app(&mut app);
prop_assert!(
!app.tooltip.should_show(),
"Tooltip should not show when disabled, even on function '{}'",
func_name
);
prop_assert_eq!(
app.tooltip.current_function.as_deref(),
Some(func_name),
"Current function should still be detected when disabled"
);
}
#[test]
fn prop_function_priority_over_operator(
func_index in 0usize..JQ_FUNCTION_METADATA.len(),
op_index in 0usize..4usize,
left_operand in "[.a-z]{1,5}",
right_operand in "[a-z0-9\"]{1,5}"
) {
let func = &JQ_FUNCTION_METADATA[func_index];
if !func.needs_parens {
return Ok(());
}
let func_name = func.name;
let operators = ["//", "|=", "//=", ".."];
let op = operators[op_index];
let query = format!("{}({} {} {})", func_name, left_operand, op, right_operand);
let op_start = func_name.len() + 1 + left_operand.len() + 1;
let json = r#"{"test": true}"#;
let mut app = test_app(json);
app.input.textarea.insert_str(&query);
app.input.textarea.move_cursor(tui_textarea::CursorMove::Head);
for _ in 0..op_start {
app.input.textarea.move_cursor(tui_textarea::CursorMove::Forward);
}
update_tooltip_from_app(&mut app);
prop_assert_eq!(
app.tooltip.current_function.as_deref(),
Some(func_name),
"Function '{}' should be detected (priority over operator '{}') in query '{}'",
func_name,
op,
query
);
prop_assert!(
app.tooltip.current_operator.is_none(),
"Operator '{}' should NOT be detected when inside function '{}' parens in query '{}'",
op,
func_name,
query
);
prop_assert!(
app.tooltip.should_show(),
"Tooltip should show for function '{}' in query '{}'",
func_name,
query
);
}
}
}