Merge branch 'main' into screen-modes

This commit is contained in:
Rodrigo Girão Serrão
2023-05-18 15:11:21 +01:00
57 changed files with 5333 additions and 3443 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,33 @@
from textual.app import App
from textual.widgets import Header, Label, Footer
# Same as dock_scroll.py but with 2 labels
class TestApp(App):
BINDINGS = [("ctrl+q", "app.quit", "Quit")]
CSS = """
Label {
border: solid red;
}
Footer {
height: 4;
}
"""
def compose(self):
text = (
"this is a sample sentence and here are some words".replace(" ", "\n") * 2
)
yield Header()
yield Label(text)
yield Label(text)
yield Footer()
def on_mount(self):
self.dark = False
if __name__ == "__main__":
app = TestApp()
app.run()

View File

@@ -0,0 +1,17 @@
from textual.app import App, ComposeResult
from textual.widgets import Checkbox, Footer
class ScrollOffByOne(App):
def compose(self) -> ComposeResult:
for number in range(1, 100):
yield Checkbox(str(number))
yield Footer()
def on_mount(self) -> None:
self.query_one("Screen").scroll_end()
app = ScrollOffByOne()
if __name__ == "__main__":
app.run()

View File

@@ -19,6 +19,6 @@
#horizontal {
width: auto;
height: auto;
height: 4;
background: darkslateblue;
}
}

View File

@@ -0,0 +1,19 @@
from textual.app import App, ComposeResult
from textual.widgets import Checkbox, Footer
class ScrollOffByOne(App):
"""Scroll to item 50."""
def compose(self) -> ComposeResult:
for number in range(1, 100):
yield Checkbox(str(number), id=f"number-{number}")
yield Footer()
def on_ready(self) -> None:
self.query_one("#number-50").scroll_visible()
app = ScrollOffByOne()
if __name__ == "__main__":
app.run()

View File

@@ -0,0 +1,21 @@
"""Test https://github.com/Textualize/textual/issues/2557"""
from textual.app import App, ComposeResult
from textual.widgets import Select, Button
class SelectRebuildApp(App[None]):
def compose(self) -> ComposeResult:
yield Select[int]((("1", 1), ("2", 2)))
yield Button("Rebuild")
def on_button_pressed(self):
self.query_one(Select).set_options((
("This", 0), ("Should", 1), ("Be", 2),
("What", 3), ("Goes", 4), ("Into",5),
("The", 6), ("Snapshit", 7)
))
if __name__ == "__main__":
SelectRebuildApp().run()

View File

