Merge pull request #2652 from davep/multiselect

This commit is contained in:
Dave Pearson
2023-05-25 17:10:20 +01:00
committed by GitHub
18 changed files with 1869 additions and 0 deletions

View 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))

View 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),
]

View 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

View File

@@ -245,6 +245,14 @@ def test_progress_bar_completed_styled(snap_compare):
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(