Merge branch 'main' into scroll-sensitivity

This commit is contained in:
Will McGugan
2023-01-30 15:59:49 +01:00
11 changed files with 63 additions and 198 deletions

41
src/textual/_wait.py Normal file
View File

@@ -0,0 +1,41 @@
from asyncio import sleep
from time import process_time, monotonic
SLEEP_GRANULARITY: float = 1 / 50
SLEEP_IDLE: float = SLEEP_GRANULARITY / 2.0
async def wait_for_idle(
min_sleep: float = SLEEP_GRANULARITY, max_sleep: float = 1
) -> None:
"""Wait until the process isn't working very hard.
This will compare wall clock time with process time, if the process time
is not advancing the same as wall clock time it means the process is in a
sleep state or waiting for input.
When the process is idle it suggests that input has been processes and the state
is predictable enough to test.
Args:
min_sleep: Minimum time to wait.
max_sleep: Maximum time to wait.
"""
start_time = monotonic()
while True:
cpu_time = process_time()
# Sleep for a predetermined amount of time
await sleep(SLEEP_GRANULARITY)
# Calculate the wall clock elapsed time and the process elapsed time
cpu_elapsed = process_time() - cpu_time
elapsed_time = monotonic() - start_time
# If we have slept the maximum, we can break
if elapsed_time >= max_sleep:
break
# If we have slept at least the minimum and the cpu elapsed is significantly less
# than wall clock, then we can assume the process has finished working for now
if elapsed_time > min_sleep and cpu_elapsed < SLEEP_IDLE:
break

View File

