Pilot - add hover and click methods (#1966)

* Delete redundant test file

* Sketch out pilot API improvements - signatures/docstrings

* Pilot click and hover

* Updating test to use new pilot hover method for DataTable

* hover and click methods for Pilot

* Update changelog

* Add docstring
This commit is contained in:
darrenburns
2023-03-07 15:19:23 +00:00
committed by GitHub
parent 6d23fdf4f1
commit b366d1d49c
5 changed files with 102 additions and 94 deletions

View File

@@ -23,13 +23,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `list_view` attribute to `ListView` events https://github.com/Textualize/textual/pull/1940
- Added `radio_set` attribute to `RadioSet` events https://github.com/Textualize/textual/pull/1940
- Added `switch` attribute to `Switch` events https://github.com/Textualize/textual/pull/1940
- Added `hover` and `click` methods to `Pilot` https://github.com/Textualize/textual/pull/1966
- Breaking change: Added `toggle_button` attribute to RadioButton and Checkbox events, replaces `input` https://github.com/Textualize/textual/pull/1940
- A percentage alpha can now be applied to a border https://github.com/Textualize/textual/issues/1863
- Added `Color.multiply_alpha`.
### Fixed
- Fixed bug that prevented app pilot to press some keys https://github.com/Textualize/textual/issues/1815
- Fixed bug that prevented pilot from pressing some keys https://github.com/Textualize/textual/issues/1815
- DataTable race condition that caused crash https://github.com/Textualize/textual/pull/1962
## [0.13.0] - 2023-03-02

View File

@@ -1,12 +1,37 @@
from __future__ import annotations
import asyncio
from typing import Generic
from typing import Any, Generic
import rich.repr
from ._wait import wait_for_idle
from .app import App, ReturnType
from .css.query import QueryType
from .events import Click, MouseDown, MouseMove, MouseUp
from .geometry import Offset
from .widget import Widget
def _get_mouse_message_arguments(
target: Widget, offset: Offset = Offset(), button: int = 0
) -> dict[str, Any]:
"""Get the arguments to pass into mouse messages for the click and hover methods."""
x, y = offset
click_x, click_y, _, _ = target.region.translate(offset)
message_arguments = {
"x": x,
"y": y,
"delta_x": 0,
"delta_y": 0,
"button": button,
"shift": False,
"meta": False,
"ctrl": False,
"screen_x": click_x,
"screen_y": click_y,
}
return message_arguments
@rich.repr.auto(angular=True)
@@ -34,6 +59,60 @@ class Pilot(Generic[ReturnType]):
if keys:
await self._app._press_keys(keys)
async def click(
self, selector: QueryType | None = None, offset: Offset = Offset()
) -> None:
"""Simulate clicking with the mouse.
Args:
selector: The widget that should be clicked. If None, then the click
will occur relative to the screen. Note that this simply causes
a click to occur at the location of the widget. If the widget is
currently hidden or obscured by another widget, then the click may
not land on it.
offset: The offset to click within the selected widget.
"""
app = self.app
screen = app.screen
if selector is not None:
target_widget = screen.query_one(selector)
else:
target_widget = screen
message_arguments = _get_mouse_message_arguments(
target_widget, offset, button=1
)
app.post_message(MouseDown(**message_arguments))
app.post_message(MouseUp(**message_arguments))
app.post_message(Click(**message_arguments))
await self.pause()
async def hover(
self, selector: QueryType | None = None, offset: Offset = Offset()
) -> None:
"""Simulate hovering with the mouse cursor.
Args:
selector: The widget that should be hovered. If None, then the click
will occur relative to the screen. Note that this simply causes
a hover to occur at the location of the widget. If the widget is
currently hidden or obscured by another widget, then the hover may
not land on it.
offset: The offset to hover over within the selected widget.
"""
app = self.app
screen = app.screen
if selector is not None:
target_widget = screen.query_one(selector)
else:
target_widget = screen
message_arguments = _get_mouse_message_arguments(
target_widget, offset, button=0
)
app.post_message(MouseMove(**message_arguments))
await self.pause()
async def pause(self, delay: float | None = None) -> None:
"""Insert a pause.

View File

@@ -1813,6 +1813,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
and column metadata from the segments present in the cells."""
self._set_hover_cursor(True)
meta = event.style.meta
if meta and self.show_cursor and self.cursor_type != "none":
try:
self.hover_coordinate = Coordinate(meta["row"], meta["column"])
@@ -1899,7 +1900,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
def on_click(self, event: events.Click) -> None:
self._set_hover_cursor(True)
meta = self.get_style_at(event.x, event.y).meta
meta = event.style.meta
if not meta:
return

View File

@@ -1,20 +0,0 @@
from textual import events
from textual.app import App
from textual.pilot import Pilot
def test_auto_pilot() -> None:
keys_pressed: list[str] = []
class TestApp(App):
def on_key(self, event: events.Key) -> None:
keys_pressed.append(event.key)
async def auto_pilot(pilot: Pilot) -> None:
await pilot.press("tab", *"foo")
await pilot.exit("bar")
app = TestApp()
result = app.run(headless=True, auto_pilot=auto_pilot)
assert result == "bar"
assert keys_pressed == ["tab", "f", "o", "o"]

View File

@@ -9,6 +9,7 @@ from textual.actions import SkipAction
from textual.app import App
from textual.coordinate import Coordinate
from textual.events import Click, MouseMove
from textual.geometry import Offset
from textual.message import Message
from textual.widgets import DataTable
from textual.widgets.data_table import (
@@ -555,32 +556,17 @@ async def test_coordinate_to_cell_key_invalid_coordinate():
table.coordinate_to_cell_key(Coordinate(9999, 9999))
def make_click_event():
return Click(
x=1,
y=2,
delta_x=0,
delta_y=0,
button=0,
shift=False,
meta=False,
ctrl=False,
)
async def test_datatable_on_click_cell_cursor():
async def test_datatable_click_cell_cursor():
"""When the cell cursor is used, and we click, we emit a CellHighlighted
*and* a CellSelected message for the cell that was clicked.
Regression test for https://github.com/Textualize/textual/issues/1723"""
app = DataTableApp()
async with app.run_test() as pilot:
table = app.query_one(DataTable)
click = make_click_event()
column_key = table.add_column("ABC")
table.add_row("123")
row_key = table.add_row("456")
table.on_click(event=click)
await pilot.pause()
await pilot.click(offset=Offset(1, 2))
# There's two CellHighlighted events since a cell is highlighted on initial load,
# then when we click, another cell is highlighted (and selected).
assert app.message_names == [
@@ -599,19 +585,17 @@ async def test_datatable_on_click_cell_cursor():
assert cell_selected_event.coordinate == Coordinate(1, 0)
async def test_on_click_row_cursor():
async def test_click_row_cursor():
"""When the row cursor is used, and we click, we emit a RowHighlighted
*and* a RowSelected message for the row that was clicked."""
app = DataTableApp()
async with app.run_test():
async with app.run_test() as pilot:
table = app.query_one(DataTable)
table.cursor_type = "row"
click = make_click_event()
table.add_column("ABC")
table.add_row("123")
row_key = table.add_row("456")
table.on_click(event=click)
await wait_for_idle(0)
await pilot.click(offset=Offset(1, 2))
assert app.message_names == ["RowHighlighted", "RowHighlighted", "RowSelected"]
row_highlighted: DataTable.RowHighlighted = app.messages[1]
@@ -624,19 +608,17 @@ async def test_on_click_row_cursor():
assert row_highlighted.cursor_row == 1
async def test_on_click_column_cursor():
async def test_click_column_cursor():
"""When the column cursor is used, and we click, we emit a ColumnHighlighted
*and* a ColumnSelected message for the column that was clicked."""
app = DataTableApp()
async with app.run_test():
async with app.run_test() as pilot:
table = app.query_one(DataTable)
table.cursor_type = "column"
column_key = table.add_column("ABC")
table.add_row("123")
table.add_row("456")
click = make_click_event()
table.on_click(event=click)
await wait_for_idle(0)
await pilot.click(offset=Offset(1, 2))
assert app.message_names == [
"ColumnHighlighted",
"ColumnHighlighted",
@@ -654,27 +636,14 @@ async def test_on_click_column_cursor():
async def test_hover_coordinate():
"""Ensure that the hover_coordinate reactive is updated as expected."""
app = DataTableApp()
async with app.run_test():
async with app.run_test() as pilot:
table = app.query_one(DataTable)
table.add_column("ABC")
table.add_row("123")
table.add_row("456")
assert table.hover_coordinate == Coordinate(0, 0)
mouse_move = MouseMove(
x=1,
y=2,
delta_x=0,
delta_y=0,
button=0,
shift=False,
meta=False,
ctrl=False,
style=Style(meta={"row": 1, "column": 2}),
)
table.on_mouse_move(mouse_move)
await wait_for_idle(0)
assert table.hover_coordinate == Coordinate(1, 2)
await pilot.hover(DataTable, offset=Offset(2, 2))
assert table.hover_coordinate == Coordinate(1, 0)
async def test_header_selected():
@@ -685,19 +654,9 @@ async def test_header_selected():
table = app.query_one(DataTable)
column_key = table.add_column("number")
table.add_row(3)
click_event = Click(
x=3,
y=0,
delta_x=0,
delta_y=0,
button=1,
shift=False,
meta=False,
ctrl=False,
)
await pilot.pause()
table.on_click(click_event)
await pilot.pause()
click_location = Offset(3, 0) # Click the header
await pilot.click(DataTable, offset=click_location)
message: DataTable.HeaderSelected = app.messages[-1]
assert message.label == Text("number")
assert message.column_index == 0
@@ -705,7 +664,7 @@ async def test_header_selected():
# Now hide the header and click in the exact same place - no additional message emitted.
table.show_header = False
table.on_click(click_event)
await pilot.click(DataTable, offset=click_location)
await pilot.pause()
assert app.message_names.count("HeaderSelected") == 1
@@ -718,19 +677,8 @@ async def test_row_label_selected():
table = app.query_one(DataTable)
table.add_column("number")
row_key = table.add_row(3, label="A")
click_event = Click(
x=1,
y=1,
delta_x=0,
delta_y=0,
button=1,
shift=False,
meta=False,
ctrl=False,
)
await pilot.pause()
table.on_click(click_event)
await pilot.pause()
await pilot.click(DataTable, offset=Offset(1, 1))
message: DataTable.RowLabelSelected = app.messages[-1]
assert message.label == Text("A")
assert message.row_index == 0
@@ -738,8 +686,7 @@ async def test_row_label_selected():
# Now hide the row label and click in the same place - no additional message emitted.
table.show_row_labels = False
table.on_click(click_event)
await pilot.pause()
await pilot.click(DataTable, offset=Offset(1, 1))
assert app.message_names.count("RowLabelSelected") == 1