mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
513 lines
16 KiB
Python
513 lines
16 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from textual import on
|
|
from textual.app import App, ComposeResult
|
|
from textual.widgets import Tab, Tabs
|
|
from textual.widgets._tabs import Underline
|
|
|
|
|
|
async def test_tab_label():
|
|
"""It should be possible to access a tab's label."""
|
|
assert Tab("Pilot").label_text == "Pilot"
|
|
|
|
|
|
async def test_tab_relabel():
|
|
"""It should be possible to relabel a tab."""
|
|
tab = Tab("Pilot")
|
|
assert tab.label_text == "Pilot"
|
|
tab.label = "Aeryn"
|
|
assert tab.label_text == "Aeryn"
|
|
|
|
|
|
async def test_compose_empty_tabs():
|
|
"""It should be possible to create an empty Tabs."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs()
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
assert pilot.app.query_one(Tabs).tab_count == 0
|
|
assert pilot.app.query_one(Tabs).active_tab is None
|
|
|
|
|
|
async def test_compose_tabs_from_strings():
|
|
"""It should be possible to create a Tabs from some strings."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs("John", "Aeryn", "Moya", "Pilot")
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 4
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
|
|
|
|
async def test_compose_tabs_from_tabs():
|
|
"""It should be possible to create a Tabs from some Tabs."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs(
|
|
Tab("John"),
|
|
Tab("Aeryn"),
|
|
Tab("Moya"),
|
|
Tab("Pilot"),
|
|
)
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 4
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
|
|
|
|
async def test_add_tabs_later():
|
|
"""It should be possible to add tabs later on in the app's cycle."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs()
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 0
|
|
assert tabs.active_tab is None
|
|
await tabs.add_tab("John")
|
|
assert tabs.tab_count == 1
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
await tabs.add_tab("Aeryn")
|
|
assert tabs.tab_count == 2
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
|
|
|
|
async def test_add_tab_before():
|
|
"""It should be possible to add a tab before another tab."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs("Pilot")
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 1
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == "tab-1"
|
|
await tabs.add_tab("John", before="tab-1")
|
|
assert tabs.tab_count == 2
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == "tab-1"
|
|
await tabs.add_tab("John", before=tabs.active_tab)
|
|
assert tabs.tab_count == 3
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == "tab-1"
|
|
|
|
|
|
async def test_add_tab_before_badly():
|
|
"""Test exceptions from badly adding a tab before another."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs("Pilot")
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 1
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == "tab-1"
|
|
with pytest.raises(Tabs.TabError):
|
|
tabs.add_tab("John", before="this-is-not-a-tab")
|
|
assert tabs.tab_count == 1
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == "tab-1"
|
|
with pytest.raises(Tabs.TabError):
|
|
tabs.add_tab("John", before=Tab("I just made this up"))
|
|
assert tabs.tab_count == 1
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == "tab-1"
|
|
|
|
|
|
async def test_add_tab_after():
|
|
"""It should be possible to add a tab after another tab."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs("Pilot")
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 1
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == "tab-1"
|
|
await tabs.add_tab("John", after="tab-1")
|
|
assert tabs.tab_count == 2
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == "tab-1"
|
|
await tabs.add_tab("John", after=tabs.active_tab)
|
|
assert tabs.tab_count == 3
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == "tab-1"
|
|
|
|
|
|
async def test_add_tab_after_badly():
|
|
"""Test exceptions from badly adding a tab after another."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs("Pilot")
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 1
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == "tab-1"
|
|
with pytest.raises(Tabs.TabError):
|
|
tabs.add_tab("John", after="this-is-not-a-tab")
|
|
assert tabs.tab_count == 1
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == "tab-1"
|
|
with pytest.raises(Tabs.TabError):
|
|
tabs.add_tab("John", after=Tab("I just made this up"))
|
|
assert tabs.tab_count == 1
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == "tab-1"
|
|
|
|
|
|
async def test_add_tab_before_and_after():
|
|
"""Attempting to add a tab before and after another is an error."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs("Pilot")
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 1
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == "tab-1"
|
|
with pytest.raises(Tabs.TabError):
|
|
tabs.add_tab("John", before="tab-1", after="tab-1")
|
|
|
|
|
|
async def test_remove_tabs():
|
|
"""It should be possible to remove tabs."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs("John", "Aeryn", "Moya", "Pilot")
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 4
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
|
|
await tabs.remove_tab("tab-1")
|
|
assert tabs.tab_count == 3
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-2"
|
|
|
|
await tabs.remove_tab(tabs.query_one("#tab-2", Tab))
|
|
assert tabs.tab_count == 2
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-3"
|
|
|
|
await tabs.remove_tab("tab-does-not-exist")
|
|
assert tabs.tab_count == 2
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-3"
|
|
|
|
await tabs.remove_tab(None)
|
|
assert tabs.tab_count == 2
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-3"
|
|
|
|
await tabs.remove_tab("tab-3")
|
|
await tabs.remove_tab("tab-4")
|
|
assert tabs.tab_count == 0
|
|
assert tabs.active_tab is None
|
|
|
|
|
|
async def test_remove_tabs_reversed():
|
|
"""It should be possible to remove tabs."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs("John", "Aeryn", "Moya", "Pilot")
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 4
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
|
|
await tabs.remove_tab("tab-4")
|
|
assert tabs.tab_count == 3
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
|
|
await tabs.remove_tab("tab-3")
|
|
assert tabs.tab_count == 2
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
|
|
await tabs.remove_tab("tab-2")
|
|
assert tabs.tab_count == 1
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
|
|
await tabs.remove_tab("tab-1")
|
|
assert tabs.tab_count == 0
|
|
assert tabs.active_tab is None
|
|
|
|
|
|
async def test_clear_tabs():
|
|
"""It should be possible to clear all tabs."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs("John", "Aeryn", "Moya", "Pilot")
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 4
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
await tabs.clear()
|
|
assert tabs.tab_count == 0
|
|
assert tabs.active_tab is None
|
|
|
|
|
|
async def test_change_active_from_code():
|
|
"""It should be possible to change the active tab from code.."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs("John", "Aeryn", "Moya", "Pilot")
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 4
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == tabs.active_tab.id
|
|
|
|
tabs.active = "tab-2"
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-2"
|
|
assert tabs.active == tabs.active_tab.id
|
|
|
|
tabs.active = ""
|
|
assert tabs.active_tab is None
|
|
|
|
|
|
async def test_navigate_tabs_with_keyboard():
|
|
"""It should be possible to navigate tabs with the keyboard."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs("John", "Aeryn", "Moya", "Pilot")
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 4
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == tabs.active_tab.id
|
|
|
|
await pilot.press("right")
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-2"
|
|
assert tabs.active == tabs.active_tab.id
|
|
|
|
await pilot.press("left")
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == tabs.active_tab.id
|
|
|
|
await pilot.press(*(["left"] * tabs.tab_count))
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
assert tabs.active == tabs.active_tab.id
|
|
|
|
|
|
async def test_navigate_empty_tabs_with_keyboard():
|
|
"""It should be possible to navigate an empty tabs with the keyboard."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs()
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 0
|
|
assert tabs.active_tab is None
|
|
assert tabs.active == ""
|
|
|
|
await pilot.press("right")
|
|
assert tabs.active_tab is None
|
|
assert tabs.active == ""
|
|
|
|
await pilot.press("left")
|
|
assert tabs.active_tab is None
|
|
assert tabs.active == ""
|
|
|
|
|
|
async def test_navigate_tabs_with_mouse():
|
|
"""It should be possible to navigate tabs with the mouse."""
|
|
|
|
class TabsApp(App[None]):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs("John", "Aeryn", "Moya", "Pilot")
|
|
|
|
async with TabsApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.tab_count == 4
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
|
|
await pilot.click("#tab-2")
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-2"
|
|
|
|
await pilot.click("Underline", offset=(2, 0))
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "tab-1"
|
|
|
|
|
|
class TabsMessageCatchApp(App[None]):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.intended_handlers: list[str] = []
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs("John", "Aeryn", "Moya", "Pilot")
|
|
|
|
@on(Tabs.Cleared)
|
|
@on(Tabs.TabActivated)
|
|
@on(Underline.Clicked)
|
|
@on(Tab.Clicked)
|
|
def log_message(
|
|
self, event: Tabs.Cleared | Tabs.TabActivated | Underline.Clicked | Tab.Clicked
|
|
) -> None:
|
|
self.intended_handlers.append(event.handler_name)
|
|
|
|
@on(Tabs.TabActivated)
|
|
@on(Tabs.Cleared)
|
|
def check_control(self, event: Tabs.TabActivated) -> None:
|
|
assert event.control is event.tabs
|
|
|
|
|
|
async def test_startup_messages():
|
|
"""On startup there should be a tab activated message."""
|
|
async with TabsMessageCatchApp().run_test() as pilot:
|
|
assert pilot.app.intended_handlers == ["on_tabs_tab_activated"]
|
|
|
|
|
|
async def test_change_tab_with_code_messages():
|
|
"""Changing tab in code should result in an activated tab message."""
|
|
async with TabsMessageCatchApp().run_test() as pilot:
|
|
pilot.app.query_one(Tabs).active = "tab-2"
|
|
await pilot.pause()
|
|
assert pilot.app.intended_handlers == [
|
|
"on_tabs_tab_activated",
|
|
"on_tabs_tab_activated",
|
|
]
|
|
|
|
|
|
async def test_remove_tabs_messages():
|
|
"""Removing tabs should result in various messages."""
|
|
async with TabsMessageCatchApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
for n in range(4):
|
|
await tabs.remove_tab(f"tab-{n+1}")
|
|
|
|
await pilot.pause()
|
|
assert pilot.app.intended_handlers == [
|
|
"on_tabs_tab_activated",
|
|
"on_tabs_tab_activated",
|
|
"on_tabs_tab_activated",
|
|
"on_tabs_tab_activated",
|
|
"on_tabs_cleared",
|
|
]
|
|
|
|
|
|
async def test_reverse_remove_tabs_messages():
|
|
"""Removing tabs should result in various messages."""
|
|
async with TabsMessageCatchApp().run_test() as pilot:
|
|
tabs = pilot.app.query_one(Tabs)
|
|
for n in reversed(range(4)):
|
|
await tabs.remove_tab(f"tab-{n+1}")
|
|
|
|
await pilot.pause()
|
|
assert pilot.app.intended_handlers == [
|
|
"on_tabs_tab_activated",
|
|
"on_tabs_cleared",
|
|
]
|
|
|
|
|
|
async def test_keyboard_navigation_messages():
|
|
"""Keyboard navigation should result in the expected messages."""
|
|
async with TabsMessageCatchApp().run_test() as pilot:
|
|
await pilot.press("right")
|
|
await pilot.pause()
|
|
await pilot.press("left")
|
|
await pilot.pause()
|
|
assert pilot.app.intended_handlers == [
|
|
"on_tabs_tab_activated",
|
|
"on_tabs_tab_activated",
|
|
"on_tabs_tab_activated",
|
|
]
|
|
|
|
|
|
async def test_mouse_navigation_messages():
|
|
"""Mouse navigation should result in the expected messages."""
|
|
async with TabsMessageCatchApp().run_test() as pilot:
|
|
await pilot.click("#tab-2")
|
|
await pilot.pause()
|
|
await pilot.click("Underline", offset=(2, 0))
|
|
await pilot.pause()
|
|
assert pilot.app.intended_handlers == [
|
|
"on_tabs_tab_activated",
|
|
"on_tabs_tab_activated",
|
|
"on_tabs_tab_activated",
|
|
]
|
|
|
|
|
|
async def test_disabled_tab_is_not_activated_by_clicking_underline():
|
|
"""Regression test for https://github.com/Textualize/textual/issues/4701"""
|
|
|
|
class DisabledTabApp(App):
|
|
def compose(self) -> ComposeResult:
|
|
yield Tabs(
|
|
Tab("Enabled", id="enabled"),
|
|
Tab("Disabled", id="disabled", disabled=True),
|
|
)
|
|
|
|
app = DisabledTabApp()
|
|
async with app.run_test() as pilot:
|
|
# Click the underline beneath the disabled tab
|
|
await pilot.click(Tabs, offset=(14, 2))
|
|
tabs = pilot.app.query_one(Tabs)
|
|
assert tabs.active_tab is not None
|
|
assert tabs.active_tab.id == "enabled"
|