Skip to content

Commit

Permalink
Merge pull request #5095 from Textualize/pilot-widget
Browse files Browse the repository at this point in the history
Pilot takes object
  • Loading branch information
willmcgugan authored Oct 7, 2024
2 parents ded24b1 + bd24588 commit d7eae41
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- `Screen.ALLOW_IN_MAXIMIZED_VIEW` will now default to `App.ALLOW_IN_MAXIMIZED_VIEW` https://github.com/Textualize/textual/pull/5088
- Widgets matching `.-textual-system` will now be included in the maximize view by default https://github.com/Textualize/textual/pull/5088
- Digits are now thin by default, style with text-style: bold to get bold digits https://github.com/Textualize/textual/pull/5094
- `Pilot.click` and friends will now accept a widget, in addition to a selector https://github.com/Textualize/textual/pull/5095

### Added

Expand Down
53 changes: 27 additions & 26 deletions src/textual/pilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ async def resize_terminal(self, width: int, height: int) -> None:

async def mouse_down(
self,
selector: type[Widget] | str | None = None,
widget: Widget | type[Widget] | str | None = None,
offset: tuple[int, int] = (0, 0),
shift: bool = False,
meta: bool = False,
Expand All @@ -110,12 +110,12 @@ async def mouse_down(
the offset specified and it must be within the visible area of the screen.
Args:
selector: A selector to specify a widget that should be used as the reference
widget: A widget or selector used as an origin
for the event offset. If this is not specified, the offset is interpreted
relative to the screen. You can use this parameter to try to target a
specific widget. However, if the widget is currently hidden or obscured by
another widget, the event may not land on the widget you specified.
offset: The offset for the event. The offset is relative to the selector
offset: The offset for the event. The offset is relative to the selector / widget
provided or to the screen, if no selector is provided.
shift: Simulate the event with the shift key held down.
meta: Simulate the event with the meta key held down.
Expand All @@ -131,7 +131,7 @@ async def mouse_down(
try:
return await self._post_mouse_events(
[MouseDown],
selector=selector,
widget=widget,
offset=offset,
button=1,
shift=shift,
Expand All @@ -143,7 +143,7 @@ async def mouse_down(

async def mouse_up(
self,
selector: type[Widget] | str | None = None,
widget: Widget | type[Widget] | str | None = None,
offset: tuple[int, int] = (0, 0),
shift: bool = False,
meta: bool = False,
Expand All @@ -155,12 +155,12 @@ async def mouse_up(
the offset specified and it must be within the visible area of the screen.
Args:
selector: A selector to specify a widget that should be used as the reference
widget: A widget or selector used as an origin
for the event offset. If this is not specified, the offset is interpreted
relative to the screen. You can use this parameter to try to target a
specific widget. However, if the widget is currently hidden or obscured by
another widget, the event may not land on the widget you specified.
offset: The offset for the event. The offset is relative to the selector
offset: The offset for the event. The offset is relative to the widget / selector
provided or to the screen, if no selector is provided.
shift: Simulate the event with the shift key held down.
meta: Simulate the event with the meta key held down.
Expand All @@ -176,7 +176,7 @@ async def mouse_up(
try:
return await self._post_mouse_events(
[MouseUp],
selector=selector,
widget=widget,
offset=offset,
button=1,
shift=shift,
Expand All @@ -188,7 +188,7 @@ async def mouse_up(

async def click(
self,
selector: type[Widget] | str | None = None,
widget: Widget | type[Widget] | str | None = None,
offset: tuple[int, int] = (0, 0),
shift: bool = False,
meta: bool = False,
Expand All @@ -207,12 +207,12 @@ async def click(
```
Args:
selector: A selector to specify a widget that should be used as the reference
widget: A widget or selector used as an origin
for the click offset. If this is not specified, the offset is interpreted
relative to the screen. You can use this parameter to try to click on a
specific widget. However, if the widget is currently hidden or obscured by
another widget, the click may not land on the widget you specified.
offset: The offset to click. The offset is relative to the selector provided
offset: The offset to click. The offset is relative to the widget / selector provided
or to the screen, if no selector is provided.
shift: Click with the shift key held down.
meta: Click with the meta key held down.
Expand All @@ -228,7 +228,7 @@ async def click(
try:
return await self._post_mouse_events(
[MouseDown, MouseUp, Click],
selector=selector,
widget=widget,
offset=offset,
button=1,
shift=shift,
Expand All @@ -240,7 +240,7 @@ async def click(

async def hover(
self,
selector: type[Widget] | str | None | None = None,
widget: Widget | type[Widget] | str | None | None = None,
offset: tuple[int, int] = (0, 0),
) -> bool:
"""Simulate hovering with the mouse cursor at a specified position.
Expand All @@ -249,12 +249,12 @@ async def hover(
the offset specified and it must be within the visible area of the screen.
Args:
selector: A selector to specify a widget that should be used as the reference
widget: A widget or selector used as an origin
for the hover offset. If this is not specified, the offset is interpreted
relative to the screen. You can use this parameter to try to hover a
specific widget. However, if the widget is currently hidden or obscured by
another widget, the hover may not land on the widget you specified.
offset: The offset to hover. The offset is relative to the selector provided
offset: The offset to hover. The offset is relative to the widget / selector provided
or to the screen, if no selector is provided.
Raises:
Expand All @@ -268,16 +268,14 @@ async def hover(
# "settle" before moving it to the new hover position.
await self.pause()
try:
return await self._post_mouse_events(
[MouseMove], selector, offset, button=0
)
return await self._post_mouse_events([MouseMove], widget, offset, button=0)
except OutOfBounds as error:
raise error from None

async def _post_mouse_events(
self,
events: list[type[MouseEvent]],
selector: type[Widget] | str | None | None = None,
widget: Widget | type[Widget] | str | None | None = None,
offset: tuple[int, int] = (0, 0),
button: int = 0,
shift: bool = False,
Expand All @@ -293,12 +291,12 @@ async def _post_mouse_events(
functions that the pilot exposes.
Args:
selector: A selector to specify a widget that should be used as the reference
for the events offset. If this is not specified, the offset is interpreted
widget: A widget or selector used as the origin
for the event's offset. If this is not specified, the offset is interpreted
relative to the screen. You can use this parameter to try to target a
specific widget. However, if the widget is currently hidden or obscured by
another widget, the events may not land on the widget you specified.
offset: The offset for the events. The offset is relative to the selector
offset: The offset for the events. The offset is relative to the widget / selector
provided or to the screen, if no selector is provided.
shift: Simulate the events with the shift key held down.
meta: Simulate the events with the meta key held down.
Expand All @@ -313,10 +311,13 @@ async def _post_mouse_events(
"""
app = self.app
screen = app.screen
if selector is not None:
target_widget = app.query_one(selector)
else:
target_widget: Widget
if widget is None:
target_widget = screen
elif isinstance(widget, Widget):
target_widget = widget
else:
target_widget = app.query_one(widget)

message_arguments = _get_mouse_message_arguments(
target_widget,
Expand Down Expand Up @@ -351,7 +352,7 @@ async def _post_mouse_events(
app.screen._forward_event(event)
await self.pause()

return selector is None or widget_at is target_widget
return widget is None or widget_at is target_widget

async def _wait_for_screen(self, timeout: float = 30.0) -> bool:
"""Wait for the current screen and its children to have processed all pending events.
Expand Down
2 changes: 1 addition & 1 deletion src/textual/widgets/_radio_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class RadioButton(ToggleButton):
A `RadioButton` is best used within a [RadioSet][textual.widgets.RadioSet].
"""

BUTTON_INNER = "\u25CF"
BUTTON_INNER = "\u25cf"
"""The character used for the inside of the button."""

class Changed(ToggleButton.Changed):
Expand Down
17 changes: 17 additions & 0 deletions tests/test_pilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,20 @@ class MyApp(App):
with pytest.raises(StylesheetError):
async with app.run_test() as pilot:
await pilot.press("enter")


async def test_click_by_widget():
"""Test that click accept a Widget instance."""
pressed = False

class TestApp(CenteredButtonApp):
def on_button_pressed(self):
nonlocal pressed
pressed = True

app = TestApp()
async with app.run_test() as pilot:
button = app.query_one(Button)
assert not pressed
await pilot.click(button)
assert pressed

0 comments on commit d7eae41

Please sign in to comment.