@@ -1,5 +1,4 @@
from pathlib import Path
import sys
import pytest
@@ -78,8 +77,7 @@ def test_switches(snap_compare):
def test_input_and_focus(snap_compare):
press = [
"tab",
*"Darren", # Focus first input, write "Darren"
*"Darren", # Write "Darren"
"tab",
*"Burns", # Focus second input, write "Burns"
]
@@ -88,7 +86,7 @@ def test_input_and_focus(snap_compare):
def test_buttons_render(snap_compare):
# Testing button rendering. We press tab to focus the first button too.
assert snap_compare(WIDGET_EXAMPLES_DIR / "button.py", press=["tab", "tab"])
assert snap_compare(WIDGET_EXAMPLES_DIR / "button.py", press=["tab"])
def test_placeholder_render(snap_compare):
@@ -189,7 +187,7 @@ def test_content_switcher_example_initial(snap_compare):
def test_content_switcher_example_switch(snap_compare):
assert snap_compare(
WIDGET_EXAMPLES_DIR / "content_switcher.py",
press=["tab", "tab", "enter", "wait:500"],
press=["tab", "enter", "wait:500"],
terminal_size=(50, 50),
)
@@ -203,9 +201,11 @@ def test_option_list(snap_compare):
assert snap_compare(WIDGET_EXAMPLES_DIR / "option_list_options.py")
assert snap_compare(WIDGET_EXAMPLES_DIR / "option_list_tables.py")
def test_option_list_build(snap_compare):
assert snap_compare(SNAPSHOT_APPS_DIR / "option_list.py")
def test_progress_bar_indeterminate(snap_compare):
assert snap_compare(WIDGET_EXAMPLES_DIR / "progress_bar_isolated_.py", press=["f"])
@@ -313,7 +313,7 @@ def test_programmatic_scrollbar_gutter_change(snap_compare):
def test_borders_preview(snap_compare):
assert snap_compare(CLI_PREVIEWS_DIR / "borders.py", press=["tab", "enter"])
assert snap_compare(CLI_PREVIEWS_DIR / "borders.py", press=["enter"])
def test_colors_preview(snap_compare):
@@ -377,9 +377,7 @@ def test_disabled_widgets(snap_compare):
def test_focus_component_class(snap_compare):
assert snap_compare(
SNAPSHOT_APPS_DIR / "focus_component_class.py", press=["tab", "tab"]
)
assert snap_compare(SNAPSHOT_APPS_DIR / "focus_component_class.py", press=["tab"])
def test_line_api_scrollbars(snap_compare):
@@ -440,7 +438,7 @@ def test_modal_dialog_bindings_input(snap_compare):
# Check https://github.com/Textualize/textual/issues/2194
assert snap_compare(
SNAPSHOT_APPS_DIR / "modal_screen_bindings.py",
press=["enter", "tab", "h", "!", "left", "i", "tab"],
press=["enter", "h", "!", "left", "i", "tab"],
)
@@ -457,6 +455,23 @@ def test_dock_scroll(snap_compare):
assert snap_compare(SNAPSHOT_APPS_DIR / "dock_scroll.py", terminal_size=(80, 25))
def test_dock_scroll2(snap_compare):
# https://github.com/Textualize/textual/issues/2525
assert snap_compare(SNAPSHOT_APPS_DIR / "dock_scroll2.py", terminal_size=(80, 25))
def test_dock_scroll_off_by_one(snap_compare):
# https://github.com/Textualize/textual/issues/2525
assert snap_compare(
SNAPSHOT_APPS_DIR / "dock_scroll_off_by_one.py", terminal_size=(80, 25)
)
def test_scroll_to(snap_compare):
# https://github.com/Textualize/textual/issues/2525
assert snap_compare(SNAPSHOT_APPS_DIR / "scroll_to.py", terminal_size=(80, 25))
def test_auto_fr(snap_compare):
# https://github.com/Textualize/textual/issues/2220
assert snap_compare(SNAPSHOT_APPS_DIR / "auto_fr.py", terminal_size=(80, 25))
@@ -493,3 +508,11 @@ def test_quickly_change_tabs(snap_compare):
def test_fr_unit_with_min(snap_compare):
# https://github.com/Textualize/textual/issues/2378
assert snap_compare(SNAPSHOT_APPS_DIR / "fr_with_min.py")
def test_select_rebuild(snap_compare):
# https://github.com/Textualize/textual/issues/2557
assert snap_compare(
SNAPSHOT_APPS_DIR / "select_rebuild.py",
press=["space", "escape", "tab", "enter", "tab", "space"],
)

View File

@@ -1,5 +1,5 @@
from textual.app import App, ComposeResult
from textual.widgets import Button
from textual.widgets import Button, Input
def test_batch_update():
@@ -20,6 +20,7 @@ def test_batch_update():
class MyApp(App):
def compose(self) -> ComposeResult:
yield Input()
yield Button("Click me!")

View File

