Files
textual/tests/test_tabs.py
Darren Burns 51da5cfcdb Testing fixes
2024-10-30 14:52:23 +00:00

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"