Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/textual/widgets/_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,12 @@ def cursor_screen_offset(self) -> Offset:
scroll_x, _ = self.scroll_offset
return Offset(x + self._cursor_offset - scroll_x, y)

def validate_value(self, value: str) -> str:
"""Truncate value to max_length when set programmatically."""
if self.max_length and len(value) > self.max_length:
return value[: self.max_length]
return value

def _watch_value(self, value: str) -> None:
"""Update the virtual size and suggestion when the value changes."""
self.virtual_size = Size(self.content_width, 1)
Expand Down
71 changes: 71 additions & 0 deletions tests/test_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Tests for Input.max_length enforcement on programmatic value assignment."""
from __future__ import annotations

import pytest

from textual.app import App, ComposeResult
from textual.widgets import Input


class InputApp(App[None]):
CSS = "Input { height: auto; }"

def __init__(self, max_length: int = 0, initial: str = "") -> None:
super().__init__()
self._max_length = max_length
self._initial = initial

def compose(self) -> ComposeResult:
yield Input(
self._initial if self._initial else None,
max_length=self._max_length,
)


async def test_programmatic_value_truncated_when_exceeds_max_length():
"""Assigning a value longer than max_length truncates to max_length characters."""
async with InputApp(max_length=5).run_test() as pilot:
widget = pilot.app.query_one(Input)
widget.value = "hello world"
assert widget.value == "hello"


async def test_programmatic_value_preserved_when_within_max_length():
"""Assigning a value within max_length stores it unchanged."""
async with InputApp(max_length=10).run_test() as pilot:
widget = pilot.app.query_one(Input)
widget.value = "hello"
assert widget.value == "hello"


async def test_programmatic_value_preserved_when_equal_to_max_length():
"""Assigning a value exactly equal to max_length stores it unchanged."""
async with InputApp(max_length=5).run_test() as pilot:
widget = pilot.app.query_one(Input)
widget.value = "hello"
assert widget.value == "hello"


async def test_initial_value_truncated_when_exceeds_max_length():
"""An initial value longer than max_length is truncated at construction."""
async with InputApp(max_length=3, initial="toolong").run_test() as pilot:
widget = pilot.app.query_one(Input)
assert widget.value == "too"


async def test_no_max_length_preserves_any_value():
"""When max_length is 0 (no limit), long strings are stored intact."""
async with InputApp(max_length=0).run_test() as pilot:
widget = pilot.app.query_one(Input)
long_value = "a" * 1000
widget.value = long_value
assert widget.value == long_value


async def test_keyboard_input_still_enforces_max_length():
"""Typing characters beyond max_length is still rejected via keyboard."""
async with InputApp(max_length=3).run_test() as pilot:
await pilot.click(Input)
await pilot.press("a", "b", "c", "d", "e")
widget = pilot.app.query_one(Input)
assert len(widget.value) <= 3