@@ -68,6 +68,7 @@ from .messages import CallbackType
from .reactive import Reactive
from .renderables.blank import Blank
from .screen import Screen
from ._wait import wait_for_idle
from .widget import AwaitMount, Widget
if TYPE_CHECKING:
@@ -801,11 +802,10 @@ class App(Generic[ReturnType], DOMNode):
app = self
driver = app._driver
assert driver is not None
await asyncio.sleep(0.02)
await wait_for_idle(0)
for key in keys:
if key == "_":
print("(pause 50ms)")
await asyncio.sleep(0.05)
continue
elif key.startswith("wait:"):
_, wait_ms = key.split(":")
print(f"(pause {wait_ms}ms)")
@@ -827,14 +827,10 @@ class App(Generic[ReturnType], DOMNode):
print(f"press {key!r} (char={char!r})")
key_event = events.Key(app, key, char)
driver.send_event(key_event)
# TODO: A bit of a fudge - extra sleep after tabbing to help guard against race
# condition between widget-level key handling and app/screen level handling.
# More information here: https://github.com/Textualize/textual/issues/1009
# This conditional sleep can be removed after that issue is closed.
if key == "tab":
await asyncio.sleep(0.05)
await asyncio.sleep(0.025)
await wait_for_idle(0)
await app._animator.wait_for_idle()
await wait_for_idle(0)
@asynccontextmanager
async def run_test(

View File

@@ -6,6 +6,7 @@ import asyncio
from typing import Generic
from .app import App, ReturnType
from ._wait import wait_for_idle
@rich.repr.auto(angular=True)
@@ -33,14 +34,17 @@ class Pilot(Generic[ReturnType]):
if keys:
await self._app._press_keys(keys)
async def pause(self, delay: float = 50 / 1000) -> None:
async def pause(self, delay: float | None = None) -> None:
"""Insert a pause.
Args:
delay: Seconds to pause. Defaults to 50ms.
delay: Seconds to pause, or None to wait for cpu idle.
"""
# These sleep zeros, are to force asyncio to give up a time-slice,
await asyncio.sleep(delay)
if delay is None:
await wait_for_idle(0)
else:
await asyncio.sleep(delay)
async def wait_for_animation(self) -> None:
"""Wait for any current animation to complete."""
@@ -56,4 +60,5 @@ class Pilot(Generic[ReturnType]):
Args:
result: The app result returned by `run` or `run_async`.
"""
await wait_for_idle()
self.app.exit(result)

View File

@@ -31,10 +31,8 @@ async def test_empty_inherited_list_view() -> None:
"""An empty self-populating inherited ListView should work as expected."""
async with ListViewApp().run_test() as pilot:
await pilot.press("tab")
await pilot.pause(2 / 100)
assert pilot.app.query_one(MyListView).index is None
await pilot.press("down")
await pilot.pause(2 / 100)
assert pilot.app.query_one(MyListView).index is None
@@ -42,8 +40,6 @@ async def test_populated_inherited_list_view() -> None:
"""A self-populating inherited ListView should work as normal."""
async with ListViewApp(30).run_test() as pilot:
await pilot.press("tab")
await pilot.pause(2 / 100)
assert pilot.app.query_one(MyListView).index == 0
await pilot.press("down")
await pilot.pause(2 / 100)
assert pilot.app.query_one(MyListView).index == 1

File diff suppressed because one or more lines are too long

View File

@@ -190,11 +190,6 @@ def test_demo(snap_compare):
"""Test the demo app (python -m textual)"""
assert snap_compare(
Path("../../src/textual/demo.py"),
press=["down", "down", "down", "_", "_", "_"],
press=["down", "down", "down"],
terminal_size=(100, 30),
)
def test_label_wrap(snap_compare):
"""Test Label wrapping with a Panel"""
assert snap_compare("snapshot_apps/label_wrap.py")

View File

@@ -14,7 +14,6 @@ def test_auto_pilot() -> None:
async def auto_pilot(pilot: Pilot) -> None:
await pilot.press("tab", *"foo")
await pilot.pause(1 / 100)
await pilot.exit("bar")
app = TestApp()

View File

@@ -238,7 +238,7 @@ async def test_pressing_alpha_on_app() -> None:
"""Test that pressing the alpha key, when it's bound on the app, results in an action fire."""
async with AppWithMovementKeysBound().run_test() as pilot:
await pilot.press(*AppKeyRecorder.ALPHAS)
await pilot.pause(2 / 100)
await pilot.pause()
assert pilot.app.pressed_keys == [*AppKeyRecorder.ALPHAS]
@@ -246,7 +246,7 @@ async def test_pressing_movement_keys_app() -> None:
"""Test that pressing the movement keys, when they're bound on the app, results in an action fire."""
async with AppWithMovementKeysBound().run_test() as pilot:
await pilot.press(*AppKeyRecorder.ALL_KEYS)
await pilot.pause(2 / 100)
await pilot.pause()
pilot.app.all_recorded()
@@ -284,7 +284,7 @@ async def test_focused_child_widget_with_movement_bindings() -> None:
"""A focused child widget with movement bindings should handle its own actions."""
async with AppWithWidgetWithBindings().run_test() as pilot:
await pilot.press(*AppKeyRecorder.ALL_KEYS)
await pilot.pause(2 / 100)
pilot.app.all_recorded("locally_")
@@ -331,7 +331,7 @@ async def test_focused_child_widget_with_movement_bindings_on_screen() -> None:
"""A focused child widget, with movement bindings in the screen, should trigger screen actions."""
async with AppWithScreenWithBindingsWidgetNoBindings().run_test() as pilot:
await pilot.press(*AppKeyRecorder.ALL_KEYS)
await pilot.pause(2 / 100)
pilot.app.all_recorded("screenly_")
@@ -374,7 +374,7 @@ async def test_contained_focused_child_widget_with_movement_bindings_on_screen()
"""A contained focused child widget, with movement bindings in the screen, should trigger screen actions."""
async with AppWithScreenWithBindingsWrappedWidgetNoBindings().run_test() as pilot:
await pilot.press(*AppKeyRecorder.ALL_KEYS)
await pilot.pause(2 / 100)
pilot.app.all_recorded("screenly_")
@@ -413,7 +413,7 @@ async def test_focused_child_widget_with_movement_bindings_no_inherit() -> None:
"""A focused child widget with movement bindings and inherit_bindings=False should handle its own actions."""
async with AppWithWidgetWithBindingsNoInherit().run_test() as pilot:
await pilot.press(*AppKeyRecorder.ALL_KEYS)
await pilot.pause(2 / 100)
pilot.app.all_recorded("locally_")
@@ -465,7 +465,7 @@ async def test_focused_child_widget_no_inherit_with_movement_bindings_on_screen(
"""A focused child widget, that doesn't inherit bindings, with movement bindings in the screen, should trigger screen actions."""
async with AppWithScreenWithBindingsWidgetNoBindingsNoInherit().run_test() as pilot:
await pilot.press(*AppKeyRecorder.ALL_KEYS)
await pilot.pause(2 / 100)
pilot.app.all_recorded("screenly_")
@@ -520,7 +520,6 @@ async def test_focused_child_widget_no_inherit_empty_bindings_with_movement_bind
"""A focused child widget, that doesn't inherit bindings and sets BINDINGS empty, with movement bindings in the screen, should trigger screen actions."""
async with AppWithScreenWithBindingsWidgetEmptyBindingsNoInherit().run_test() as pilot:
await pilot.press(*AppKeyRecorder.ALL_KEYS)
await pilot.pause(2 / 100)
pilot.app.all_recorded("screenly_")
@@ -602,7 +601,6 @@ async def test_overlapping_priority_bindings() -> None:
"""Test an app stack with overlapping bindings."""
async with PriorityOverlapApp().run_test() as pilot:
await pilot.press(*"0abcdef")
await pilot.pause(2 / 100)
assert pilot.app.pressed_keys == [
"widget_0",
"app_a",

View File

@@ -40,5 +40,4 @@ async def test_toggle_dark_in_action() -> None:
"""It should be possible to toggle dark mode with an action."""
async with OnMountDarkSwitch().run_test() as pilot:
await pilot.press("d")
await pilot.pause(2 / 100)
assert not pilot.app.dark

View File

@@ -16,7 +16,6 @@ async def test_run_test() -> None:
str(pilot) == "<Pilot app=TestApp(title='TestApp', classes={'-dark-mode'})>"
)
await pilot.press("tab", *"foo")
await pilot.pause(1 / 100)
await pilot.exit("bar")
assert app.return_value == "bar"

View File

@@ -45,7 +45,6 @@ async def test_tree_node_selected_message() -> None:
"""Selecting a node should result in a selected message being emitted."""
async with TreeApp().run_test() as pilot:
await pilot.press("enter")
await pilot.pause(2 / 100)
assert pilot.app.messages == ["NodeExpanded", "NodeSelected"]
@@ -53,7 +52,6 @@ async def test_tree_node_expanded_message() -> None:
"""Expanding a node should result in an expanded message being emitted."""
async with TreeApp().run_test() as pilot:
await pilot.press("enter")
await pilot.pause(2 / 100)
assert pilot.app.messages == ["NodeExpanded", "NodeSelected"]
@@ -61,7 +59,6 @@ async def test_tree_node_collapsed_message() -> None:
"""Collapsing a node should result in a collapsed message being emitted."""
async with TreeApp().run_test() as pilot:
await pilot.press("enter", "enter")
await pilot.pause(2 / 100)
assert pilot.app.messages == [
"NodeExpanded",
"NodeSelected",
@@ -74,5 +71,4 @@ async def test_tree_node_highlighted_message() -> None:
"""Highlighting a node should result in a highlighted message being emitted."""
async with TreeApp().run_test() as pilot:
await pilot.press("enter", "down")
await pilot.pause(2 / 100)
assert pilot.app.messages == ["NodeExpanded", "NodeSelected", "NodeHighlighted"]