fix(tui): replace single-line Input with multi-line TextArea in PromptInput #10753
@@ -0,0 +1,95 @@
|
||||
"""Step definitions for tdd_prompt_input_textarea.feature.
|
||||
|
||||
Tests for:
|
||||
- PromptInput inherits from TextArea (not Input)
|
||||
- PromptInput.consume_text() returns PromptSubmitted with correct text
|
||||
- PromptInput text is cleared after consume_text()
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from behave import then, when
|
||||
from behave.runner import Context
|
||||
|
||||
import cleveragents.tui.widgets.prompt as _prompt_mod
|
||||
from cleveragents.tui.widgets.prompt import PromptInput, PromptSubmitted
|
||||
|
||||
|
||||
@when("I inspect the PromptInput base class")
|
||||
def step_inspect_prompt_input_base(context: Context) -> None:
|
||||
"""Load the PromptInput class for inspection."""
|
||||
context.prompt_input_cls = PromptInput
|
||||
|
||||
|
||||
@then("PromptInput should be a subclass of TextArea")
|
||||
def step_prompt_input_is_textarea(context: Context) -> None:
|
||||
"""Verify PromptInput inherits from textual.widgets.TextArea."""
|
||||
try:
|
||||
import textual.widgets as tw
|
||||
|
||||
assert issubclass(context.prompt_input_cls, tw.TextArea), (
|
||||
f"PromptInput must inherit from TextArea, "
|
||||
f"but its MRO is: {[c.__name__ for c in context.prompt_input_cls.__mro__]}"
|
||||
)
|
||||
except ImportError: # pragma: no cover
|
||||
# Textual not installed — check the fallback base name
|
||||
base_name = context.prompt_input_cls.__bases__[0].__name__
|
||||
assert "TextArea" in base_name, (
|
||||
f"PromptInput fallback base must be TextArea-like, got: {base_name}"
|
||||
)
|
||||
|
||||
|
||||
@then("PromptInput should not be a subclass of Input")
|
||||
def step_prompt_input_not_input(context: Context) -> None:
|
||||
"""Verify PromptInput does NOT inherit from textual.widgets.Input."""
|
||||
try:
|
||||
import textual.widgets as tw
|
||||
|
||||
assert not issubclass(context.prompt_input_cls, tw.Input), (
|
||||
"PromptInput must NOT inherit from Input (single-line widget). "
|
||||
"Use TextArea for multi-line support."
|
||||
)
|
||||
except ImportError: # pragma: no cover
|
||||
# Textual not installed — check the fallback base name
|
||||
base_name = context.prompt_input_cls.__bases__[0].__name__
|
||||
assert "Input" not in base_name or "TextArea" in base_name, (
|
||||
f"PromptInput fallback base must not be Input-like, got: {base_name}"
|
||||
)
|
||||
|
||||
|
||||
@when("I create a PromptInput instance")
|
||||
def step_create_prompt_input(context: Context) -> None:
|
||||
"""Create a PromptInput instance using the fallback (no Textual needed)."""
|
||||
# Create a PromptInput instance directly
|
||||
# The PromptInput class is already loaded with the appropriate base class
|
||||
context.prompt_input = _prompt_mod.PromptInput()
|
||||
context.prompt_submitted: PromptSubmitted | None = None
|
||||
|
||||
|
||||
@when('I set the PromptInput text to "{text}"')
|
||||
def step_set_prompt_input_text(context: Context, text: str) -> None:
|
||||
"""Set the text on the PromptInput instance."""
|
||||
context.prompt_input.text = text
|
||||
|
||||
|
||||
@when("I call consume_text on the PromptInput")
|
||||
def step_call_consume_text(context: Context) -> None:
|
||||
"""Call consume_text() and store the result."""
|
||||
context.prompt_submitted = context.prompt_input.consume_text()
|
||||
|
||||
|
||||
@then('the PromptSubmitted text should be "{expected}"')
|
||||
def step_prompt_submitted_text(context: Context, expected: str) -> None:
|
||||
"""Verify the PromptSubmitted payload has the expected text."""
|
||||
assert context.prompt_submitted is not None, "consume_text() returned None"
|
||||
assert context.prompt_submitted.text == expected, (
|
||||
f"Expected text '{expected}', got '{context.prompt_submitted.text}'"
|
||||
)
|
||||
|
||||
|
||||
@then("the PromptInput text should be empty after consume")
|
||||
def step_prompt_input_empty_after_consume(context: Context) -> None:
|
||||
"""Verify the PromptInput text is cleared after consume_text()."""
|
||||
assert context.prompt_input.text == "", (
|
||||
f"Expected empty text after consume_text(), got '{context.prompt_input.text}'"
|
||||
)
|
||||
@@ -82,20 +82,21 @@ def _build_mock_textual():
|
||||
def update(self, text):
|
||||
self._text = text
|
||||
|
||||
class MockInput:
|
||||
"""Minimal Input stand-in for the Textual base class."""
|
||||
class MockTextArea:
|
||||
class MockTextArea:
|
||||
"""Minimal TextArea stand-in for the Textual base class."""
|
||||
|
||||
value = ""
|
||||
text = ""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.value = ""
|
||||
self.text = ""
|
||||
|
||||
mock_textual_app.App = MockApp
|
||||
mock_textual_containers.Vertical = MockVertical
|
||||
mock_textual_widgets.Header = MockHeader
|
||||
mock_textual_widgets.Footer = MockFooter
|
||||
mock_textual_widgets.Static = MockStatic
|
||||
mock_textual_widgets.Input = MockInput
|
||||
mock_textual_widgets.TextArea = MockTextArea
|
||||
|
||||
return {
|
||||
"textual": mock_textual,
|
||||
@@ -114,7 +115,7 @@ def _install_mock_textual(context):
|
||||
for key, mod in mocks.items():
|
||||
sys.modules[key] = mod
|
||||
|
||||
# Reload widget modules so they pick up the mock Static/Input base class
|
||||
# Reload widget modules so they pick up the mock Static/TextArea base class
|
||||
import cleveragents.tui.widgets.help_panel_overlay as hp_mod
|
||||
import cleveragents.tui.widgets.persona_bar as pb_mod
|
||||
import cleveragents.tui.widgets.prompt as prompt_mod
|
||||
@@ -142,7 +143,7 @@ def _restore_modules(context):
|
||||
else:
|
||||
sys.modules[key] = val
|
||||
|
||||
# Reload widget modules so they pick up the real Static/Input base class again
|
||||
# Reload widget modules so they pick up the real Static/TextArea base class again
|
||||
import cleveragents.tui.widgets.help_panel_overlay as hp_mod
|
||||
import cleveragents.tui.widgets.persona_bar as pb_mod
|
||||
import cleveragents.tui.widgets.prompt as prompt_mod
|
||||
@@ -370,7 +371,7 @@ def step_set_prompt_text(context, text):
|
||||
from cleveragents.tui.widgets.prompt import PromptInput
|
||||
|
||||
prompt = context._tui_app.query_one("#prompt", PromptInput)
|
||||
prompt.value = text
|
||||
prompt.text = text
|
||||
|
||||
|
||||
@then('the conversation widget should contain "{text}"')
|
||||
@@ -429,7 +430,7 @@ def _submit_text(context, text):
|
||||
from cleveragents.tui.widgets.prompt import PromptInput
|
||||
|
||||
prompt = context._tui_app.query_one("#prompt", PromptInput)
|
||||
prompt.value = text
|
||||
prompt.text = text
|
||||
event = SimpleNamespace()
|
||||
context._tui_app.on_input_submitted(event)
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
@tdd_issue @tdd_issue_10411
|
||||
Feature: TDD Issue #10411 - PromptInput uses single-line Input instead of multi-line TextArea
|
||||
As a CleverAgents TUI user
|
||||
I want the PromptInput widget to use TextArea (multi-line)
|
||||
So that I can enter multi-line prompts in the TUI
|
||||
|
||||
This test captures the bug described in issue #10411.
|
||||
PromptInput was inheriting from textual.widgets.Input (single-line)
|
||||
instead of textual.widgets.TextArea (multi-line).
|
||||
|
||||
@tdd_issue @tdd_issue_10411
|
||||
Scenario: PromptInput inherits from TextArea not Input
|
||||
When I inspect the PromptInput base class
|
||||
Then PromptInput should be a subclass of TextArea
|
||||
And PromptInput should not be a subclass of Input
|
||||
|
||||
@tdd_issue @tdd_issue_10411
|
||||
Scenario: PromptInput consume_text returns PromptSubmitted with text
|
||||
When I create a PromptInput instance
|
||||
And I set the PromptInput text to "hello world"
|
||||
And I call consume_text on the PromptInput
|
||||
Then the PromptSubmitted text should be "hello world"
|
||||
And the PromptInput text should be empty after consume
|
||||
@@ -145,7 +145,7 @@ if _TEXTUAL_AVAILABLE:
|
||||
def action_help(self) -> None:
|
||||
prompt = self.query_one("#prompt", PromptInput)
|
||||
help_panel = self.query_one("#help-panel", HelpPanelOverlay)
|
||||
context_name = resolve_help_context(prompt.value)
|
||||
context_name = resolve_help_context(prompt.text)
|
||||
help_panel.toggle(context_name)
|
||||
|
||||
def action_cycle_preset(self) -> None:
|
||||
|
||||
@@ -7,21 +7,21 @@ from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _load_input_base() -> type[Any]:
|
||||
def _load_textarea_base() -> type[Any]:
|
||||
try:
|
||||
return importlib.import_module("textual.widgets").Input
|
||||
return importlib.import_module("textual.widgets").TextArea
|
||||
except Exception: # pragma: no cover
|
||||
|
||||
class _FallbackInput:
|
||||
value = ""
|
||||
class _FallbackTextArea:
|
||||
text = ""
|
||||
|
||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||
self.value = ""
|
||||
self.text = ""
|
||||
|
||||
return _FallbackInput
|
||||
return _FallbackTextArea
|
||||
|
||||
|
||||
_InputBase = _load_input_base()
|
||||
_TextAreaBase = _load_textarea_base()
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
@@ -31,10 +31,10 @@ class PromptSubmitted:
|
||||
text: str
|
||||
|
||||
|
||||
class PromptInput(_InputBase):
|
||||
"""Input widget wrapper with helper methods."""
|
||||
class PromptInput(_TextAreaBase):
|
||||
"""TextArea widget wrapper with helper methods."""
|
||||
|
||||
def consume_text(self) -> PromptSubmitted:
|
||||
text = self.value
|
||||
self.value = ""
|
||||
text = self.text
|
||||
self.text = ""
|
||||
return PromptSubmitted(text=text)
|
||||
|
||||
Reference in New Issue
Block a user