@@ -36,7 +36,7 @@ async def test_on_button_pressed() -> None:
app = ButtonApp()
async with app.run_test() as pilot:
await pilot.press("tab", "enter", "tab", "enter", "tab", "enter")
await pilot.press("enter", "tab", "enter", "tab", "enter")
await pilot.pause()
assert pressed == [

View File

@@ -1,5 +1,6 @@
from textual import events
from textual.app import App
from textual.widgets import Input
async def test_paste_app():
@@ -16,3 +17,29 @@ async def test_paste_app():
assert len(paste_events) == 1
assert paste_events[0].text == "Hello"
async def test_empty_paste():
"""Regression test for https://github.com/Textualize/textual/issues/2563."""
paste_events = []
class MyInput(Input):
def on_paste(self, event):
super()._on_paste(event)
paste_events.append(event)
class PasteApp(App):
def compose(self):
yield MyInput()
def key_p(self):
self.query_one(MyInput).post_message(events.Paste(""))
app = PasteApp()
async with app.run_test() as pilot:
app.set_focus(None)
await pilot.press("p")
assert app.query_one(MyInput).value == ""
assert len(paste_events) == 1
assert paste_events[0].text == ""

View File

@@ -6,6 +6,7 @@ import pytest
from textual.app import App, ScreenStackError
from textual.screen import Screen
from textual.widgets import Button, Input, Label
skip_py310 = pytest.mark.skipif(
sys.version_info.minor == 10 and sys.version_info.major == 3,
@@ -150,3 +151,73 @@ async def test_screens():
screen2.remove()
screen3.remove()
await app._shutdown()
async def test_auto_focus():
class MyScreen(Screen[None]):
def compose(self):
yield Button()
yield Input(id="one")
yield Input(id="two")
class MyApp(App[None]):
pass
app = MyApp()
async with app.run_test():
await app.push_screen(MyScreen())
assert isinstance(app.focused, Button)
app.pop_screen()
MyScreen.AUTO_FOCUS = None
await app.push_screen(MyScreen())
assert app.focused is None
app.pop_screen()
MyScreen.AUTO_FOCUS = "Input"
await app.push_screen(MyScreen())
assert isinstance(app.focused, Input)
assert app.focused.id == "one"
app.pop_screen()
MyScreen.AUTO_FOCUS = "#two"
await app.push_screen(MyScreen())
assert isinstance(app.focused, Input)
assert app.focused.id == "two"
# If we push and pop another screen, focus should be preserved for #two.
MyScreen.AUTO_FOCUS = None
await app.push_screen(MyScreen())
assert app.focused is None
app.pop_screen()
assert app.focused.id == "two"
async def test_auto_focus_skips_non_focusable_widgets():
class MyScreen(Screen[None]):
def compose(self):
yield Label()
yield Button()
class MyApp(App[None]):
def on_mount(self):
self.push_screen(MyScreen())
app = MyApp()
async with app.run_test():
assert app.focused is not None
assert isinstance(app.focused, Button)
async def test_dismiss_non_top_screen():
class MyApp(App[None]):
async def key_p(self) -> None:
self.bottom, top = Screen(), Screen()
await self.push_screen(self.bottom)
await self.push_screen(top)
app = MyApp()
async with app.run_test() as pilot:
await pilot.press("p")
with pytest.raises(ScreenStackError):
app.bottom.dismiss()

View File

@@ -42,22 +42,18 @@ async def test_move_child_to_outside() -> None:
pilot.app.screen.move_child(child, before=Widget())
@pytest.mark.xfail(
strict=True, reason="https://github.com/Textualize/textual/issues/1743"
)
async def test_move_child_before_itself() -> None:
"""Test moving a widget before itself."""
async with App().run_test() as pilot:
child = Widget(Widget())
await pilot.app.mount(child)
pilot.app.screen.move_child(child, before=child)
@pytest.mark.xfail(
strict=True, reason="https://github.com/Textualize/textual/issues/1743"
)
async def test_move_child_after_itself() -> None:
"""Test moving a widget after itself."""
# Regression test for https://github.com/Textualize/textual/issues/1743
async with App().run_test() as pilot:
child = Widget(Widget())
await pilot.app.mount(child)

View File

@@ -39,6 +39,7 @@ async def test_radio_sets_initial_state():
async def test_click_sets_focus():
"""Clicking within a radio set should set focus."""
async with RadioSetApp().run_test() as pilot:
pilot.app.set_focus(None)
assert pilot.app.screen.focused is None
await pilot.click("#clickme")
assert pilot.app.screen.focused == pilot.app.query_one("#from_buttons")
@@ -72,8 +73,6 @@ async def test_radioset_same_button_mash():
async def test_radioset_inner_navigation():
"""Using the cursor keys should navigate between buttons in a set."""
async with RadioSetApp().run_test() as pilot:
assert pilot.app.screen.focused is None
await pilot.press("tab")
for key, landing in (
("down", 1),
("up", 0),
@@ -88,8 +87,6 @@ async def test_radioset_inner_navigation():
== pilot.app.query_one("#from_buttons").children[landing]
)
async with RadioSetApp().run_test() as pilot:
assert pilot.app.screen.focused is None
await pilot.press("tab")
assert pilot.app.screen.focused is pilot.app.screen.query_one("#from_buttons")
await pilot.press("tab")
assert pilot.app.screen.focused is pilot.app.screen.query_one("#from_strings")
@@ -101,8 +98,6 @@ async def test_radioset_inner_navigation():
async def test_radioset_breakout_navigation():
"""Shift/Tabbing while in a radioset should move to the previous/next focsuable after the set itself."""
async with RadioSetApp().run_test() as pilot:
assert pilot.app.screen.focused is None
await pilot.press("tab")
assert pilot.app.screen.focused is pilot.app.query_one("#from_buttons")
await pilot.press("tab")
assert pilot.app.screen.focused is pilot.app.query_one("#from_strings")