mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'main' into input-auto-completion
This commit is contained in:
79
tests/input/test_input_validation.py
Normal file
79
tests/input/test_input_validation.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from textual import on
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.validation import Number, ValidationResult
|
||||
from textual.widgets import Input
|
||||
|
||||
|
||||
class InputApp(App):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.messages = []
|
||||
self.validator = Number(minimum=1, maximum=5)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Input(
|
||||
validators=self.validator,
|
||||
)
|
||||
|
||||
@on(Input.Changed)
|
||||
@on(Input.Submitted)
|
||||
def on_changed_or_submitted(self, event):
|
||||
self.messages.append(event)
|
||||
|
||||
|
||||
async def test_input_changed_message_validation_failure():
|
||||
app = InputApp()
|
||||
async with app.run_test() as pilot:
|
||||
input = app.query_one(Input)
|
||||
input.value = "8"
|
||||
await pilot.pause()
|
||||
assert len(app.messages) == 1
|
||||
assert app.messages[0].validation_result == ValidationResult.failure(
|
||||
failures=[
|
||||
Number.NotInRange(
|
||||
value="8",
|
||||
validator=app.validator,
|
||||
description="Must be between 1 and 5.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
async def test_input_changed_message_validation_success():
|
||||
app = InputApp()
|
||||
async with app.run_test() as pilot:
|
||||
input = app.query_one(Input)
|
||||
input.value = "3"
|
||||
await pilot.pause()
|
||||
assert len(app.messages) == 1
|
||||
assert app.messages[0].validation_result == ValidationResult.success()
|
||||
|
||||
|
||||
async def test_input_submitted_message_validation_failure():
|
||||
app = InputApp()
|
||||
async with app.run_test() as pilot:
|
||||
input = app.query_one(Input)
|
||||
input.value = "8"
|
||||
await input.action_submit()
|
||||
await pilot.pause()
|
||||
assert len(app.messages) == 2
|
||||
assert app.messages[1].validation_result == ValidationResult.failure(
|
||||
failures=[
|
||||
Number.NotInRange(
|
||||
value="8",
|
||||
validator=app.validator,
|
||||
description="Must be between 1 and 5.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
async def test_input_submitted_message_validation_success():
|
||||
app = InputApp()
|
||||
async with app.run_test() as pilot:
|
||||
input = app.query_one(Input)
|
||||
input.value = "3"
|
||||
await input.action_submit()
|
||||
await pilot.pause()
|
||||
assert len(app.messages) == 2
|
||||
assert app.messages[1].validation_result == ValidationResult.success()
|
||||
100
tests/selection_list/test_selection_list_create.py
Normal file
100
tests/selection_list/test_selection_list_create.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""Core selection list unit tests, aimed at testing basic list creation.
|
||||
|
||||
Note that the vast majority of the API *isn't* tested in here as
|
||||
`SelectionList` inherits from `OptionList` and so that would be duplicated
|
||||
effort. Instead these tests aim to just test the things that have been
|
||||
changed or wrapped in some way.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from rich.text import Text
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import SelectionList
|
||||
from textual.widgets.option_list import Option
|
||||
from textual.widgets.selection_list import Selection, SelectionError
|
||||
|
||||
|
||||
class SelectionListApp(App[None]):
|
||||
"""Test selection list application."""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield SelectionList[int](
|
||||
("0", 0),
|
||||
("1", 1, False),
|
||||
("2", 2, True),
|
||||
Selection("3", 3, id="3"),
|
||||
Selection("4", 4, True, id="4"),
|
||||
)
|
||||
|
||||
|
||||
async def test_all_parameters_become_selctions() -> None:
|
||||
"""All input parameters to a list should become selections."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
selections = pilot.app.query_one(SelectionList)
|
||||
assert selections.option_count == 5
|
||||
for n in range(5):
|
||||
assert isinstance(selections.get_option_at_index(n), Selection)
|
||||
|
||||
|
||||
async def test_get_selection_by_index() -> None:
|
||||
"""It should be possible to get a selection by index."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
option_list = pilot.app.query_one(SelectionList)
|
||||
for n in range(5):
|
||||
assert option_list.get_option_at_index(n).prompt == Text(str(n))
|
||||
assert option_list.get_option_at_index(-1).prompt == Text("4")
|
||||
|
||||
|
||||
async def test_get_selection_by_id() -> None:
|
||||
"""It should be possible to get a selection by ID."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
option_list = pilot.app.query_one(SelectionList)
|
||||
assert option_list.get_option("3").prompt == Text("3")
|
||||
assert option_list.get_option("4").prompt == Text("4")
|
||||
|
||||
|
||||
async def test_add_later() -> None:
|
||||
"""It should be possible to add more items to a selection list."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
selections = pilot.app.query_one(SelectionList)
|
||||
assert selections.option_count == 5
|
||||
selections.add_option(("5", 5))
|
||||
assert selections.option_count == 6
|
||||
selections.add_option(Selection("6", 6))
|
||||
assert selections.option_count == 7
|
||||
selections.add_options(
|
||||
[Selection("7", 7), Selection("8", 8, True), ("9", 9), ("10", 10, True)]
|
||||
)
|
||||
assert selections.option_count == 11
|
||||
selections.add_options([])
|
||||
assert selections.option_count == 11
|
||||
|
||||
|
||||
async def test_add_later_selcted_state() -> None:
|
||||
"""When adding selections later the selected collection should get updated."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
selections = pilot.app.query_one(SelectionList)
|
||||
assert selections.selected == [2, 4]
|
||||
selections.add_option(("5", 5, True))
|
||||
assert selections.selected == [2, 4, 5]
|
||||
selections.add_option(Selection("6", 6, True))
|
||||
assert selections.selected == [2, 4, 5, 6]
|
||||
|
||||
|
||||
async def test_add_non_selections() -> None:
|
||||
"""Adding options that aren't selections should result in errors."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
selections = pilot.app.query_one(SelectionList)
|
||||
with pytest.raises(SelectionError):
|
||||
selections.add_option(None)
|
||||
with pytest.raises(SelectionError):
|
||||
selections.add_option(Option("Nope"))
|
||||
with pytest.raises(SelectionError):
|
||||
selections.add_option("Nope")
|
||||
with pytest.raises(SelectionError):
|
||||
selections.add_option(("Nope",))
|
||||
with pytest.raises(SelectionError):
|
||||
selections.add_option(("Nope", 0, False, 23))
|
||||
210
tests/selection_list/test_selection_messages.py
Normal file
210
tests/selection_list/test_selection_messages.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""Unit tests aimed at testing the selection list messages.
|
||||
|
||||
Note that these tests only cover a subset of the public API of this widget.
|
||||
The bulk of the API is inherited from OptionList, and as such there are
|
||||
comprehensive tests for that. These tests simply cover the parts of the API
|
||||
that have been modified by the child class.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from textual import on
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.messages import Message
|
||||
from textual.widgets import OptionList, SelectionList
|
||||
|
||||
|
||||
class SelectionListApp(App[None]):
|
||||
"""Test selection list application."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.messages: list[tuple[str, int | None]] = []
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield SelectionList[int](*[(str(n), n) for n in range(10)])
|
||||
|
||||
@on(OptionList.OptionHighlighted)
|
||||
@on(OptionList.OptionSelected)
|
||||
@on(SelectionList.SelectionHighlighted)
|
||||
@on(SelectionList.SelectionToggled)
|
||||
@on(SelectionList.SelectedChanged)
|
||||
def _record(self, event: Message) -> None:
|
||||
assert event.control == self.query_one(SelectionList)
|
||||
self.messages.append(
|
||||
(
|
||||
event.__class__.__name__,
|
||||
event.selection_index
|
||||
if isinstance(event, SelectionList.SelectionMessage)
|
||||
else None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def test_messages_on_startup() -> None:
|
||||
"""There should be a highlighted message when a non-empty selection list first starts up."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
assert isinstance(pilot.app, SelectionListApp)
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == [("SelectionHighlighted", 0)]
|
||||
|
||||
|
||||
async def test_new_highlight() -> None:
|
||||
"""Setting the highlight to a new option should result in a message."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
assert isinstance(pilot.app, SelectionListApp)
|
||||
await pilot.pause()
|
||||
pilot.app.query_one(SelectionList).highlighted = 2
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages[1:] == [("SelectionHighlighted", 2)]
|
||||
|
||||
|
||||
async def test_toggle() -> None:
|
||||
"""Toggling an option should result in messages."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
assert isinstance(pilot.app, SelectionListApp)
|
||||
await pilot.pause()
|
||||
pilot.app.query_one(SelectionList).toggle(0)
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == [
|
||||
("SelectionHighlighted", 0),
|
||||
("SelectedChanged", None),
|
||||
]
|
||||
|
||||
|
||||
async def test_toggle_via_user() -> None:
|
||||
"""Toggling via the user should result in the correct messages."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
assert isinstance(pilot.app, SelectionListApp)
|
||||
await pilot.press("space")
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == [
|
||||
("SelectionHighlighted", 0),
|
||||
("SelectedChanged", None),
|
||||
("SelectionToggled", 0),
|
||||
]
|
||||
|
||||
|
||||
async def test_toggle_all() -> None:
|
||||
"""Toggling all options should result in messages."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
assert isinstance(pilot.app, SelectionListApp)
|
||||
await pilot.pause()
|
||||
pilot.app.query_one(SelectionList).toggle_all()
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == [
|
||||
("SelectionHighlighted", 0),
|
||||
("SelectedChanged", None),
|
||||
]
|
||||
|
||||
|
||||
async def test_select() -> None:
|
||||
"""Selecting all an option should result in a message."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
assert isinstance(pilot.app, SelectionListApp)
|
||||
await pilot.pause()
|
||||
pilot.app.query_one(SelectionList).select(1)
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == [
|
||||
("SelectionHighlighted", 0),
|
||||
("SelectedChanged", None),
|
||||
]
|
||||
|
||||
|
||||
async def test_select_selected() -> None:
|
||||
"""Selecting an option that is already selected should emit no extra message.."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
assert isinstance(pilot.app, SelectionListApp)
|
||||
await pilot.pause()
|
||||
pilot.app.query_one(SelectionList).select(0)
|
||||
await pilot.pause()
|
||||
pilot.app.query_one(SelectionList).select(0)
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == [
|
||||
("SelectionHighlighted", 0),
|
||||
("SelectedChanged", None),
|
||||
]
|
||||
|
||||
|
||||
async def test_select_all() -> None:
|
||||
"""Selecting all options should result in messages."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
assert isinstance(pilot.app, SelectionListApp)
|
||||
await pilot.pause()
|
||||
pilot.app.query_one(SelectionList).select_all()
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == [
|
||||
("SelectionHighlighted", 0),
|
||||
("SelectedChanged", None),
|
||||
]
|
||||
|
||||
|
||||
async def test_select_all_selected() -> None:
|
||||
"""Selecting all when all are selected should result in no extra messages."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
assert isinstance(pilot.app, SelectionListApp)
|
||||
await pilot.pause()
|
||||
pilot.app.query_one(SelectionList).select_all()
|
||||
await pilot.pause()
|
||||
pilot.app.query_one(SelectionList).select_all()
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == [
|
||||
("SelectionHighlighted", 0),
|
||||
("SelectedChanged", None),
|
||||
]
|
||||
|
||||
|
||||
async def test_deselect() -> None:
|
||||
"""Deselecting an option should result in a message."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
assert isinstance(pilot.app, SelectionListApp)
|
||||
await pilot.pause()
|
||||
pilot.app.query_one(SelectionList).select(1)
|
||||
await pilot.pause()
|
||||
pilot.app.query_one(SelectionList).deselect(1)
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == [
|
||||
("SelectionHighlighted", 0),
|
||||
("SelectedChanged", None),
|
||||
("SelectedChanged", None),
|
||||
]
|
||||
|
||||
|
||||
async def test_deselect_deselected() -> None:
|
||||
"""Deselecting a deselected option should result in no extra messages."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
assert isinstance(pilot.app, SelectionListApp)
|
||||
await pilot.pause()
|
||||
pilot.app.query_one(SelectionList).deselect(0)
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == [("SelectionHighlighted", 0)]
|
||||
|
||||
|
||||
async def test_deselect_all() -> None:
|
||||
"""Deselecting all deselected options should result in no additional messages."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
assert isinstance(pilot.app, SelectionListApp)
|
||||
await pilot.pause()
|
||||
pilot.app.query_one(SelectionList).deselect_all()
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == [("SelectionHighlighted", 0)]
|
||||
|
||||
|
||||
async def test_select_then_deselect_all() -> None:
|
||||
"""Selecting and then deselecting all options should result in messages."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
assert isinstance(pilot.app, SelectionListApp)
|
||||
await pilot.pause()
|
||||
pilot.app.query_one(SelectionList).select_all()
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == [
|
||||
("SelectionHighlighted", 0),
|
||||
("SelectedChanged", None),
|
||||
]
|
||||
pilot.app.query_one(SelectionList).deselect_all()
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == [
|
||||
("SelectionHighlighted", 0),
|
||||
("SelectedChanged", None),
|
||||
("SelectedChanged", None),
|
||||
]
|
||||
82
tests/selection_list/test_selection_values.py
Normal file
82
tests/selection_list/test_selection_values.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Unit tests dealing with the tracking of selection list values."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import SelectionList
|
||||
|
||||
|
||||
class SelectionListApp(App[None]):
|
||||
def __init__(self, default_state: bool = False) -> None:
|
||||
super().__init__()
|
||||
self._default_state = default_state
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield SelectionList[int](*[(str(n), n, self._default_state) for n in range(50)])
|
||||
|
||||
|
||||
async def test_empty_selected() -> None:
|
||||
"""Selected should be empty when nothing is selected."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
assert pilot.app.query_one(SelectionList).selected == []
|
||||
|
||||
|
||||
async def test_programatic_select() -> None:
|
||||
"""Selected should contain a selected value."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
selection = pilot.app.query_one(SelectionList)
|
||||
selection.select(0)
|
||||
assert pilot.app.query_one(SelectionList).selected == [0]
|
||||
|
||||
|
||||
async def test_programatic_select_all() -> None:
|
||||
"""Selected should contain all selected values."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
selection = pilot.app.query_one(SelectionList)
|
||||
selection.select_all()
|
||||
assert pilot.app.query_one(SelectionList).selected == list(range(50))
|
||||
|
||||
|
||||
async def test_programatic_deselect() -> None:
|
||||
"""Selected should not contain a deselected value."""
|
||||
async with SelectionListApp(True).run_test() as pilot:
|
||||
selection = pilot.app.query_one(SelectionList)
|
||||
selection.deselect(0)
|
||||
assert pilot.app.query_one(SelectionList).selected == list(range(50)[1:])
|
||||
|
||||
|
||||
async def test_programatic_deselect_all() -> None:
|
||||
"""Selected should not contain anything after deselecting all values."""
|
||||
async with SelectionListApp(True).run_test() as pilot:
|
||||
selection = pilot.app.query_one(SelectionList)
|
||||
selection.deselect_all()
|
||||
assert pilot.app.query_one(SelectionList).selected == []
|
||||
|
||||
|
||||
async def test_programatic_toggle() -> None:
|
||||
"""Selected should reflect a toggle."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
selection = pilot.app.query_one(SelectionList)
|
||||
for n in range(25, 50):
|
||||
selection.select(n)
|
||||
for n in range(50):
|
||||
selection.toggle(n)
|
||||
assert pilot.app.query_one(SelectionList).selected == list(range(50)[:25])
|
||||
|
||||
|
||||
async def test_programatic_toggle_all() -> None:
|
||||
"""Selected should contain all values after toggling all on."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
selection = pilot.app.query_one(SelectionList)
|
||||
selection.toggle_all()
|
||||
assert pilot.app.query_one(SelectionList).selected == list(range(50))
|
||||
|
||||
|
||||
async def test_removal_of_selected_item() -> None:
|
||||
"""Removing a selected selection should remove its value from the selected set."""
|
||||
async with SelectionListApp().run_test() as pilot:
|
||||
selection = pilot.app.query_one(SelectionList)
|
||||
selection.toggle(0)
|
||||
assert pilot.app.query_one(SelectionList).selected == [0]
|
||||
selection.remove_option_at_index(0)
|
||||
assert pilot.app.query_one(SelectionList).selected == []
|
||||
File diff suppressed because one or more lines are too long
20
tests/snapshot_tests/snapshot_apps/blur_on_disabled.py
Normal file
20
tests/snapshot_tests/snapshot_apps/blur_on_disabled.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Input
|
||||
|
||||
|
||||
class BlurApp(App):
|
||||
BINDINGS = [("f3", "disable")]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Input()
|
||||
|
||||
def on_ready(self) -> None:
|
||||
self.query_one(Input).focus()
|
||||
|
||||
def action_disable(self) -> None:
|
||||
self.query_one(Input).disabled = True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = BlurApp()
|
||||
app.run()
|
||||
45
tests/snapshot_tests/snapshot_apps/input_validation.py
Normal file
45
tests/snapshot_tests/snapshot_apps/input_validation.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.validation import Number
|
||||
from textual.widgets import Input
|
||||
|
||||
VALIDATORS = [
|
||||
Number(minimum=1, maximum=5),
|
||||
]
|
||||
|
||||
|
||||
class InputApp(App):
|
||||
CSS = """
|
||||
Input.-valid {
|
||||
border: tall $success 60%;
|
||||
}
|
||||
Input.-valid:focus {
|
||||
border: tall $success;
|
||||
}
|
||||
Input {
|
||||
margin: 1 2;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Input(
|
||||
placeholder="Enter a number between 1 and 5",
|
||||
validators=VALIDATORS,
|
||||
)
|
||||
yield Input(
|
||||
placeholder="Enter a number between 1 and 5",
|
||||
validators=VALIDATORS,
|
||||
)
|
||||
yield Input(
|
||||
placeholder="Enter a number between 1 and 5",
|
||||
validators=VALIDATORS,
|
||||
)
|
||||
yield Input(
|
||||
placeholder="Enter a number between 1 and 5",
|
||||
validators=VALIDATORS,
|
||||
)
|
||||
|
||||
|
||||
app = InputApp()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
@@ -84,6 +84,19 @@ def test_input_and_focus(snap_compare):
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "input.py", press=press)
|
||||
|
||||
|
||||
def test_input_validation(snap_compare):
|
||||
"""Checking that invalid styling is applied. The snapshot app itself
|
||||
also adds styling for -valid which gives a green border."""
|
||||
press = [
|
||||
*"-2", # -2 is invalid, so -invalid should be applied
|
||||
"tab",
|
||||
"3", # This is valid, so -valid should be applied
|
||||
"tab",
|
||||
*"-2", # -2 is invalid, so -invalid should be applied (and :focus, since we stop here)
|
||||
]
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "input_validation.py", press=press)
|
||||
|
||||
|
||||
def test_input_suggestions(snap_compare):
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "input_suggestions.py", press=[])
|
||||
|
||||
@@ -95,7 +108,6 @@ def test_buttons_render(snap_compare):
|
||||
|
||||
def test_placeholder_render(snap_compare):
|
||||
# Testing the rendering of the multiple placeholder variants and labels.
|
||||
Placeholder.reset_color_cycle()
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "placeholder.py")
|
||||
|
||||
|
||||
@@ -238,6 +250,18 @@ def test_select(snap_compare):
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "select_widget.py")
|
||||
|
||||
|
||||
def test_selection_list_selected(snap_compare):
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "selection_list_selected.py")
|
||||
|
||||
|
||||
def test_selection_list_selections(snap_compare):
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "selection_list_selections.py")
|
||||
|
||||
|
||||
def test_selection_list_tuples(snap_compare):
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "selection_list_tuples.py")
|
||||
|
||||
|
||||
def test_select_expanded(snap_compare):
|
||||
assert snap_compare(
|
||||
WIDGET_EXAMPLES_DIR / "select_widget.py", press=["tab", "enter"]
|
||||
@@ -265,7 +289,6 @@ PATHS = [
|
||||
@pytest.mark.parametrize("file_name", PATHS)
|
||||
def test_css_property(file_name, snap_compare):
|
||||
path_to_app = STYLES_EXAMPLES_DIR / file_name
|
||||
Placeholder.reset_color_cycle()
|
||||
assert snap_compare(path_to_app)
|
||||
|
||||
|
||||
@@ -524,3 +547,11 @@ def test_select_rebuild(snap_compare):
|
||||
SNAPSHOT_APPS_DIR / "select_rebuild.py",
|
||||
press=["space", "escape", "tab", "enter", "tab", "space"],
|
||||
)
|
||||
|
||||
|
||||
def test_blur_on_disabled(snap_compare):
|
||||
# https://github.com/Textualize/textual/issues/2641
|
||||
assert snap_compare(
|
||||
SNAPSHOT_APPS_DIR / "blur_on_disabled.py",
|
||||
press=[*"foo", "f3", *"this should not appear"],
|
||||
)
|
||||
|
||||
@@ -1009,7 +1009,9 @@ async def test_scrolling_cursor_into_view():
|
||||
table.add_column("n")
|
||||
table.add_rows([(n,) for n in range(300)])
|
||||
|
||||
await pilot.pause()
|
||||
await pilot.press("c")
|
||||
await pilot.pause()
|
||||
assert table.scroll_y > 100
|
||||
|
||||
|
||||
|
||||
28
tests/test_footer.py
Normal file
28
tests/test_footer.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.geometry import Offset
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Footer, Label
|
||||
|
||||
|
||||
async def test_footer_highlight_when_pushing_modal():
|
||||
"""Regression test for https://github.com/Textualize/textual/issues/2606"""
|
||||
|
||||
class MyModalScreen(ModalScreen):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label("apple")
|
||||
|
||||
class MyApp(App[None]):
|
||||
BINDINGS = [("a", "p", "push")]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Footer()
|
||||
|
||||
def action_p(self):
|
||||
self.push_screen(MyModalScreen())
|
||||
|
||||
app = MyApp()
|
||||
async with app.run_test(size=(80, 2)) as pilot:
|
||||
await pilot.hover(None, Offset(0, 1))
|
||||
await pilot.click(None, Offset(0, 1))
|
||||
assert isinstance(app.screen, MyModalScreen)
|
||||
assert app.screen_stack[0].query_one(Footer).highlight_key is None
|
||||
@@ -413,3 +413,51 @@ async def test_public_and_private_watch() -> None:
|
||||
pilot.app.counter += 1
|
||||
assert calls["private"] is True
|
||||
assert calls["public"] is True
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="https://github.com/Textualize/textual/issues/2539")
|
||||
async def test_public_and_private_validate() -> None:
|
||||
"""If a reactive/var has public and private validate both should get called."""
|
||||
|
||||
calls: dict[str, bool] = {"private": False, "public": False}
|
||||
|
||||
class PrivateValidateTest(App):
|
||||
counter = var(0, init=False)
|
||||
|
||||
def validate_counter(self, _: int) -> None:
|
||||
calls["public"] = True
|
||||
|
||||
def _validate_counter(self, _: int) -> None:
|
||||
calls["private"] = True
|
||||
|
||||
async with PrivateValidateTest().run_test() as pilot:
|
||||
assert calls["private"] is False
|
||||
assert calls["public"] is False
|
||||
pilot.app.counter += 1
|
||||
assert calls["private"] is True
|
||||
assert calls["public"] is True
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="https://github.com/Textualize/textual/issues/2539")
|
||||
async def test_public_and_private_compute() -> None:
|
||||
"""If a reactive/var has public and private compute both should get called."""
|
||||
|
||||
calls: dict[str, bool] = {"private": False, "public": False}
|
||||
|
||||
class PrivateComputeTest(App):
|
||||
counter = var(0, init=False)
|
||||
|
||||
def compute_counter(self) -> int:
|
||||
calls["public"] = True
|
||||
return 23
|
||||
|
||||
def _compute_counter(self) -> int:
|
||||
calls["private"] = True
|
||||
return 42
|
||||
|
||||
async with PrivateComputeTest().run_test() as pilot:
|
||||
assert calls["private"] is False
|
||||
assert calls["public"] is False
|
||||
_ = pilot.app.counter
|
||||
assert calls["private"] is True
|
||||
assert calls["public"] is True
|
||||
|
||||
277
tests/test_screen_modes.py
Normal file
277
tests/test_screen_modes.py
Normal file
@@ -0,0 +1,277 @@
|
||||
from functools import partial
|
||||
from itertools import cycle
|
||||
from typing import Type
|
||||
|
||||
import pytest
|
||||
|
||||
from textual.app import (
|
||||
ActiveModeError,
|
||||
App,
|
||||
ComposeResult,
|
||||
InvalidModeError,
|
||||
UnknownModeError,
|
||||
)
|
||||
from textual.screen import ModalScreen, Screen
|
||||
from textual.widgets import Footer, Header, Label, TextLog
|
||||
|
||||
FRUITS = cycle("apple mango strawberry banana peach pear melon watermelon".split())
|
||||
|
||||
|
||||
class ScreenBindingsMixin(Screen[None]):
|
||||
BINDINGS = [
|
||||
("1", "one", "Mode 1"),
|
||||
("2", "two", "Mode 2"),
|
||||
("p", "push", "Push rnd scrn"),
|
||||
("o", "pop_screen", "Pop"),
|
||||
("r", "remove", "Remove mode 1"),
|
||||
]
|
||||
|
||||
def action_one(self) -> None:
|
||||
self.app.switch_mode("one")
|
||||
|
||||
def action_two(self) -> None:
|
||||
self.app.switch_mode("two")
|
||||
|
||||
def action_fruits(self) -> None:
|
||||
self.app.switch_mode("fruits")
|
||||
|
||||
def action_push(self) -> None:
|
||||
self.app.push_screen(FruitModal())
|
||||
|
||||
|
||||
class BaseScreen(ScreenBindingsMixin):
|
||||
def __init__(self, label):
|
||||
super().__init__()
|
||||
self.label = label
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Label(self.label)
|
||||
yield Footer()
|
||||
|
||||
def action_remove(self) -> None:
|
||||
self.app.remove_mode("one")
|
||||
|
||||
|
||||
class FruitModal(ModalScreen[str], ScreenBindingsMixin):
|
||||
BINDINGS = [("d", "dismiss_fruit", "Dismiss")]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label(next(FRUITS))
|
||||
|
||||
|
||||
class FruitsScreen(ScreenBindingsMixin):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield TextLog()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ModesApp():
|
||||
class ModesApp(App[None]):
|
||||
MODES = {
|
||||
"one": lambda: BaseScreen("one"),
|
||||
"two": "screen_two",
|
||||
}
|
||||
|
||||
SCREENS = {
|
||||
"screen_two": lambda: BaseScreen("two"),
|
||||
}
|
||||
|
||||
def on_mount(self):
|
||||
self.switch_mode("one")
|
||||
|
||||
return ModesApp
|
||||
|
||||
|
||||
async def test_mode_setup(ModesApp: Type[App]):
|
||||
app = ModesApp()
|
||||
async with app.run_test():
|
||||
assert isinstance(app.screen, BaseScreen)
|
||||
assert str(app.screen.query_one(Label).renderable) == "one"
|
||||
|
||||
|
||||
async def test_switch_mode(ModesApp: Type[App]):
|
||||
app = ModesApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press("2")
|
||||
assert str(app.screen.query_one(Label).renderable) == "two"
|
||||
await pilot.press("1")
|
||||
assert str(app.screen.query_one(Label).renderable) == "one"
|
||||
|
||||
|
||||
async def test_switch_same_mode(ModesApp: Type[App]):
|
||||
app = ModesApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press("1")
|
||||
assert str(app.screen.query_one(Label).renderable) == "one"
|
||||
await pilot.press("1")
|
||||
assert str(app.screen.query_one(Label).renderable) == "one"
|
||||
|
||||
|
||||
async def test_switch_unknown_mode(ModesApp: Type[App]):
|
||||
app = ModesApp()
|
||||
async with app.run_test():
|
||||
with pytest.raises(UnknownModeError):
|
||||
app.switch_mode("unknown mode here")
|
||||
|
||||
|
||||
async def test_remove_mode(ModesApp: Type[App]):
|
||||
app = ModesApp()
|
||||
async with app.run_test() as pilot:
|
||||
app.switch_mode("two")
|
||||
await pilot.pause()
|
||||
assert str(app.screen.query_one(Label).renderable) == "two"
|
||||
app.remove_mode("one")
|
||||
assert "one" not in app.MODES
|
||||
|
||||
|
||||
async def test_remove_active_mode(ModesApp: Type[App]):
|
||||
app = ModesApp()
|
||||
async with app.run_test():
|
||||
with pytest.raises(ActiveModeError):
|
||||
app.remove_mode("one")
|
||||
|
||||
|
||||
async def test_add_mode(ModesApp: Type[App]):
|
||||
app = ModesApp()
|
||||
async with app.run_test() as pilot:
|
||||
app.add_mode("three", BaseScreen("three"))
|
||||
app.switch_mode("three")
|
||||
await pilot.pause()
|
||||
assert str(app.screen.query_one(Label).renderable) == "three"
|
||||
|
||||
|
||||
async def test_add_mode_duplicated(ModesApp: Type[App]):
|
||||
app = ModesApp()
|
||||
async with app.run_test():
|
||||
with pytest.raises(InvalidModeError):
|
||||
app.add_mode("one", BaseScreen("one"))
|
||||
|
||||
|
||||
async def test_screen_stack_preserved(ModesApp: Type[App]):
|
||||
fruits = []
|
||||
N = 5
|
||||
|
||||
app = ModesApp()
|
||||
async with app.run_test() as pilot:
|
||||
# Build the stack up.
|
||||
for _ in range(N):
|
||||
await pilot.press("p")
|
||||
fruits.append(str(app.query_one(Label).renderable))
|
||||
|
||||
assert len(app.screen_stack) == N + 1
|
||||
|
||||
# Switch out and back.
|
||||
await pilot.press("2")
|
||||
assert len(app.screen_stack) == 1
|
||||
await pilot.press("1")
|
||||
|
||||
# Check the stack.
|
||||
assert len(app.screen_stack) == N + 1
|
||||
for _ in range(N):
|
||||
assert str(app.query_one(Label).renderable) == fruits.pop()
|
||||
await pilot.press("o")
|
||||
|
||||
|
||||
async def test_inactive_stack_is_alive():
|
||||
"""This tests that timers in screens outside the active stack keep going."""
|
||||
pings = []
|
||||
|
||||
class FastCounter(Screen[None]):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label("fast")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.set_interval(0.01, self.ping)
|
||||
|
||||
def ping(self) -> None:
|
||||
pings.append(str(self.app.query_one(Label).renderable))
|
||||
|
||||
def key_s(self):
|
||||
self.app.switch_mode("smile")
|
||||
|
||||
class SmileScreen(Screen[None]):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label(":)")
|
||||
|
||||
def key_s(self):
|
||||
self.app.switch_mode("fast")
|
||||
|
||||
class ModesApp(App[None]):
|
||||
MODES = {
|
||||
"fast": FastCounter,
|
||||
"smile": SmileScreen,
|
||||
}
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.switch_mode("fast")
|
||||
|
||||
app = ModesApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press("s")
|
||||
assert str(app.query_one(Label).renderable) == ":)"
|
||||
await pilot.press("s")
|
||||
assert ":)" in pings
|
||||
|
||||
|
||||
async def test_multiple_mode_callbacks():
|
||||
written = []
|
||||
|
||||
class LogScreen(Screen[None]):
|
||||
def __init__(self, value):
|
||||
super().__init__()
|
||||
self.value = value
|
||||
|
||||
def key_p(self) -> None:
|
||||
self.app.push_screen(ResultScreen(self.value), written.append)
|
||||
|
||||
class ResultScreen(Screen[str]):
|
||||
def __init__(self, value):
|
||||
super().__init__()
|
||||
self.value = value
|
||||
|
||||
def key_p(self) -> None:
|
||||
self.dismiss(self.value)
|
||||
|
||||
def key_f(self) -> None:
|
||||
self.app.switch_mode("first")
|
||||
|
||||
def key_o(self) -> None:
|
||||
self.app.switch_mode("other")
|
||||
|
||||
class ModesApp(App[None]):
|
||||
MODES = {
|
||||
"first": lambda: LogScreen("first"),
|
||||
"other": lambda: LogScreen("other"),
|
||||
}
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.switch_mode("first")
|
||||
|
||||
def key_f(self) -> None:
|
||||
self.switch_mode("first")
|
||||
|
||||
def key_o(self) -> None:
|
||||
self.switch_mode("other")
|
||||
|
||||
app = ModesApp()
|
||||
async with app.run_test() as pilot:
|
||||
# Push and dismiss ResultScreen("first")
|
||||
await pilot.press("p")
|
||||
await pilot.press("p")
|
||||
assert written == ["first"]
|
||||
|
||||
# Push ResultScreen("first")
|
||||
await pilot.press("p")
|
||||
# Switch to LogScreen("other")
|
||||
await pilot.press("o")
|
||||
# Push and dismiss ResultScreen("other")
|
||||
await pilot.press("p")
|
||||
await pilot.press("p")
|
||||
assert written == ["first", "other"]
|
||||
|
||||
# Go back to ResultScreen("first")
|
||||
await pilot.press("f")
|
||||
# Dismiss ResultScreen("first")
|
||||
await pilot.press("p")
|
||||
assert written == ["first", "other", "first"]
|
||||
@@ -153,7 +153,9 @@ async def test_screens():
|
||||
await app._shutdown()
|
||||
|
||||
|
||||
async def test_auto_focus():
|
||||
async def test_auto_focus_on_screen_if_app_auto_focus_is_none():
|
||||
"""Setting app.AUTO_FOCUS = `None` means it is not taken into consideration."""
|
||||
|
||||
class MyScreen(Screen[None]):
|
||||
def compose(self):
|
||||
yield Button()
|
||||
@@ -161,10 +163,11 @@ async def test_auto_focus():
|
||||
yield Input(id="two")
|
||||
|
||||
class MyApp(App[None]):
|
||||
pass
|
||||
AUTO_FOCUS = None
|
||||
|
||||
app = MyApp()
|
||||
async with app.run_test():
|
||||
MyScreen.AUTO_FOCUS = "*"
|
||||
await app.push_screen(MyScreen())
|
||||
assert isinstance(app.focused, Button)
|
||||
app.pop_screen()
|
||||
@@ -193,6 +196,80 @@ async def test_auto_focus():
|
||||
assert app.focused.id == "two"
|
||||
|
||||
|
||||
async def test_auto_focus_on_screen_if_app_auto_focus_is_disabled():
|
||||
"""Setting app.AUTO_FOCUS = `None` means it is not taken into consideration."""
|
||||
|
||||
class MyScreen(Screen[None]):
|
||||
def compose(self):
|
||||
yield Button()
|
||||
yield Input(id="one")
|
||||
yield Input(id="two")
|
||||
|
||||
class MyApp(App[None]):
|
||||
AUTO_FOCUS = ""
|
||||
|
||||
app = MyApp()
|
||||
async with app.run_test():
|
||||
MyScreen.AUTO_FOCUS = "*"
|
||||
await app.push_screen(MyScreen())
|
||||
assert isinstance(app.focused, Button)
|
||||
app.pop_screen()
|
||||
|
||||
MyScreen.AUTO_FOCUS = None
|
||||
await app.push_screen(MyScreen())
|
||||
assert app.focused is None
|
||||
app.pop_screen()
|
||||
|
||||
MyScreen.AUTO_FOCUS = "Input"
|
||||
await app.push_screen(MyScreen())
|
||||
assert isinstance(app.focused, Input)
|
||||
assert app.focused.id == "one"
|
||||
app.pop_screen()
|
||||
|
||||
MyScreen.AUTO_FOCUS = "#two"
|
||||
await app.push_screen(MyScreen())
|
||||
assert isinstance(app.focused, Input)
|
||||
assert app.focused.id == "two"
|
||||
|
||||
# If we push and pop another screen, focus should be preserved for #two.
|
||||
MyScreen.AUTO_FOCUS = None
|
||||
await app.push_screen(MyScreen())
|
||||
assert app.focused is None
|
||||
app.pop_screen()
|
||||
assert app.focused.id == "two"
|
||||
|
||||
|
||||
async def test_auto_focus_inheritance():
|
||||
"""Setting app.AUTO_FOCUS = `None` means it is not taken into consideration."""
|
||||
|
||||
class MyScreen(Screen[None]):
|
||||
def compose(self):
|
||||
yield Button()
|
||||
yield Input(id="one")
|
||||
yield Input(id="two")
|
||||
|
||||
class MyApp(App[None]):
|
||||
pass
|
||||
|
||||
app = MyApp()
|
||||
async with app.run_test():
|
||||
MyApp.AUTO_FOCUS = "Input"
|
||||
MyScreen.AUTO_FOCUS = "*"
|
||||
await app.push_screen(MyScreen())
|
||||
assert isinstance(app.focused, Button)
|
||||
app.pop_screen()
|
||||
|
||||
MyScreen.AUTO_FOCUS = None
|
||||
await app.push_screen(MyScreen())
|
||||
assert isinstance(app.focused, Input)
|
||||
app.pop_screen()
|
||||
|
||||
MyScreen.AUTO_FOCUS = ""
|
||||
await app.push_screen(MyScreen())
|
||||
assert app.focused is None
|
||||
app.pop_screen()
|
||||
|
||||
|
||||
async def test_auto_focus_skips_non_focusable_widgets():
|
||||
class MyScreen(Screen[None]):
|
||||
def compose(self):
|
||||
|
||||
216
tests/test_validation.py
Normal file
216
tests/test_validation.py
Normal file
@@ -0,0 +1,216 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from textual.validation import (
|
||||
URL,
|
||||
Failure,
|
||||
Function,
|
||||
Integer,
|
||||
Length,
|
||||
Number,
|
||||
Regex,
|
||||
ValidationResult,
|
||||
Validator,
|
||||
)
|
||||
|
||||
VALIDATOR = Function(lambda value: True)
|
||||
|
||||
|
||||
def test_ValidationResult_merge_successes():
|
||||
results = [ValidationResult.success(), ValidationResult.success()]
|
||||
assert ValidationResult.merge(results) == ValidationResult.success()
|
||||
|
||||
|
||||
def test_ValidationResult_merge_failures():
|
||||
failure_one = Failure(VALIDATOR, "1")
|
||||
failure_two = Failure(VALIDATOR, "2")
|
||||
results = [
|
||||
ValidationResult.failure([failure_one]),
|
||||
ValidationResult.failure([failure_two]),
|
||||
ValidationResult.success(),
|
||||
]
|
||||
expected_result = ValidationResult.failure([failure_one, failure_two])
|
||||
assert ValidationResult.merge(results) == expected_result
|
||||
|
||||
|
||||
def test_ValidationResult_failure_descriptions():
|
||||
result = ValidationResult.failure(
|
||||
[
|
||||
Failure(VALIDATOR, description="One"),
|
||||
Failure(VALIDATOR, description="Two"),
|
||||
Failure(VALIDATOR, description="Three"),
|
||||
],
|
||||
)
|
||||
assert result.failure_descriptions == ["One", "Two", "Three"]
|
||||
|
||||
|
||||
class ValidatorWithDescribeFailure(Validator):
|
||||
def validate(self, value: str) -> ValidationResult:
|
||||
return self.failure()
|
||||
|
||||
def describe_failure(self, failure: Failure) -> str | None:
|
||||
return "describe_failure"
|
||||
|
||||
|
||||
def test_Failure_description_priorities_parameter_only():
|
||||
number_validator = Number(failure_description="ABC")
|
||||
non_number_value = "x"
|
||||
result = number_validator.validate(non_number_value)
|
||||
# The inline value takes priority over the describe_failure.
|
||||
assert result.failures[0].description == "ABC"
|
||||
|
||||
|
||||
def test_Failure_description_priorities_parameter_and_describe_failure():
|
||||
validator = ValidatorWithDescribeFailure(failure_description="ABC")
|
||||
result = validator.validate("x")
|
||||
# Even though the validator has a `describe_failure`, we've provided it
|
||||
# inline and the inline value should take priority.
|
||||
assert result.failures[0].description == "ABC"
|
||||
|
||||
|
||||
def test_Failure_description_priorities_describe_failure_only():
|
||||
validator = ValidatorWithDescribeFailure()
|
||||
result = validator.validate("x")
|
||||
assert result.failures[0].description == "describe_failure"
|
||||
|
||||
|
||||
class ValidatorWithFailureMessageAndNoDescribe(Validator):
|
||||
def validate(self, value: str) -> ValidationResult:
|
||||
return self.failure(description="ABC")
|
||||
|
||||
|
||||
def test_Failure_description_parameter_and_description_inside_validate():
|
||||
validator = ValidatorWithFailureMessageAndNoDescribe()
|
||||
result = validator.validate("x")
|
||||
assert result.failures[0].description == "ABC"
|
||||
|
||||
|
||||
class ValidatorWithFailureMessageAndDescribe(Validator):
|
||||
def validate(self, value: str) -> ValidationResult:
|
||||
return self.failure(value=value, description="ABC")
|
||||
|
||||
def describe_failure(self, failure: Failure) -> str | None:
|
||||
return "describe_failure"
|
||||
|
||||
|
||||
def test_Failure_description_describe_and_description_inside_validate():
|
||||
# This is kind of a weird case - there's no reason to supply both of
|
||||
# these but lets still make sure we're sensible about how we handle it.
|
||||
validator = ValidatorWithFailureMessageAndDescribe()
|
||||
result = validator.validate("x")
|
||||
assert result.failures == [Failure(validator, "x", "ABC")]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, minimum, maximum, expected_result",
|
||||
[
|
||||
("123", None, None, True), # valid number, no range
|
||||
("-123", None, None, True), # valid negative number, no range
|
||||
("123.45", None, None, True), # valid float, no range
|
||||
("1.23e-4", None, None, True), # valid scientific notation, no range
|
||||
("abc", None, None, False), # non-numeric string, no range
|
||||
("123", 100, 200, True), # valid number within range
|
||||
("99", 100, 200, False), # valid number but not in range
|
||||
("201", 100, 200, False), # valid number but not in range
|
||||
("1.23e4", 0, 50000, True), # valid scientific notation within range
|
||||
],
|
||||
)
|
||||
def test_Number_validate(value, minimum, maximum, expected_result):
|
||||
validator = Number(minimum=minimum, maximum=maximum)
|
||||
result = validator.validate(value)
|
||||
assert result.is_valid == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"regex, value, expected_result",
|
||||
[
|
||||
(r"\d+", "123", True), # matches regex for one or more digits
|
||||
(r"\d+", "abc", False), # does not match regex for one or more digits
|
||||
(r"[a-z]+", "abc", True), # matches regex for one or more lowercase letters
|
||||
(
|
||||
r"[a-z]+",
|
||||
"ABC",
|
||||
False,
|
||||
), # does not match regex for one or more lowercase letters
|
||||
(r"\w+", "abc123", True), # matches regex for one or more word characters
|
||||
(r"\w+", "!@#", False), # does not match regex for one or more word characters
|
||||
],
|
||||
)
|
||||
def test_Regex_validate(regex, value, expected_result):
|
||||
validator = Regex(regex)
|
||||
result = validator.validate(value)
|
||||
assert result.is_valid == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, minimum, maximum, expected_result",
|
||||
[
|
||||
("123", None, None, True), # valid integer, no range
|
||||
("-123", None, None, True), # valid negative integer, no range
|
||||
("123.45", None, None, False), # float, not a valid integer
|
||||
("1.23e-4", None, None, False), # scientific notation, not a valid integer
|
||||
("abc", None, None, False), # non-numeric string, not a valid integer
|
||||
("123", 100, 200, True), # valid integer within range
|
||||
("99", 100, 200, False), # valid integer but not in range
|
||||
("201", 100, 200, False), # valid integer but not in range
|
||||
("1.23e4", None, None, True), # valid integer in scientific notation
|
||||
],
|
||||
)
|
||||
def test_Integer_validate(value, minimum, maximum, expected_result):
|
||||
validator = Integer(minimum=minimum, maximum=maximum)
|
||||
result = validator.validate(value)
|
||||
assert result.is_valid == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, min_length, max_length, expected_result",
|
||||
[
|
||||
("", None, None, True), # empty string
|
||||
("test", None, None, True), # any string with no restrictions
|
||||
("test", 5, None, False), # shorter than minimum length
|
||||
("test", None, 3, False), # longer than maximum length
|
||||
("test", 4, 4, True), # exactly matches minimum and maximum length
|
||||
("test", 2, 6, True), # within length range
|
||||
],
|
||||
)
|
||||
def test_Length_validate(value, min_length, max_length, expected_result):
|
||||
validator = Length(minimum=min_length, maximum=max_length)
|
||||
result = validator.validate(value)
|
||||
assert result.is_valid == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, expected_result",
|
||||
[
|
||||
("http://example.com", True), # valid URL
|
||||
("https://example.com", True), # valid URL with https
|
||||
("www.example.com", False), # missing scheme
|
||||
("://example.com", False), # invalid URL (no scheme)
|
||||
("https:///path", False), # missing netloc
|
||||
(
|
||||
"redis://username:pass[word@localhost:6379/0",
|
||||
False,
|
||||
), # invalid URL characters
|
||||
("", False), # empty string
|
||||
],
|
||||
)
|
||||
def test_URL_validate(value, expected_result):
|
||||
validator = URL()
|
||||
result = validator.validate(value)
|
||||
assert result.is_valid == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"function, failure_description, is_valid",
|
||||
[
|
||||
((lambda value: True), None, True),
|
||||
((lambda value: False), "failure!", False),
|
||||
],
|
||||
)
|
||||
def test_Function_validate(function, failure_description, is_valid):
|
||||
validator = Function(function, failure_description)
|
||||
result = validator.validate("x")
|
||||
assert result.is_valid is is_valid
|
||||
if result.failure_descriptions:
|
||||
assert result.failure_descriptions[0] == failure_description
|
||||
Reference in New Issue
Block a user