diff --git a/CHANGELOG.md b/CHANGELOG.md index f1485e47d7..1854c24bc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/textual/pilot.py b/src/textual/pilot.py index 9ce2375be8..63f3ac3db0 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -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, @@ -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. @@ -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, @@ -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, @@ -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. @@ -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, @@ -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, @@ -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. @@ -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, @@ -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. @@ -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: @@ -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, @@ -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. @@ -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, @@ -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. diff --git a/src/textual/widgets/_radio_button.py b/src/textual/widgets/_radio_button.py index b22aadeadd..f1ec037a15 100644 --- a/src/textual/widgets/_radio_button.py +++ b/src/textual/widgets/_radio_button.py @@ -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): diff --git a/tests/test_pilot.py b/tests/test_pilot.py index 52a759416f..008d711389 100644 --- a/tests/test_pilot.py +++ b/tests/test_pilot.py @@ -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