Co-authored-by: Cyprien <courtot.c@gmail.com> Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com> Co-authored-by: Laure Hugo <201583486+laure0303@users.noreply.github.com> Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@users.noreply.github.com> Co-authored-by: Peter Evers <peter.evers@mistral.ai> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Yousria <yousria.debaud@mistral.ai> Co-authored-by: renovate-mistral[bot] <253709520+renovate-mistral[bot]@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
781 lines
25 KiB
Python
781 lines
25 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from vibe.core.tools.builtins.ask_user_question import (
|
|
AskUserQuestionArgs,
|
|
Choice,
|
|
Question,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def single_question_args():
|
|
return AskUserQuestionArgs(
|
|
questions=[
|
|
Question(
|
|
question="Which database?",
|
|
header="DB",
|
|
options=[
|
|
Choice(label="PostgreSQL", description="Relational DB"),
|
|
Choice(label="MongoDB", description="Document DB"),
|
|
],
|
|
)
|
|
]
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def multi_question_args():
|
|
return AskUserQuestionArgs(
|
|
questions=[
|
|
Question(
|
|
question="Which database?",
|
|
header="DB",
|
|
options=[Choice(label="PostgreSQL"), Choice(label="MongoDB")],
|
|
),
|
|
Question(
|
|
question="Which framework?",
|
|
header="Framework",
|
|
options=[Choice(label="FastAPI"), Choice(label="Django")],
|
|
),
|
|
]
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def multi_select_args():
|
|
return AskUserQuestionArgs(
|
|
questions=[
|
|
Question(
|
|
question="Which features?",
|
|
header="Features",
|
|
options=[
|
|
Choice(label="Auth"),
|
|
Choice(label="Caching"),
|
|
Choice(label="Logging"),
|
|
],
|
|
multi_select=True,
|
|
)
|
|
]
|
|
)
|
|
|
|
|
|
class TestQuestionAppState:
|
|
def test_init_state(self, single_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
|
|
assert app.current_question_idx == 0
|
|
assert app.selected_option == 0
|
|
assert len(app.answers) == 0
|
|
assert len(app.other_texts) == 0
|
|
|
|
def test_total_options_single_select(self, single_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
|
|
# 2 options + Other = 3 (no Submit for single-select)
|
|
assert app._total_options == 3
|
|
|
|
def test_total_options_multi_select_includes_submit(self, multi_select_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
|
|
# 3 options + Other + Submit = 5
|
|
assert app._total_options == 5
|
|
assert app._other_option_idx == 3
|
|
assert app._submit_option_idx == 4
|
|
|
|
def test_is_other_selected(self, single_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
|
|
assert app._is_other_selected is False
|
|
app.selected_option = 2 # Other option
|
|
assert app._is_other_selected is True
|
|
|
|
def test_is_submit_selected(self, multi_select_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
|
|
assert app._is_submit_selected is False
|
|
app.selected_option = 4 # Submit option
|
|
assert app._is_submit_selected is True
|
|
|
|
def test_is_submit_selected_false_for_single_select(self, single_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
|
|
# Even if selected_option is 3, is_submit_selected is False for single-select
|
|
app.selected_option = 3
|
|
assert app._is_submit_selected is False
|
|
|
|
def test_store_other_text_per_question(self, multi_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_question_args)
|
|
|
|
# Store text for question 0
|
|
app.other_texts[0] = "Custom DB"
|
|
|
|
# Switch to question 1
|
|
app.current_question_idx = 1
|
|
app.other_texts[1] = "Custom Framework"
|
|
|
|
# Verify both stored separately
|
|
assert app._get_other_text(0) == "Custom DB"
|
|
assert app._get_other_text(1) == "Custom Framework"
|
|
|
|
def test_save_regular_option_answer(self, single_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
app.selected_option = 0 # PostgreSQL
|
|
|
|
app._save_current_answer()
|
|
|
|
assert 0 in app.answers
|
|
answer_text, is_other = app.answers[0]
|
|
assert answer_text == "PostgreSQL"
|
|
assert is_other is False
|
|
|
|
def test_save_other_option_answer(self, single_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
app.selected_option = 2 # Other
|
|
app.other_texts[0] = "SQLite"
|
|
|
|
app._save_current_answer()
|
|
|
|
assert 0 in app.answers
|
|
answer_text, is_other = app.answers[0]
|
|
assert answer_text == "SQLite"
|
|
assert is_other is True
|
|
|
|
def test_save_other_option_empty_does_not_save(self, single_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
app.selected_option = 2 # Other
|
|
app.other_texts[0] = "" # Empty
|
|
|
|
app._save_current_answer()
|
|
|
|
assert 0 not in app.answers
|
|
|
|
def test_all_answered_false_initially(self, multi_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_question_args)
|
|
|
|
assert app._all_answered() is False
|
|
|
|
def test_all_answered_true_when_complete(self, multi_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_question_args)
|
|
app.answers[0] = ("PostgreSQL", False)
|
|
app.answers[1] = ("FastAPI", False)
|
|
|
|
assert app._all_answered() is True
|
|
|
|
def test_multi_select_toggle(self, multi_select_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
|
|
# Initially no selections
|
|
assert len(app.multi_selections.get(0, set())) == 0
|
|
|
|
# Add selection
|
|
app.multi_selections.setdefault(0, set()).add(0)
|
|
assert 0 in app.multi_selections[0]
|
|
|
|
# Add another
|
|
app.multi_selections[0].add(2)
|
|
assert 2 in app.multi_selections[0]
|
|
|
|
# Remove first
|
|
app.multi_selections[0].discard(0)
|
|
assert 0 not in app.multi_selections[0]
|
|
assert 2 in app.multi_selections[0]
|
|
|
|
def test_multi_select_save_answer(self, multi_select_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
app.multi_selections[0] = {0, 2} # Auth and Logging
|
|
|
|
app._save_current_answer()
|
|
|
|
assert 0 in app.answers
|
|
answer_text, is_other = app.answers[0]
|
|
assert "Auth" in answer_text
|
|
assert "Logging" in answer_text
|
|
assert is_other is False
|
|
|
|
def test_multi_select_with_other(self, multi_select_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
app.multi_selections[0] = {0, 3} # Auth and Other
|
|
app.other_texts[0] = "Custom Feature"
|
|
|
|
app._save_current_answer()
|
|
|
|
assert 0 in app.answers
|
|
answer_text, is_other = app.answers[0]
|
|
assert "Auth" in answer_text
|
|
assert "Custom Feature" in answer_text
|
|
assert is_other is True
|
|
|
|
|
|
class TestQuestionAppActions:
|
|
def test_action_move_down(self, single_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
assert app.selected_option == 0
|
|
|
|
app.action_move_down()
|
|
assert app.selected_option == 1
|
|
|
|
app.action_move_down()
|
|
assert app.selected_option == 2 # Other
|
|
|
|
app.action_move_down()
|
|
assert app.selected_option == 0 # Wraps around
|
|
|
|
def test_action_move_up(self, single_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
assert app.selected_option == 0
|
|
|
|
app.action_move_up()
|
|
assert app.selected_option == 2 # Wraps to Other
|
|
|
|
app.action_move_up()
|
|
assert app.selected_option == 1
|
|
|
|
def test_switch_question_preserves_other_text(self, multi_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_question_args)
|
|
app.other_texts[0] = "Text for Q1"
|
|
|
|
app._switch_question(1)
|
|
|
|
assert app.current_question_idx == 1
|
|
assert app._get_other_text(0) == "Text for Q1"
|
|
assert app._get_other_text(1) == ""
|
|
|
|
def test_switch_question_restores_cursor_to_answered_option(
|
|
self, multi_question_args
|
|
):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_question_args)
|
|
# Answer question 0 with MongoDB (index 1)
|
|
app.answers[0] = ("MongoDB", False)
|
|
|
|
# Switch away then back
|
|
app._switch_question(1)
|
|
app._switch_question(0)
|
|
|
|
assert app.selected_option == 1
|
|
|
|
def test_switch_question_restores_cursor_to_other(self, multi_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_question_args)
|
|
# Answer question 0 with Other
|
|
app.answers[0] = ("Custom DB", True)
|
|
|
|
app._switch_question(1)
|
|
app._switch_question(0)
|
|
|
|
assert app.selected_option == len(app.questions[0].options)
|
|
assert app._is_other_selected
|
|
|
|
def test_switch_question_defaults_to_zero_if_unanswered(self, multi_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_question_args)
|
|
|
|
app._switch_question(1)
|
|
assert app.selected_option == 0
|
|
|
|
def test_switch_question_restores_cursor_multi_select(self):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
args = AskUserQuestionArgs(
|
|
questions=[
|
|
Question(
|
|
question="Q1?", options=[Choice(label="A"), Choice(label="B")]
|
|
),
|
|
Question(
|
|
question="Q2?",
|
|
options=[Choice(label="X"), Choice(label="Y"), Choice(label="Z")],
|
|
multi_select=True,
|
|
),
|
|
]
|
|
)
|
|
app = QuestionApp(args)
|
|
# Select Y (1) and Z (2) for question 1
|
|
app.multi_selections[1] = {1, 2}
|
|
|
|
app._switch_question(1)
|
|
|
|
assert app.selected_option == 1 # min of {1, 2}
|
|
|
|
|
|
class TestMultiSelectOtherBehavior:
|
|
def test_multi_select_other_does_not_advance_on_save(self, multi_select_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
app.selected_option = 3 # Other option (3 options + Other)
|
|
app.other_texts[0] = "Custom feature"
|
|
|
|
# Save should not advance for multi-select
|
|
app._save_current_answer()
|
|
|
|
# Should stay on same question
|
|
assert app.current_question_idx == 0
|
|
|
|
def test_multi_select_other_toggle_adds_to_selections(self, multi_select_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
other_idx = len(app._current_question.options) # 3
|
|
|
|
# Initially no selections
|
|
assert len(app.multi_selections.get(0, set())) == 0
|
|
|
|
# Add Other to selections
|
|
app.multi_selections.setdefault(0, set()).add(other_idx)
|
|
app.other_texts[0] = "Custom"
|
|
|
|
# Can still add regular options
|
|
app.multi_selections[0].add(0) # Auth
|
|
app.multi_selections[0].add(1) # Caching
|
|
|
|
assert other_idx in app.multi_selections[0]
|
|
assert 0 in app.multi_selections[0]
|
|
assert 1 in app.multi_selections[0]
|
|
|
|
def test_multi_select_save_with_other_and_regular_options(self, multi_select_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
other_idx = len(app._current_question.options)
|
|
|
|
# Select Auth (0), Logging (2), and Other (3)
|
|
app.multi_selections[0] = {0, 2, other_idx}
|
|
app.other_texts[0] = "Custom Feature"
|
|
|
|
app._save_current_answer()
|
|
|
|
assert 0 in app.answers
|
|
answer_text, is_other = app.answers[0]
|
|
assert "Auth" in answer_text
|
|
assert "Logging" in answer_text
|
|
assert "Custom Feature" in answer_text
|
|
assert is_other is True
|
|
|
|
def test_multi_select_other_without_text_not_in_answer(self, multi_select_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
other_idx = len(app._current_question.options)
|
|
|
|
# Select Auth (0) and Other (3) but no text for Other
|
|
app.multi_selections[0] = {0, other_idx}
|
|
app.other_texts[0] = ""
|
|
|
|
app._save_current_answer()
|
|
|
|
assert 0 in app.answers
|
|
answer_text, is_other = app.answers[0]
|
|
assert "Auth" in answer_text
|
|
# Empty Other should not appear
|
|
assert is_other is False # No valid Other text
|
|
|
|
def test_multi_select_can_toggle_after_selecting_other(self, multi_select_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
other_idx = len(app._current_question.options)
|
|
|
|
# Select Other first
|
|
app.multi_selections[0] = {other_idx}
|
|
app.other_texts[0] = "Custom"
|
|
|
|
# Now navigate to and toggle Auth
|
|
app.selected_option = 0
|
|
selections = app.multi_selections.setdefault(0, set())
|
|
selections.add(0) # Toggle Auth on
|
|
|
|
assert 0 in app.multi_selections[0]
|
|
assert other_idx in app.multi_selections[0]
|
|
|
|
# Toggle Auth off
|
|
selections.discard(0)
|
|
assert 0 not in app.multi_selections[0]
|
|
assert other_idx in app.multi_selections[0]
|
|
|
|
def test_multi_select_empty_selections_does_not_save(self, multi_select_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
|
|
# No selections
|
|
app._save_current_answer()
|
|
|
|
assert 0 not in app.answers
|
|
|
|
|
|
class TestSingleSelectOtherBehavior:
|
|
def test_single_select_other_with_text_saves(self, single_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
app.selected_option = 2 # Other
|
|
app.other_texts[0] = "Custom DB"
|
|
|
|
app._save_current_answer()
|
|
|
|
assert 0 in app.answers
|
|
answer_text, is_other = app.answers[0]
|
|
assert answer_text == "Custom DB"
|
|
assert is_other is True
|
|
|
|
def test_single_select_other_without_text_does_not_save(self, single_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
app.selected_option = 2 # Other
|
|
app.other_texts[0] = ""
|
|
|
|
app._save_current_answer()
|
|
|
|
assert 0 not in app.answers
|
|
|
|
def test_single_select_regular_option_saves_immediately(self, single_question_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
app.selected_option = 1 # MongoDB
|
|
|
|
app._save_current_answer()
|
|
|
|
assert 0 in app.answers
|
|
answer_text, is_other = app.answers[0]
|
|
assert answer_text == "MongoDB"
|
|
assert is_other is False
|
|
|
|
|
|
class TestMultiSelectAutoSelect:
|
|
def test_typing_auto_selects_other(self, multi_select_args):
|
|
from unittest.mock import MagicMock
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
app.other_input = MagicMock()
|
|
app.other_input.value = "Custom text"
|
|
|
|
# Initially no selections
|
|
assert app._other_option_idx not in app.multi_selections.get(0, set())
|
|
|
|
# Simulate input change
|
|
from textual.widgets import Input
|
|
|
|
app.on_input_changed(Input.Changed(app.other_input, "Custom text"))
|
|
|
|
# Other should now be selected
|
|
assert app._other_option_idx in app.multi_selections[0]
|
|
|
|
def test_clearing_auto_deselects_other(self, multi_select_args):
|
|
from unittest.mock import MagicMock
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
app.other_input = MagicMock()
|
|
|
|
# Start with Other selected and text
|
|
app.multi_selections[0] = {app._other_option_idx}
|
|
app.other_input.value = "" # Cleared
|
|
|
|
# Simulate input change with empty value
|
|
from textual.widgets import Input
|
|
|
|
app.on_input_changed(Input.Changed(app.other_input, ""))
|
|
|
|
# Other should now be deselected
|
|
assert app._other_option_idx not in app.multi_selections[0]
|
|
|
|
def test_auto_select_preserves_other_selections(self, multi_select_args):
|
|
from unittest.mock import MagicMock
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
app.other_input = MagicMock()
|
|
app.other_input.value = "Custom"
|
|
|
|
# Pre-select Auth and Logging
|
|
app.multi_selections[0] = {0, 2}
|
|
|
|
# Simulate typing
|
|
from textual.widgets import Input
|
|
|
|
app.on_input_changed(Input.Changed(app.other_input, "Custom"))
|
|
|
|
# All selections should be preserved plus Other
|
|
assert 0 in app.multi_selections[0]
|
|
assert 2 in app.multi_selections[0]
|
|
assert app._other_option_idx in app.multi_selections[0]
|
|
|
|
|
|
class TestNumberKeyShortcuts:
|
|
def test_number_key_selects_predefined_option(self, single_question_args):
|
|
from unittest.mock import patch
|
|
|
|
from textual import events
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
app.selected_option = 0
|
|
|
|
# Press "2" -> should select MongoDB (index 1) and save
|
|
event = events.Key("2", "2")
|
|
with patch.object(app, "_advance_or_submit"):
|
|
app._handle_number_key(event)
|
|
|
|
assert app.selected_option == 1
|
|
assert 0 in app.answers
|
|
answer_text, is_other = app.answers[0]
|
|
assert answer_text == "MongoDB"
|
|
assert is_other is False
|
|
|
|
def test_number_key_first_option(self, single_question_args):
|
|
from unittest.mock import patch
|
|
|
|
from textual import events
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
|
|
event = events.Key("1", "1")
|
|
with patch.object(app, "_advance_or_submit"):
|
|
app._handle_number_key(event)
|
|
|
|
assert app.selected_option == 0
|
|
assert 0 in app.answers
|
|
answer_text, _ = app.answers[0]
|
|
assert answer_text == "PostgreSQL"
|
|
|
|
def test_number_key_other_empty_focuses(self, single_question_args):
|
|
from textual import events
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
# Other is option index 2 -> key "3"
|
|
event = events.Key("3", "3")
|
|
app._handle_number_key(event)
|
|
|
|
# Should navigate to Other but not validate (no text)
|
|
assert app.selected_option == 2
|
|
assert app._is_other_selected is True
|
|
assert 0 not in app.answers
|
|
|
|
def test_number_key_other_with_text_focuses_without_validating(
|
|
self, single_question_args
|
|
):
|
|
from unittest.mock import MagicMock
|
|
|
|
from textual import events
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
app.other_texts[0] = "SQLite"
|
|
app.other_input = MagicMock()
|
|
app.other_input.has_focus = False
|
|
|
|
event = events.Key("3", "3")
|
|
app._handle_number_key(event)
|
|
|
|
# Should navigate to Other but not validate — just focus the input
|
|
assert app.selected_option == 2
|
|
assert app._is_other_selected is True
|
|
assert 0 not in app.answers
|
|
|
|
def test_number_key_out_of_range_ignored(self, single_question_args):
|
|
from textual import events
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
# 3 options total (2 + Other), so "4" is out of range
|
|
event = events.Key("4", "4")
|
|
app._handle_number_key(event)
|
|
|
|
assert app.selected_option == 0
|
|
assert 0 not in app.answers
|
|
|
|
def test_number_key_zero_ignored(self, single_question_args):
|
|
from textual import events
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
event = events.Key("0", "0")
|
|
app._handle_number_key(event)
|
|
|
|
assert app.selected_option == 0
|
|
assert 0 not in app.answers
|
|
|
|
def test_number_key_ignored_when_input_focused(self, single_question_args):
|
|
from unittest.mock import MagicMock
|
|
|
|
from textual import events
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
app.other_input = MagicMock()
|
|
app.other_input.has_focus = True
|
|
|
|
event = events.Key("1", "1")
|
|
app._handle_number_key(event)
|
|
|
|
assert app.selected_option == 0
|
|
assert 0 not in app.answers
|
|
|
|
def test_number_key_multi_select_toggles(self, multi_select_args):
|
|
from textual import events
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
|
|
# Press "1" in multi-select -> should toggle Auth
|
|
event = events.Key("1", "1")
|
|
app._handle_number_key(event)
|
|
|
|
assert 0 in app.multi_selections.get(0, set())
|
|
|
|
def test_number_key_multi_select_submit_ignored(self, multi_select_args):
|
|
from textual import events
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
# Submit is at index 4 -> key "5"
|
|
event = events.Key("5", "5")
|
|
app._handle_number_key(event)
|
|
|
|
# Should not navigate to submit
|
|
assert app.selected_option == 0
|
|
|
|
|
|
class TestMultiSelectSubmit:
|
|
def test_navigate_to_submit(self, multi_select_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
|
|
# Navigate down through all options to Submit
|
|
for _ in range(4): # 0->1->2->3(Other)->4(Submit)
|
|
app.action_move_down()
|
|
|
|
assert app.selected_option == 4
|
|
assert app._is_submit_selected is True
|
|
|
|
def test_submit_wraps_around(self, multi_select_args):
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(multi_select_args)
|
|
app.selected_option = 4 # Submit
|
|
|
|
app.action_move_down()
|
|
|
|
assert app.selected_option == 0
|
|
|
|
|
|
class TestVimKeybindings:
|
|
def test_j_moves_down(self, single_question_args):
|
|
from textual import events
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
assert app.selected_option == 0
|
|
|
|
app.on_key(events.Key("j", "j"))
|
|
|
|
assert app.selected_option == 1
|
|
|
|
def test_k_moves_up(self, single_question_args):
|
|
from textual import events
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
assert app.selected_option == 0
|
|
|
|
app.on_key(events.Key("k", "k"))
|
|
|
|
# Wraps to last option (Other = index 2)
|
|
assert app.selected_option == 2
|
|
|
|
def test_j_k_ignored_when_other_input_focused(self, single_question_args):
|
|
from unittest.mock import MagicMock
|
|
|
|
from textual import events
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
app = QuestionApp(single_question_args)
|
|
app.other_input = MagicMock()
|
|
app.other_input.has_focus = True
|
|
|
|
app.on_key(events.Key("j", "j"))
|
|
assert app.selected_option == 0
|
|
|
|
def test_j_moves_down_for_single_question(self):
|
|
# Vim navigation must fire before the len <= 1 early return in on_key.
|
|
from textual import events
|
|
|
|
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|
|
|
args = AskUserQuestionArgs(
|
|
questions=[
|
|
Question(
|
|
question="Only question?",
|
|
header="Q",
|
|
options=[Choice(label="A"), Choice(label="B")],
|
|
)
|
|
]
|
|
)
|
|
app = QuestionApp(args)
|
|
assert app.selected_option == 0
|
|
|
|
app.on_key(events.Key("j", "j"))
|
|
|
|
assert app.selected_option == 1
|