mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
478 lines
16 KiB
Python
478 lines
16 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from textual._wait import wait_for_idle
|
|
from textual.app import App
|
|
from textual.coordinate import Coordinate
|
|
from textual.events import Click
|
|
from textual.message import Message
|
|
from textual.widgets import DataTable
|
|
from textual.widgets._data_table import CellKey
|
|
from textual.widgets.data_table import (
|
|
CellDoesNotExist,
|
|
RowKey,
|
|
Row,
|
|
ColumnKey,
|
|
)
|
|
|
|
ROWS = [["0/0", "0/1"], ["1/0", "1/1"], ["2/0", "2/1"]]
|
|
|
|
|
|
class DataTableApp(App):
|
|
messages_to_record = {
|
|
"CellHighlighted",
|
|
"CellSelected",
|
|
"RowHighlighted",
|
|
"RowSelected",
|
|
"ColumnHighlighted",
|
|
"ColumnSelected",
|
|
}
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.messages = []
|
|
|
|
def compose(self):
|
|
table = DataTable()
|
|
table.focus()
|
|
yield table
|
|
|
|
def record_data_table_event(self, message: Message) -> None:
|
|
name = message.__class__.__name__
|
|
if name in self.messages_to_record:
|
|
self.messages.append(message)
|
|
|
|
@property
|
|
def message_names(self) -> list[str]:
|
|
return [message.__class__.__name__ for message in self.messages]
|
|
|
|
async def _on_message(self, message: Message) -> None:
|
|
await super()._on_message(message)
|
|
self.record_data_table_event(message)
|
|
|
|
|
|
async def test_datatable_message_emission():
|
|
app = DataTableApp()
|
|
expected_messages = []
|
|
async with app.run_test() as pilot:
|
|
table = app.query_one(DataTable)
|
|
|
|
assert app.message_names == expected_messages
|
|
|
|
table.add_columns("Column0", "Column1")
|
|
table.add_rows(ROWS)
|
|
|
|
# A CellHighlighted is emitted because there were no rows (and
|
|
# therefore no highlighted cells), but then a row was added, and
|
|
# so the cell at (0, 0) became highlighted.
|
|
expected_messages.append("CellHighlighted")
|
|
await pilot.pause(2 / 100)
|
|
assert app.message_names == expected_messages
|
|
|
|
# Pressing Enter when the cursor is on a cell emits a CellSelected
|
|
await pilot.press("enter")
|
|
expected_messages.append("CellSelected")
|
|
assert app.message_names == expected_messages
|
|
|
|
# Moving the cursor left and up when the cursor is at origin
|
|
# emits no events, since the cursor doesn't move at all.
|
|
await pilot.press("left", "up")
|
|
assert app.message_names == expected_messages
|
|
|
|
# ROW CURSOR
|
|
# Switch over to the row cursor... should emit a `RowHighlighted`
|
|
table.cursor_type = "row"
|
|
expected_messages.append("RowHighlighted")
|
|
await pilot.pause(2 / 100)
|
|
assert app.message_names == expected_messages
|
|
|
|
# Select the row...
|
|
await pilot.press("enter")
|
|
expected_messages.append("RowSelected")
|
|
assert app.message_names == expected_messages
|
|
|
|
# COLUMN CURSOR
|
|
# Switching to the column cursor emits a `ColumnHighlighted`
|
|
table.cursor_type = "column"
|
|
expected_messages.append("ColumnHighlighted")
|
|
await pilot.pause(2 / 100)
|
|
assert app.message_names == expected_messages
|
|
|
|
# Select the column...
|
|
await pilot.press("enter")
|
|
expected_messages.append("ColumnSelected")
|
|
assert app.message_names == expected_messages
|
|
|
|
# NONE CURSOR
|
|
# No messages get emitted at all...
|
|
table.cursor_type = "none"
|
|
await pilot.press("up", "down", "left", "right", "enter")
|
|
# No new messages since cursor not visible
|
|
assert app.message_names == expected_messages
|
|
|
|
# Edge case - if show_cursor is False, and the cursor type
|
|
# is changed back to a visible type, then no messages should
|
|
# be emitted since the cursor is still not visible.
|
|
table.show_cursor = False
|
|
table.cursor_type = "cell"
|
|
await pilot.press("up", "down", "left", "right", "enter")
|
|
# No new messages since show_cursor = False
|
|
assert app.message_names == expected_messages
|
|
|
|
# Now when show_cursor is set back to True, the appropriate
|
|
# message should be emitted for highlighting the cell.
|
|
table.show_cursor = True
|
|
expected_messages.append("CellHighlighted")
|
|
await pilot.pause(2 / 100)
|
|
assert app.message_names == expected_messages
|
|
|
|
# Similarly for showing the cursor again when row or column
|
|
# cursor was active before the cursor was hidden.
|
|
table.show_cursor = False
|
|
table.cursor_type = "row"
|
|
table.show_cursor = True
|
|
expected_messages.append("RowHighlighted")
|
|
await pilot.pause(2 / 100)
|
|
assert app.message_names == expected_messages
|
|
|
|
table.show_cursor = False
|
|
table.cursor_type = "column"
|
|
table.show_cursor = True
|
|
expected_messages.append("ColumnHighlighted")
|
|
await pilot.pause(2 / 100)
|
|
assert app.message_names == expected_messages
|
|
|
|
# Likewise, if the cursor_type is "none", and we change the
|
|
# show_cursor to True, then no events should be raised since
|
|
# the cursor is still not visible to the user.
|
|
table.cursor_type = "none"
|
|
await pilot.press("up", "down", "left", "right", "enter")
|
|
assert app.message_names == expected_messages
|
|
|
|
|
|
async def test_add_rows():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
table.add_columns("A", "B")
|
|
row_keys = table.add_rows(ROWS)
|
|
# We're given a key for each row
|
|
assert len(row_keys) == len(ROWS)
|
|
assert len(row_keys) == len(table.data)
|
|
assert table.row_count == len(ROWS)
|
|
# Each key can be used to fetch a row from the DataTable
|
|
assert all(key in table.data for key in row_keys)
|
|
|
|
|
|
async def test_add_rows_user_defined_keys():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
key_a, key_b = table.add_columns("A", "B")
|
|
algernon_key = table.add_row(*ROWS[0], key="algernon")
|
|
table.add_row(*ROWS[1], key="charlie")
|
|
auto_key = table.add_row(*ROWS[2])
|
|
|
|
assert algernon_key == "algernon"
|
|
# We get a RowKey object back, but we can use our own string *or* this object
|
|
# to find the row we're looking for, they're considered equivalent for lookups.
|
|
assert isinstance(algernon_key, RowKey)
|
|
|
|
# Ensure the data in the table is mapped as expected
|
|
first_row = {key_a: ROWS[0][0], key_b: ROWS[0][1]}
|
|
assert table.data[algernon_key] == first_row
|
|
assert table.data["algernon"] == first_row
|
|
|
|
second_row = {key_a: ROWS[1][0], key_b: ROWS[1][1]}
|
|
assert table.data["charlie"] == second_row
|
|
|
|
third_row = {key_a: ROWS[2][0], key_b: ROWS[2][1]}
|
|
assert table.data[auto_key] == third_row
|
|
|
|
first_row = Row(algernon_key, height=1)
|
|
assert table.rows[algernon_key] == first_row
|
|
assert table.rows["algernon"] == first_row
|
|
|
|
|
|
async def test_add_column_with_width():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
column = table.add_column("ABC", width=10, key="ABC")
|
|
row = table.add_row("123")
|
|
assert table.get_cell_value(row, column) == "123"
|
|
assert table.columns[column].width == 10
|
|
assert table.columns[column].render_width == 12 # 10 + (2 padding)
|
|
|
|
|
|
async def test_add_columns():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
column_keys = table.add_columns("1", "2", "3")
|
|
assert len(column_keys) == 3
|
|
assert len(table.columns) == 3
|
|
|
|
|
|
async def test_add_columns_user_defined_keys():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
key = table.add_column("Column", key="donut")
|
|
assert key == "donut"
|
|
assert key == key
|
|
|
|
|
|
async def test_clear():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
assert table.cursor_coordinate == Coordinate(0, 0)
|
|
assert table.hover_coordinate == Coordinate(0, 0)
|
|
|
|
# Add some data and update cursor positions
|
|
table.add_column("Column0")
|
|
table.add_rows([["Row0"], ["Row1"], ["Row2"]])
|
|
table.cursor_coordinate = Coordinate(1, 0)
|
|
table.hover_coordinate = Coordinate(2, 0)
|
|
|
|
# Ensure the cursor positions are reset to origin on clear()
|
|
table.clear()
|
|
assert table.cursor_coordinate == Coordinate(0, 0)
|
|
assert table.hover_coordinate == Coordinate(0, 0)
|
|
|
|
# Ensure that the table has been cleared
|
|
assert table.data == {}
|
|
assert table.rows == {}
|
|
assert table.row_count == 0
|
|
assert len(table.columns) == 1
|
|
|
|
# Clearing the columns too
|
|
table.clear(columns=True)
|
|
assert len(table.columns) == 0
|
|
|
|
|
|
async def test_column_labels() -> None:
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
table.add_columns("1", "2", "3")
|
|
actual_labels = [col.label for col in table.columns.values()]
|
|
expected_labels = ["1", "2", "3"]
|
|
assert actual_labels == expected_labels
|
|
|
|
|
|
async def test_initial_column_widths() -> None:
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
foo, bar = table.add_columns("foo", "bar")
|
|
|
|
assert table.columns[foo].width == 3
|
|
assert table.columns[bar].width == 3
|
|
table.add_row("Hello", "World!")
|
|
await wait_for_idle()
|
|
assert table.columns[foo].content_width == 5
|
|
assert table.columns[bar].content_width == 6
|
|
|
|
table.add_row("Hello World!!!", "fo")
|
|
await wait_for_idle()
|
|
assert table.columns[foo].content_width == 14
|
|
assert table.columns[bar].content_width == 6
|
|
|
|
|
|
async def test_get_cell_value_returns_value_at_cell():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
table.add_column("Column1", key="C1")
|
|
table.add_row("TargetValue", key="R1")
|
|
assert table.get_cell_value("R1", "C1") == "TargetValue"
|
|
|
|
|
|
async def test_get_cell_value_invalid_row_key():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
table.add_column("Column1", key="C1")
|
|
table.add_row("TargetValue", key="R1")
|
|
with pytest.raises(CellDoesNotExist):
|
|
table.get_cell_value("INVALID_ROW", "C1")
|
|
|
|
|
|
async def test_get_cell_value_invalid_column_key():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
table.add_column("Column1", key="C1")
|
|
table.add_row("TargetValue", key="R1")
|
|
with pytest.raises(CellDoesNotExist):
|
|
table.get_cell_value("R1", "INVALID_COLUMN")
|
|
|
|
|
|
async def test_get_value_at_returns_value_at_cell():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
table.add_columns("A", "B")
|
|
table.add_rows(ROWS)
|
|
assert table.get_value_at(Coordinate(0, 0)) == "0/0"
|
|
|
|
|
|
async def test_get_value_at_exception():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
table.add_columns("A", "B")
|
|
table.add_rows(ROWS)
|
|
with pytest.raises(CellDoesNotExist):
|
|
table.get_value_at(Coordinate(9999, 0))
|
|
|
|
|
|
async def test_update_cell_cell_exists():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
table.add_column("A", key="A")
|
|
table.add_row("1", key="1")
|
|
table.update_cell("1", "A", "NEW_VALUE")
|
|
assert table.get_cell_value("1", "A") == "NEW_VALUE"
|
|
|
|
|
|
async def test_update_cell_cell_doesnt_exist():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
table.add_column("A", key="A")
|
|
table.add_row("1", key="1")
|
|
with pytest.raises(CellDoesNotExist):
|
|
table.update_cell("INVALID", "CELL", "Value")
|
|
|
|
|
|
async def test_update_coordinate_coordinate_exists():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
column_0, column_1 = table.add_columns("A", "B")
|
|
row_0, *_ = table.add_rows(ROWS)
|
|
|
|
table.update_coordinate(Coordinate(0, 1), "newvalue")
|
|
assert table.get_cell_value(row_0, column_1) == "newvalue"
|
|
|
|
|
|
async def test_update_coordinate_coordinate_doesnt_exist():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
table.add_columns("A", "B")
|
|
table.add_rows(ROWS)
|
|
with pytest.raises(CellDoesNotExist):
|
|
table.update_coordinate(Coordinate(999, 999), "newvalue")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"label,new_value,new_content_width",
|
|
[
|
|
# Shorter than initial cell value, larger than label => width remains same
|
|
("A", "BB", 3),
|
|
# Larger than initial cell value, shorter than label => width remains that of label
|
|
("1234567", "1234", 7),
|
|
# Shorter than initial cell value, shorter than label => width remains same
|
|
("12345", "123", 5),
|
|
# Larger than initial cell value, larger than label => width updates to new cell value
|
|
("12345", "123456789", 9),
|
|
],
|
|
)
|
|
async def test_update_coordinate_column_width(label, new_value, new_content_width):
|
|
# Initial cell values are length 3. Let's update cell content and ensure
|
|
# that the width of the column is correct given the new cell content widths
|
|
# and the label of the column the cell is in.
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
key, _ = table.add_columns(label, "Column2")
|
|
table.add_rows(ROWS)
|
|
first_column = table.columns.get(key)
|
|
|
|
table.update_coordinate(Coordinate(0, 0), new_value, update_width=True)
|
|
await wait_for_idle()
|
|
assert first_column.content_width == new_content_width
|
|
assert first_column.render_width == new_content_width + 2
|
|
|
|
|
|
async def test_coordinate_to_cell_key():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
column_key, _ = table.add_columns("Column0", "Column1")
|
|
row_key = table.add_row("A", "B")
|
|
|
|
cell_key = table.coordinate_to_cell_key(Coordinate(0, 0))
|
|
assert cell_key == CellKey(row_key, column_key)
|
|
|
|
|
|
async def test_coordinate_to_cell_key_invalid_coordinate():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
with pytest.raises(CellDoesNotExist):
|
|
table.coordinate_to_cell_key(Coordinate(9999, 9999))
|
|
|
|
|
|
async def test_datatable_on_click_cell_cursor():
|
|
app = DataTableApp()
|
|
async with app.run_test():
|
|
table = app.query_one(DataTable)
|
|
click = Click(
|
|
sender=app,
|
|
x=1,
|
|
y=1,
|
|
delta_x=3,
|
|
delta_y=3,
|
|
button=0,
|
|
shift=False,
|
|
meta=False,
|
|
ctrl=False,
|
|
)
|
|
table.add_column("ABC")
|
|
table.add_row("123")
|
|
table.on_click(event=click)
|
|
await wait_for_idle(0)
|
|
assert app.message_names == ["CellHighlighted", "CellSelected"]
|
|
|
|
|
|
def test_key_equals_equivalent_string():
|
|
text = "Hello"
|
|
key = RowKey(text)
|
|
assert key == text
|
|
assert hash(key) == hash(text)
|
|
|
|
|
|
def test_key_doesnt_match_non_equal_string():
|
|
key = ColumnKey("123")
|
|
text = "laksjdlaskjd"
|
|
assert key != text
|
|
assert hash(key) != hash(text)
|
|
|
|
|
|
def test_key_equals_self():
|
|
row_key = RowKey()
|
|
column_key = ColumnKey()
|
|
assert row_key == row_key
|
|
assert column_key == column_key
|
|
assert row_key != column_key
|
|
|
|
|
|
def test_key_string_lookup():
|
|
# Indirectly covered by other tests, but let's explicitly document
|
|
# in tests how we intend for the keys to work for cache lookups.
|
|
dictionary = {
|
|
"foo": "bar",
|
|
RowKey("hello"): "world",
|
|
}
|
|
assert dictionary["foo"] == "bar"
|
|
assert dictionary[RowKey("foo")] == "bar"
|
|
assert dictionary["hello"] == "world"
|
|
assert dictionary[RowKey("hello")] == "world"
|