Files
MyFastHtml/tests/controls/test_tabsmanager.py

757 lines
30 KiB
Python

import shutil
import pytest
from fasthtml.common import Div, Span
from myfasthtml.controls.TabsManager import TabsManager
from myfasthtml.controls.VisNetwork import VisNetwork
from myfasthtml.core.instances import InstancesManager
from myfasthtml.test.matcher import matches, find_one, find, Contains, TestIcon, TestScript, TestObject, DoesNotContain, \
And, TestIconNotStr
@pytest.fixture()
def tabs_manager(root_instance):
"""Create a fresh TabsManager instance for each test."""
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
yield TabsManager(root_instance)
InstancesManager.reset()
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
class TestTabsManagerBehaviour:
"""Tests for TabsManager behavior and logic."""
# =========================================================================
# Initialization
# =========================================================================
def test_i_can_create_tabs_manager(self, root_instance):
"""Test that TabsManager can be created with default state."""
tm = TabsManager(root_instance)
assert tm is not None
assert tm.get_state().tabs == {}
assert tm.get_state().tabs_order == []
assert tm.get_state().active_tab is None
assert tm.get_state().ns_tabs_content == {}
assert tm.get_state().ns_tabs_sent_to_client == set()
assert tm._tab_count == 0
# =========================================================================
# Tab Creation
# =========================================================================
def test_i_can_create_tab_with_simple_component(self, tabs_manager):
"""Test creating a tab with a simple Div component."""
tab_id = tabs_manager.create_tab("Tab1", Div("Content 1"))
assert tab_id is not None
assert tab_id in tabs_manager.get_state().tabs
assert tabs_manager.get_state().tabs[tab_id]["label"] == "Tab1"
assert tabs_manager.get_state().tabs[tab_id]["id"] == tab_id
assert tabs_manager.get_state().tabs[tab_id]["component_type"] is None
assert tabs_manager.get_state().tabs[tab_id]["component_id"] is None
assert tabs_manager.get_state().tabs_order == [tab_id]
assert tabs_manager.get_state().active_tab == tab_id
def test_i_can_create_tab_with_base_instance(self, tabs_manager):
"""Test creating a tab with a BaseInstance component (VisNetwork)."""
vis_network = VisNetwork(tabs_manager, nodes=[], edges=[])
tab_id = tabs_manager.create_tab("Network", vis_network)
assert tab_id is not None
assert tabs_manager.get_state().tabs[tab_id]["component_type"] == vis_network.get_prefix()
assert tabs_manager.get_state().tabs[tab_id]["component_id"] == vis_network.get_id()
def test_i_can_create_multiple_tabs(self, tabs_manager):
"""Test creating multiple tabs maintains correct order and activation."""
tab_id1 = tabs_manager.create_tab("Users", Div("Content 1"))
tab_id2 = tabs_manager.create_tab("User2", Div("Content 2"))
assert len(tabs_manager.get_state().tabs) == 2
assert tabs_manager.get_state().tabs_order == [tab_id1, tab_id2]
assert tabs_manager.get_state().active_tab == tab_id2
def test_created_tab_is_activated_by_default(self, tabs_manager):
"""Test that newly created tab becomes active by default."""
tab_id = tabs_manager.create_tab("Tab1", Div("Content 1"))
assert tabs_manager.get_state().active_tab == tab_id
def test_i_can_create_tab_without_activating(self, tabs_manager):
"""Test creating a tab without activating it."""
tab_id1 = tabs_manager.create_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.create_tab("Tab2", Div("Content 2"), activate=False)
assert tab_id2 in tabs_manager.get_state().tabs
assert tabs_manager.get_state().active_tab == tab_id1
# =========================================================================
# Tab Activation
# =========================================================================
def test_i_can_show_existing_tab(self, tabs_manager):
"""Test showing an existing tab activates it."""
tab_id1 = tabs_manager.create_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.create_tab("Tab2", Div("Content 2"))
assert tabs_manager.get_state().active_tab == tab_id2
tabs_manager.show_tab(tab_id1)
assert tabs_manager.get_state().active_tab == tab_id1
def test_i_can_show_tab_without_activating(self, tabs_manager):
"""Test showing a tab without changing active tab."""
tab_id1 = tabs_manager.create_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.create_tab("Tab2", Div("Content 2"))
assert tabs_manager.get_state().active_tab == tab_id2
tabs_manager.show_tab(tab_id1, activate=False)
assert tabs_manager.get_state().active_tab == tab_id2
def test_show_tab_returns_controller_only_when_already_sent(self, tabs_manager):
"""Test that show_tab returns only controller when tab was already sent to client."""
tab_id = tabs_manager.create_tab("Tab1", Div("Content 1"))
# First call: tab not sent yet, returns controller + header + content
result1 = tabs_manager.show_tab(tab_id, oob=False)
assert len(result1) == 3 # controller, header, wrapped_content
assert tab_id in tabs_manager.get_state().ns_tabs_sent_to_client
# Second call: tab already sent, returns only controller
result2 = tabs_manager.show_tab(tab_id, oob=False)
assert result2 is not None
# Result2 is a single Div (controller), not a tuple
assert hasattr(result2, 'tag') and result2.tag == 'div'
def test_i_cannot_show_nonexistent_tab(self, tabs_manager):
"""Test that showing a nonexistent tab returns None."""
result = tabs_manager.show_tab("nonexistent_id")
assert result is None
# =========================================================================
# Tab Closing
# =========================================================================
def test_i_can_close_tab(self, tabs_manager):
"""Test closing a tab removes it from state."""
tab_id1 = tabs_manager.create_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.create_tab("Tab2", Div("Content 2"))
tab_id3 = tabs_manager.create_tab("Tab3", Div("Content 3"))
tabs_manager.close_tab(tab_id2)
assert len(tabs_manager.get_state().tabs) == 2
assert tab_id2 not in tabs_manager.get_state().tabs
assert tabs_manager.get_state().tabs_order == [tab_id1, tab_id3]
assert tabs_manager.get_state().active_tab == tab_id3
def test_closing_active_tab_activates_first_remaining(self, tabs_manager):
"""Test that closing the active tab activates the first remaining tab."""
tab_id1 = tabs_manager.create_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.create_tab("Tab2", Div("Content 2"))
tab_id3 = tabs_manager.create_tab("Tab3", Div("Content 3"))
tabs_manager.close_tab(tab_id3) # close the currently active tab
assert tabs_manager.get_state().active_tab == tab_id1
def test_closing_last_tab_sets_active_to_none(self, tabs_manager):
"""Test that closing the last remaining tab sets active_tab to None."""
tab_id = tabs_manager.create_tab("Tab1", Div("Content 1"))
tabs_manager.close_tab(tab_id)
assert len(tabs_manager.get_state().tabs) == 0
assert tabs_manager.get_state().active_tab is None
def test_close_tab_cleans_cache(self, tabs_manager):
"""Test that closing a tab removes it from content cache."""
tab_id = tabs_manager.create_tab("Tab1", Div("Content 1"))
# Trigger cache population
tabs_manager._get_or_create_tab_content(tab_id)
assert tab_id in tabs_manager.get_state().ns_tabs_content
tabs_manager.close_tab(tab_id)
assert tab_id not in tabs_manager.get_state().ns_tabs_content
def test_close_tab_cleans_sent_to_client(self, tabs_manager):
"""Test that closing a tab removes it from ns_tabs_sent_to_client."""
tab_id = tabs_manager.create_tab("Tab1", Div("Content 1"))
# Send tab to client
tabs_manager.show_tab(tab_id)
assert tab_id in tabs_manager.get_state().ns_tabs_sent_to_client
tabs_manager.close_tab(tab_id)
assert tab_id not in tabs_manager.get_state().ns_tabs_sent_to_client
def test_i_cannot_close_nonexistent_tab(self, tabs_manager):
"""Test that closing a nonexistent tab returns self without error."""
result = tabs_manager.close_tab("nonexistent_id")
assert result == tabs_manager
# =========================================================================
# Content Management
# =========================================================================
def test_cached_content_is_reused(self, tabs_manager):
"""Test that cached content is returned without re-creation."""
content = Div("Test Content")
tab_id = tabs_manager.create_tab("Tab1", content)
# First call creates cache
result1 = tabs_manager._get_or_create_tab_content(tab_id)
# Second call should return same object from cache
result2 = tabs_manager._get_or_create_tab_content(tab_id)
assert result1 is result2
def test_dynamic_get_content_for_base_instance(self, tabs_manager):
"""Test that _dynamic_get_content retrieves BaseInstance from InstancesManager."""
vis_network = VisNetwork(tabs_manager, nodes=[], edges=[])
tab_id = tabs_manager.create_tab("Network", vis_network)
# Get content dynamically
result = tabs_manager._dynamic_get_content(tab_id)
assert result == vis_network
def test_dynamic_get_content_for_simple_component(self, tabs_manager):
"""Test that _dynamic_get_content returns error message for non-BaseInstance."""
content = Div("Simple content")
tab_id = tabs_manager.create_tab("Tab1", content)
# Get content dynamically (should fail gracefully)
result = tabs_manager._dynamic_get_content(tab_id)
# Should return error Div since Div is not serializable
assert matches(result, Div('Tab content does not support serialization.'))
def test_dynamic_get_content_for_nonexistent_tab(self, tabs_manager):
"""Test that _dynamic_get_content returns error for nonexistent tab."""
result = tabs_manager._dynamic_get_content("nonexistent_id")
assert matches(result, Div('Tab not found.'))
# =========================================================================
# Content Change
# =========================================================================
def test_i_can_change_tab_content(self, tabs_manager):
"""Test changing the content of an existing tab."""
tab_id = tabs_manager.create_tab("Tab1", Div("Original Content"))
new_content = Div("New Content")
tabs_manager.change_tab_content(tab_id, "Updated Tab", new_content)
assert tabs_manager.get_state().tabs[tab_id]["label"] == "Updated Tab"
assert tabs_manager.get_state().ns_tabs_content[tab_id] == new_content
def test_change_tab_content_invalidates_cache(self, tabs_manager):
"""Test that changing tab content invalidates the sent_to_client cache."""
tab_id = tabs_manager.create_tab("Tab1", Div("Original"))
# Send tab to client
tabs_manager.show_tab(tab_id)
assert tab_id in tabs_manager.get_state().ns_tabs_sent_to_client
# Change content
tabs_manager.change_tab_content(tab_id, "Tab1", Div("New Content"))
# Cache should be invalidated (discard was called)
# The show_tab inside change_tab_content will re-add it, but the test
# verifies that discard was called by checking the behavior
# Since show_tab is called after discard, tab will be in sent_to_client again
# We verify the invalidation worked by checking the method was called
# This is more of an integration test
assert tab_id in tabs_manager.get_state().ns_tabs_sent_to_client
def test_i_cannot_change_content_of_nonexistent_tab(self, tabs_manager):
"""Test that changing content of nonexistent tab returns None."""
result = tabs_manager.change_tab_content("nonexistent", "Label", Div("Content"))
assert result is None
# =========================================================================
# Auto-increment
# =========================================================================
def test_on_new_tab_with_auto_increment(self, tabs_manager):
"""Test that on_new_tab with auto_increment appends counter to label."""
tabs_manager.on_new_tab("Untitled", None, auto_increment=True)
tabs_manager.on_new_tab("Untitled", None, auto_increment=True)
tabs_manager.on_new_tab("Untitled", None, auto_increment=True)
tabs = list(tabs_manager.get_state().tabs.values())
assert tabs[0]["label"] == "Untitled_0"
assert tabs[1]["label"] == "Untitled_1"
assert tabs[2]["label"] == "Untitled_2"
def test_each_instance_has_independent_counter(self, root_instance):
"""Test that each TabsManager instance has its own independent counter."""
tm1 = TabsManager(root_instance, _id="tm1")
tm2 = TabsManager(root_instance, _id="tm2")
tm1.on_new_tab("Tab", None, auto_increment=True)
tm1.on_new_tab("Tab", None, auto_increment=True)
tm2.on_new_tab("Tab", None, auto_increment=True)
# tm1 should have Tab_0 and Tab_1
tabs_tm1 = list(tm1.get_state().tabs.values())
assert tabs_tm1[0]["label"] == "Tab_0"
assert tabs_tm1[1]["label"] == "Tab_1"
# tm2 should have Tab_0 (independent counter)
tabs_tm2 = list(tm2.get_state().tabs.values())
assert tabs_tm2[0]["label"] == "Tab_0"
def test_auto_increment_uses_default_component_when_none(self, tabs_manager):
"""Test that on_new_tab uses VisNetwork when component is None."""
tabs_manager.on_new_tab("Network", None, auto_increment=False)
tab_id = tabs_manager.get_state().tabs_order[0]
content = tabs_manager.get_state().ns_tabs_content[tab_id]
assert isinstance(content, VisNetwork)
# =========================================================================
# Duplicate Detection
# =========================================================================
def test_tab_already_exists_with_same_label_and_component(self, tabs_manager):
"""Test that duplicate tab is detected and returns existing tab_id."""
vis_network = VisNetwork(tabs_manager, nodes=[], edges=[])
tab_id1 = tabs_manager.create_tab("Network", vis_network)
tab_id2 = tabs_manager.create_tab("Network", vis_network)
# Should return same tab_id
assert tab_id1 == tab_id2
assert len(tabs_manager.get_state().tabs) == 1
def test_tab_already_exists_returns_none_for_different_label(self, tabs_manager):
"""Test that same component with different label creates new tab."""
vis_network = VisNetwork(tabs_manager, nodes=[], edges=[])
tab_id1 = tabs_manager.create_tab("Network 1", vis_network)
tab_id2 = tabs_manager.create_tab("Network 2", vis_network)
# Should create two different tabs
assert tab_id1 != tab_id2
assert len(tabs_manager.get_state().tabs) == 2
def test_tab_already_exists_returns_none_for_non_base_instance(self, tabs_manager):
"""Test that Div components are never considered duplicates."""
content = Div("Content")
tab_id1 = tabs_manager.create_tab("Tab", content)
tab_id2 = tabs_manager.create_tab("Tab", content)
# Should create two different tabs (Div is not BaseInstance)
assert tab_id1 != tab_id2
assert len(tabs_manager.get_state().tabs) == 2
# =========================================================================
# Edge Cases
# =========================================================================
def test_get_ordered_tabs_respects_order(self, tabs_manager):
"""Test that _get_ordered_tabs returns tabs in correct order."""
tab_id1 = tabs_manager.create_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.create_tab("Tab2", Div("Content 2"))
tab_id3 = tabs_manager.create_tab("Tab3", Div("Content 3"))
ordered = tabs_manager._get_ordered_tabs()
assert list(ordered.keys()) == [tab_id1, tab_id2, tab_id3]
def test_get_tab_list_returns_only_existing_tabs(self, tabs_manager):
"""Test that _get_tab_list is robust to inconsistent state."""
tab_id1 = tabs_manager.create_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.create_tab("Tab2", Div("Content 2"))
# Manually corrupt state (tab_id in order but not in tabs dict)
tabs_manager._state.tabs_order.append("fake_id")
tab_list = tabs_manager._get_tab_list()
# Should only return existing tabs
assert len(tab_list) == 2
assert all(tab["id"] in [tab_id1, tab_id2] for tab in tab_list)
def test_state_update_propagates_to_search(self, tabs_manager):
"""Test that state updates propagate to the Search component."""
tab_id1 = tabs_manager.create_tab("Tab1", Div("Content 1"))
# Search should have 1 item
assert len(tabs_manager._search.get_items()) == 1
tab_id2 = tabs_manager.create_tab("Tab2", Div("Content 2"))
# Search should have 2 items
assert len(tabs_manager._search.get_items()) == 2
tabs_manager.close_tab(tab_id1)
# Search should have 1 item again
assert len(tabs_manager._search.get_items()) == 1
def test_multiple_tabs_managers_in_same_session(self, root_instance):
"""Test that multiple TabsManager instances can coexist in same session."""
tm1 = TabsManager(root_instance, _id="tm1")
tm2 = TabsManager(root_instance, _id="tm2")
tm1.create_tab("Tab1", Div("Content 1"))
tm2.create_tab("Tab2", Div("Content 2"))
assert len(tm1.get_state().tabs) == 1
assert len(tm2.get_state().tabs) == 1
assert tm1.get_id() != tm2.get_id()
class TestTabsManagerRender:
"""Tests for TabsManager HTML rendering."""
@pytest.fixture
def tabs_manager(self, root_instance):
"""Create a fresh TabsManager instance for render tests."""
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
tm = TabsManager(root_instance)
yield tm
InstancesManager.reset()
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
# =========================================================================
# Controller
# =========================================================================
def test_i_can_render_tabs_manager_with_no_tabs(self, tabs_manager):
"""Test that TabsManager renders with no tabs."""
html = tabs_manager.render()
expected = Div(
Div(id=f"{tabs_manager.get_id()}-controller"), # controller
Div(id=f"{tabs_manager.get_id()}-header-wrapper"), # header
Div(id=f"{tabs_manager.get_id()}-content-wrapper"), # active content
id=tabs_manager.get_id(),
)
assert matches(html, expected)
def test_controller_has_correct_id_and_attributes(self, tabs_manager):
"""Test that controller has correct ID, data attribute, and HTMX script.
Why these elements matter:
- id: Required for HTMX targeting in commands
- data_active_tab: Stores active tab ID for JavaScript access
- hx_on__after_settle: Triggers updateTabs() after HTMX swap completes
"""
tab_id = tabs_manager.create_tab("Tab1", Div("Content"))
controller = tabs_manager._mk_tabs_controller(oob=False)
expected = Div(
id=f"{tabs_manager.get_id()}-controller",
data_active_tab=tab_id,
hx_on__after_settle=f'updateTabs("{tabs_manager.get_id()}-controller");'
)
assert matches(controller, expected)
def test_controller_with_oob_false(self, tabs_manager):
"""Test that controller has no swap attribute when oob=False.
Why this matters:
- hx_swap_oob controls whether element swaps out-of-band
- When False, element swaps into target specified in command
"""
controller = tabs_manager._mk_tabs_controller(oob=False)
# Controller should not have hx_swap_oob attribute
assert not hasattr(controller, 'hx_swap_oob') or controller.attrs.get('hx_swap_oob') is None
def test_controller_with_oob_true(self, tabs_manager):
"""Test that controller has swap attribute when oob=True.
Why this matters:
- hx_swap_oob="true" enables out-of-band swapping
- Allows updating controller independently of main target
"""
controller = tabs_manager._mk_tabs_controller(oob=True)
expected = Div(hx_swap_oob="true")
assert matches(controller, expected)
# =========================================================================
# Header
# =========================================================================
def test_header_wrapper_with_no_tabs(self, tabs_manager):
"""Test that header wrapper renders empty when no tabs exist.
Why these elements matter:
- id: Required for targeting header in updates
- cls: Provides base styling for header
- Empty header div: Shows no tabs are present
- Search menu: Always present for adding tabs
"""
header = tabs_manager._mk_tabs_header_wrapper(oob=False)
# Should have no children (no tabs)
tab_buttons = find(header, Div(cls=Contains("mf-tab-button")))
assert len(tab_buttons) == 0, "Header should contain no tab buttons when empty"
def test_header_wrapper_with_multiple_tabs(self, tabs_manager):
"""Test that header wrapper renders all tab buttons.
Why these elements matter:
- Multiple Div elements: Each represents a tab button
- Order preservation: Tabs must appear in creation order
- cls mf-tab-button: Identifies tab buttons for styling/selection
"""
tabs_manager.create_tab("Tab1", Div("Content 1"))
tabs_manager.create_tab("Tab2", Div("Content 2"))
tabs_manager.create_tab("Tab3", Div("Content 3"))
header = tabs_manager._mk_tabs_header_wrapper(oob=False)
# Should have 3 tab buttons
tab_buttons = find(header, Div(cls=Contains("mf-tab-button")))
assert len(tab_buttons) == 3, "Header should contain exactly 3 tab buttons"
def test_header_wrapper_contains_search_menu(self, tabs_manager):
"""Test that header wrapper contains the search menu dropdown.
Why these elements matter:
- dropdown: DaisyUI dropdown container for tab search
- dropdown-end: Positions dropdown at end of header
- TestIcon for tabs icon: Visual indicator for menu
- Search component: Provides tab search functionality
"""
header = tabs_manager._mk_tabs_header_wrapper(oob=False)
# Find dropdown menu
dropdown = find_one(header, Div(cls=Contains("dropdown", "dropdown-end")))
# Should contain tabs icon
icon = find_one(dropdown, TestIcon("tabs24_regular"))
assert icon is not None, "Dropdown should contain tabs icon"
# Should contain Search component
search = find_one(dropdown, TestObject(tabs_manager._search.__class__))
assert search is not None, "Dropdown should contain Search component"
# =========================================================================
# Tab Button
# =========================================================================
def test_tab_button_for_active_tab(self, tabs_manager):
"""Test that active tab button has the active class.
Why these elements matter:
- cls mf-tab-active: Highlights the currently active tab
- data_tab_id: Links button to specific tab
- data_manager_id: Links button to TabsManager instance
"""
tab_id = tabs_manager.create_tab("Active Tab", Div("Content"))
tab_data = tabs_manager.get_state().tabs[tab_id]
button = tabs_manager._mk_tab_button(tab_data)
expected = Div(
cls=Contains("mf-tab-button", "mf-tab-active"),
data_tab_id=tab_id,
data_manager_id=tabs_manager.get_id()
)
assert matches(button, expected)
def test_tab_button_for_inactive_tab(self, tabs_manager):
"""Test that inactive tab button does not have active class.
Why these elements matter:
- cls without mf-tab-active: Indicates tab is not active
- Visual distinction: User can see which tab is selected
"""
tab_id1 = tabs_manager.create_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.create_tab("Tab2", Div("Content 2"))
# tab_id1 is now inactive
tab_data = tabs_manager.get_state().tabs[tab_id1]
button = tabs_manager._mk_tab_button(tab_data)
expected = Div(
cls=And(DoesNotContain("mf-tab-active"), Contains("mf-tab-button")),
data_tab_id=tab_id1,
data_manager_id=tabs_manager.get_id()
)
assert matches(button, expected)
def test_tab_button_has_label_and_close_icon(self, tabs_manager):
"""Test that tab button contains label and close icon.
Why these elements matter:
- Label Span: Displays tab name
- Close icon: Allows user to close the tab
- cls mf-tab-label: Styles the label text
- cls mf-tab-close-btn: Styles the close button
"""
tab_id = tabs_manager.create_tab("My Tab", Div("Content"))
tab_data = tabs_manager.get_state().tabs[tab_id]
button = tabs_manager._mk_tab_button(tab_data)
expected = Div(
Span("My Tab", cls=Contains("mf-tab-label")),
Span(TestIconNotStr("dismiss_circle16_regular"), cls=Contains("mf-tab-close-btn")),
)
assert matches(button, expected)
# =========================================================================
# Content
# =========================================================================
def test_content_wrapper_when_no_active_tab(self, tabs_manager):
"""Test that content wrapper shows 'No Content' when no tab is active.
Why these elements matter:
- id content-wrapper: Container for all tab contents
- 'No Content' message: Informs user no tab is selected
- cls hidden: Hides empty content by default
"""
wrapper = tabs_manager._mk_tab_content_wrapper()
# Should contain "No Content" message
content = find_one(wrapper, Div(cls=Contains("mf-empty-content")))
assert "No Content" in str(content)
def test_content_wrapper_when_tab_active(self, tabs_manager):
"""Test that content wrapper shows active tab content.
Why these elements matter:
- Active tab content is visible
- Content is wrapped in mf-tab-content div
- Correct tab ID in content div ID
"""
tab_id = tabs_manager.create_tab("Tab1", Div("My Content"))
wrapper = tabs_manager._mk_tab_content_wrapper()
# global view from the wrapper
expected = Div(
Div(), # tab content, tested just after
id=f"{tabs_manager.get_id()}-content-wrapper",
cls=Contains("mf-tab-content-wrapper"),
)
assert matches(wrapper, expected)
# check if the content is present
tab_content = find_one(wrapper, Div(id=f"{tabs_manager.get_id()}-{tab_id}-content"))
expected = Div(
Div("My Content"), # <= content
cls=Contains("mf-tab-content"),
)
assert matches(tab_content, expected)
def test_tab_content_for_active_tab(self, tabs_manager):
"""Test that active tab content does not have hidden class.
Why these elements matter:
- No 'hidden' class: Content is visible to user
- Correct content ID: Links to specific tab
"""
content_div = Div("Test Content")
tab_id = tabs_manager.create_tab("Tab1", content_div)
tab_content_wrapper = tabs_manager._mk_tab_content(tab_id, content_div)
tab_content = find_one(tab_content_wrapper, Div(id=f"{tabs_manager.get_id()}-{tab_id}-content"))
# Should not have 'hidden' in classes
classes = tab_content.attrs.get('class', '')
assert 'hidden' not in classes
assert 'mf-tab-content' in classes
def test_tab_content_for_inactive_tab(self, tabs_manager):
"""Test that inactive tab content has hidden class.
Why these elements matter:
- 'hidden' class: Hides inactive tab content
- Content still in DOM: Enables fast tab switching
"""
tab_id1 = tabs_manager.create_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.create_tab("Tab2", Div("Content 2"))
# tab_id1 is now inactive
content_div = Div("Content 1")
tab_content = tabs_manager._mk_tab_content(tab_id1, content_div)
# Should have 'hidden' class
expected = Div(cls=Contains("hidden"))
assert matches(tab_content, expected)
# =========================================================================
# Complete Render
# =========================================================================
def test_complete_render_with_no_tabs(self, tabs_manager):
"""Test complete render structure when no tabs exist.
Why these elements matter:
- cls mf-tabs-manager: Root container class for styling
- Controller: HTMX control and state management
- Header wrapper: Tab buttons container (empty)
- Content wrapper: Tab content container (shows 'No Content')
- Script: Initializes tabs on first render
"""
render = tabs_manager.render()
# Extract main elements
controller = find_one(render, Div(id=f"{tabs_manager.get_id()}-controller"))
header = find_one(render, Div(id=f"{tabs_manager.get_id()}-header-wrapper"))
content = find_one(render, Div(id=f"{tabs_manager.get_id()}-content-wrapper"))
script = find_one(render, TestScript(f'updateTabs("{tabs_manager.get_id()}-controller")'))
assert controller is not None, "Render should contain controller"
assert header is not None, "Render should contain header wrapper"
assert content is not None, "Render should contain content wrapper"
assert script is not None, "Render should contain initialization script"
def test_complete_render_with_multiple_tabs(self, tabs_manager):
"""Test complete render structure with multiple tabs.
Why these elements matter:
- 3 tab buttons: One for each created tab
- Active tab content visible: Shows current tab content
- Inactive tabs hidden: Lazy-loaded on activation
- Structure consistency: All components present
"""
tabs_manager.create_tab("Tab1", Div("Content 1"))
tabs_manager.create_tab("Tab2", Div("Content 2"))
tab_id3 = tabs_manager.create_tab("Tab3", Div("Content 3"))
render = tabs_manager.render()
# Find header
header_inner = find_one(render, Div(id=f"{tabs_manager.get_id()}-header"))
# Should have 3 tab buttons
tab_buttons = find(header_inner, Div(cls=Contains("mf-tab-button")))
assert len(tab_buttons) == 3, "Render should contain exactly 3 tab buttons"
# Content wrapper should show active tab (tab3)
active_content = find_one(render, Div(id=f"{tabs_manager.get_id()}-{tab_id3}-content"))
expected = Div(Div("Content 3"), cls=Contains("mf-tab-content"))
assert matches(active_content, expected), "Active tab content should be visible"