498 lines
17 KiB
Python
498 lines
17 KiB
Python
"""Unit tests for Panel component."""
|
|
import shutil
|
|
|
|
import pytest
|
|
from fasthtml.components import *
|
|
|
|
from myfasthtml.controls.Panel import Panel, PanelConf
|
|
from myfasthtml.test.matcher import matches, find, Contains, find_one, TestIcon, TestScript, TestIconNotStr
|
|
from .conftest import root_instance
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def cleanup_db():
|
|
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
|
|
|
|
|
|
class TestPanelBehaviour:
|
|
"""Tests for Panel behavior and logic."""
|
|
|
|
# 1. Creation and initialization
|
|
|
|
def test_i_can_create_panel_with_default_config(self, root_instance):
|
|
"""Test that a Panel can be created with default configuration."""
|
|
panel = Panel(root_instance)
|
|
|
|
assert panel is not None
|
|
assert panel.conf.left is True
|
|
assert panel.conf.right is True
|
|
|
|
def test_i_can_create_panel_with_custom_config(self, root_instance):
|
|
"""Test that a Panel accepts a custom PanelConf."""
|
|
custom_conf = PanelConf(left=False, right=True)
|
|
panel = Panel(root_instance, conf=custom_conf)
|
|
|
|
assert panel.conf.left is False
|
|
assert panel.conf.right is True
|
|
|
|
def test_panel_has_default_state_after_creation(self, root_instance):
|
|
"""Test that _state has correct initial values."""
|
|
panel = Panel(root_instance)
|
|
state = panel._state
|
|
|
|
assert state.left_visible is True
|
|
assert state.right_visible is True
|
|
assert state.left_width == 250
|
|
assert state.right_width == 250
|
|
|
|
def test_panel_creates_commands_instance(self, root_instance):
|
|
"""Test that panel.commands exists and is of type Commands."""
|
|
panel = Panel(root_instance)
|
|
|
|
assert panel.commands is not None
|
|
assert panel.commands.__class__.__name__ == "Commands"
|
|
|
|
# 2. Content management
|
|
|
|
def test_i_can_set_main_content(self, root_instance):
|
|
"""Test that set_main() stores content in _main."""
|
|
panel = Panel(root_instance)
|
|
content = Div("Main content")
|
|
|
|
panel.set_main(content)
|
|
|
|
assert panel._main == content
|
|
|
|
def test_set_main_returns_self(self, root_instance):
|
|
"""Test that set_main() returns self for method chaining."""
|
|
panel = Panel(root_instance)
|
|
content = Div("Main content")
|
|
|
|
result = panel.set_main(content)
|
|
|
|
assert result is panel
|
|
|
|
def test_i_can_set_left_content(self, root_instance):
|
|
"""Test that set_left() stores content in _left."""
|
|
panel = Panel(root_instance)
|
|
content = Div("Left content")
|
|
|
|
panel.set_left(content)
|
|
|
|
assert panel._left == content
|
|
|
|
def test_i_can_set_right_content(self, root_instance):
|
|
"""Test that set_right() stores content in _right."""
|
|
panel = Panel(root_instance)
|
|
content = Div("Right content")
|
|
|
|
panel.set_right(content)
|
|
|
|
assert panel._right == content
|
|
|
|
# 3. Toggle visibility
|
|
|
|
def test_i_can_hide_left_panel(self, root_instance):
|
|
"""Test that toggle_side('left', False) sets _state.left_visible to False."""
|
|
panel = Panel(root_instance)
|
|
|
|
panel.toggle_side("left", False)
|
|
|
|
assert panel._state.left_visible is False
|
|
|
|
def test_i_can_show_left_panel(self, root_instance):
|
|
"""Test that toggle_side('left', True) sets _state.left_visible to True."""
|
|
panel = Panel(root_instance)
|
|
panel._state.left_visible = False
|
|
|
|
panel.toggle_side("left", True)
|
|
|
|
assert panel._state.left_visible is True
|
|
|
|
def test_i_can_hide_right_panel(self, root_instance):
|
|
"""Test that toggle_side('right', False) sets _state.right_visible to False."""
|
|
panel = Panel(root_instance)
|
|
|
|
panel.toggle_side("right", False)
|
|
|
|
assert panel._state.right_visible is False
|
|
|
|
def test_i_can_show_right_panel(self, root_instance):
|
|
"""Test that toggle_side('right', True) sets _state.right_visible to True."""
|
|
panel = Panel(root_instance)
|
|
panel._state.right_visible = False
|
|
|
|
panel.toggle_side("right", True)
|
|
|
|
assert panel._state.right_visible is True
|
|
|
|
def test_toggle_side_returns_panel_and_icon(self, root_instance):
|
|
"""Test that toggle_side() returns a tuple (panel_element, show_icon_element)."""
|
|
panel = Panel(root_instance)
|
|
|
|
result = panel.toggle_side("left", False)
|
|
|
|
assert isinstance(result, tuple)
|
|
assert len(result) == 2
|
|
|
|
# 4. Width management
|
|
|
|
def test_i_can_update_left_panel_width(self, root_instance):
|
|
"""Test that update_side_width('left', 300) sets _state.left_width to 300."""
|
|
panel = Panel(root_instance)
|
|
|
|
panel.update_side_width("left", 300)
|
|
|
|
assert panel._state.left_width == 300
|
|
|
|
def test_i_can_update_right_panel_width(self, root_instance):
|
|
"""Test that update_side_width('right', 400) sets _state.right_width to 400."""
|
|
panel = Panel(root_instance)
|
|
|
|
panel.update_side_width("right", 400)
|
|
|
|
assert panel._state.right_width == 400
|
|
|
|
def test_update_width_returns_panel_element(self, root_instance):
|
|
"""Test that update_side_width() returns a panel element."""
|
|
panel = Panel(root_instance)
|
|
|
|
result = panel.update_side_width("left", 300)
|
|
|
|
assert result is not None
|
|
|
|
# 5. Configuration
|
|
|
|
def test_disabled_left_panel_returns_none(self, root_instance):
|
|
"""Test that _mk_panel('left') returns None when conf.left=False."""
|
|
custom_conf = PanelConf(left=False, right=True)
|
|
panel = Panel(root_instance, conf=custom_conf)
|
|
|
|
result = panel._mk_panel("left")
|
|
|
|
assert result is None
|
|
|
|
def test_disabled_right_panel_returns_none(self, root_instance):
|
|
"""Test that _mk_panel('right') returns None when conf.right=False."""
|
|
custom_conf = PanelConf(left=True, right=False)
|
|
panel = Panel(root_instance, conf=custom_conf)
|
|
|
|
result = panel._mk_panel("right")
|
|
|
|
assert result is None
|
|
|
|
def test_disabled_panel_show_icon_returns_none(self, root_instance):
|
|
"""Test that _mk_show_icon() returns None when the panel is disabled."""
|
|
custom_conf = PanelConf(left=False, right=True)
|
|
panel = Panel(root_instance, conf=custom_conf)
|
|
|
|
result = panel._mk_show_icon("left")
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestPanelRender:
|
|
"""Tests for Panel HTML rendering."""
|
|
|
|
@pytest.fixture
|
|
def panel(self, root_instance):
|
|
panel = Panel(root_instance)
|
|
panel.set_main(Div("Main content"))
|
|
panel.set_left(Div("Left content"))
|
|
panel.set_right(Div("Right content"))
|
|
return panel
|
|
|
|
# 1. Global structure (UTR-11.1 - FIRST TEST)
|
|
|
|
def test_i_can_render_panel_with_default_state(self, panel):
|
|
"""Test that Panel renders with correct global structure.
|
|
|
|
Why these elements matter:
|
|
- 4 children: Verifies all main sections are rendered (left panel, main, right panel, script)
|
|
- _id: Essential for panel identification and resizer initialization
|
|
- cls="mf-panel": Root CSS class for panel styling
|
|
"""
|
|
expected = Div(
|
|
Div(), # left panel
|
|
Div(), # main
|
|
Div(), # right panel
|
|
Script(),
|
|
id=panel._id,
|
|
cls="mf-panel"
|
|
)
|
|
|
|
assert matches(panel.render(), expected)
|
|
|
|
# 2. Left panel
|
|
|
|
def test_left_panel_renders_with_correct_structure(self, panel):
|
|
"""Test that left panel has content div before resizer.
|
|
|
|
Why these elements matter:
|
|
- Order (content then resizer): Critical for positioning resizer on the right side
|
|
- id: Required for HTMX targeting during toggle/resize operations
|
|
- cls Contains "mf-panel-left": CSS class for left panel styling
|
|
"""
|
|
left_panel = find_one(panel.render(), Div(id=f"{panel._id}_panel_left"))
|
|
|
|
# Step 1: Validate left panel global structure
|
|
expected = Div(
|
|
Div(id=f"{panel._id}_content_left"), # content div, tested in detail later
|
|
Div(cls=Contains("mf-resizer-left")), # resizer
|
|
id=f"{panel._id}_panel_left",
|
|
cls=Contains("mf-panel-left")
|
|
)
|
|
|
|
assert matches(left_panel, expected)
|
|
|
|
def test_left_panel_has_mf_hidden_class_when_not_visible(self, panel):
|
|
"""Test that left panel has 'mf-hidden' class when not visible.
|
|
|
|
Why these elements matter:
|
|
- cls Contains "mf-hidden": CSS class required for width animation
|
|
"""
|
|
panel._state.left_visible = False
|
|
left_panel = find_one(panel.render(), Div(id=f"{panel._id}_panel_left"))
|
|
|
|
expected = Div(cls=Contains("mf-hidden"))
|
|
|
|
assert matches(left_panel, expected)
|
|
|
|
def test_left_panel_does_not_render_when_disabled(self, panel):
|
|
"""Test that render() does not contain left panel when conf.left=False.
|
|
|
|
Why these elements matter:
|
|
- Absence of left panel: Configuration must prevent rendering
|
|
"""
|
|
panel.conf.left = False
|
|
rendered = panel.render()
|
|
|
|
# Verify left panel is not present
|
|
left_panels = find(rendered, Div(id=f"{panel._id}_panel_left"))
|
|
assert len(left_panels) == 0, "Left panel should not be present when conf.left=False"
|
|
|
|
# 3. Right panel
|
|
|
|
def test_right_panel_renders_with_correct_structure(self, panel):
|
|
"""Test that right panel has resizer before content div.
|
|
|
|
Why these elements matter:
|
|
- Order (resizer then content): Critical for positioning resizer on the left side
|
|
- id: Required for HTMX targeting during toggle/resize operations
|
|
- cls Contains "mf-panel-right": CSS class for right panel styling
|
|
"""
|
|
right_panel = find_one(panel.render(), Div(id=f"{panel._id}_panel_right"))
|
|
|
|
# Step 1: Validate right panel global structure
|
|
expected = Div(
|
|
Div(cls=Contains("mf-resizer-right")), # resizer
|
|
Div(id=f"{panel._id}_content_right"), # content div, tested in detail later
|
|
id=f"{panel._id}_panel_right",
|
|
cls=Contains("mf-panel-right")
|
|
)
|
|
|
|
assert matches(right_panel, expected)
|
|
|
|
def test_right_panel_has_mf_hidden_class_when_not_visible(self, panel):
|
|
"""Test that right panel has 'mf-hidden' class when not visible.
|
|
|
|
Why these elements matter:
|
|
- cls Contains "mf-hidden": CSS class required for width animation
|
|
"""
|
|
panel._state.right_visible = False
|
|
right_panel = find_one(panel.render(), Div(id=f"{panel._id}_panel_right"))
|
|
|
|
expected = Div(cls=Contains("mf-hidden"))
|
|
|
|
assert matches(right_panel, expected)
|
|
|
|
def test_right_panel_does_not_render_when_disabled(self, panel):
|
|
"""Test that render() does not contain right panel when conf.right=False.
|
|
|
|
Why these elements matter:
|
|
- Absence of right panel: Configuration must prevent rendering
|
|
"""
|
|
panel.conf.right = False
|
|
rendered = panel.render()
|
|
|
|
# Verify right panel is not present
|
|
right_panels = find(rendered, Div(id=f"{panel._id}_panel_right"))
|
|
assert len(right_panels) == 0, "Right panel should not be present when conf.right=False"
|
|
|
|
# 4. Resizers
|
|
|
|
def test_left_panel_has_resizer_with_correct_attributes(self, panel):
|
|
"""Test that left panel resizer has required attributes.
|
|
|
|
Why these elements matter:
|
|
- data_side="left": JavaScript uses this to determine which side is being resized
|
|
- data_command_id: Required to trigger update_side_width command via HTMX
|
|
- cls Contains "mf-resizer": Base CSS class for resizer styling
|
|
- cls Contains "mf-resizer-left": Left-specific CSS class for positioning
|
|
"""
|
|
left_panel = find_one(panel.render(), Div(id=f"{panel._id}_panel_left"))
|
|
resizer = find_one(left_panel, Div(cls=Contains("mf-resizer-left")))
|
|
|
|
expected = Div(
|
|
data_side="left",
|
|
cls=Contains("mf-resizer", "mf-resizer-left")
|
|
)
|
|
|
|
assert matches(resizer, expected)
|
|
# Verify data-command-id exists (value is dynamic, HTML uses hyphens)
|
|
assert "data-command-id" in resizer.attrs
|
|
|
|
def test_right_panel_has_resizer_with_correct_attributes(self, panel):
|
|
"""Test that right panel resizer has required attributes.
|
|
|
|
Why these elements matter:
|
|
- data_side="right": JavaScript uses this to determine which side is being resized
|
|
- data_command_id: Required to trigger update_side_width command via HTMX
|
|
- cls Contains "mf-resizer": Base CSS class for resizer styling
|
|
- cls Contains "mf-resizer-right": Right-specific CSS class for positioning
|
|
"""
|
|
right_panel = find_one(panel.render(), Div(id=f"{panel._id}_panel_right"))
|
|
resizer = find_one(right_panel, Div(cls=Contains("mf-resizer-right")))
|
|
|
|
expected = Div(
|
|
data_side="right",
|
|
cls=Contains("mf-resizer", "mf-resizer-right")
|
|
)
|
|
|
|
assert matches(resizer, expected)
|
|
# Verify data-command-id exists (value is dynamic, HTML uses hyphens)
|
|
assert "data-command-id" in resizer.attrs
|
|
|
|
# 5. Icons
|
|
|
|
def test_hide_icon_in_left_panel_has_correct_command(self, panel):
|
|
"""Test that hide icon in left panel triggers toggle_side command.
|
|
|
|
Why these elements matter:
|
|
- TestIconNotStr("subtract20_regular"): Verify correct icon is used for hiding
|
|
- cls Contains "mf-panel-hide-icon": CSS class for hide icon positioning
|
|
"""
|
|
left_content = find_one(panel.render(), Div(id=f"{panel._id}_content_left"))
|
|
|
|
# Find the hide icon (should be wrapped by mk.icon)
|
|
hide_icons = find(left_content, Div(cls=Contains("mf-panel-hide-icon")))
|
|
assert len(hide_icons) == 1, "Left panel should contain exactly one hide icon"
|
|
|
|
# Verify it contains the subtract icon
|
|
expected = Div(
|
|
TestIconNotStr("subtract20_regular"),
|
|
cls=Contains("mf-panel-hide-icon")
|
|
)
|
|
assert matches(hide_icons[0], expected)
|
|
|
|
def test_hide_icon_in_right_panel_has_correct_command(self, panel):
|
|
"""Test that hide icon in right panel triggers toggle_side command.
|
|
|
|
Why these elements matter:
|
|
- TestIconNotStr("subtract20_regular"): Verify correct icon is used for hiding
|
|
- cls Contains "mf-panel-hide-icon": CSS class for hide icon positioning
|
|
"""
|
|
right_content = find_one(panel.render(), Div(id=f"{panel._id}_content_right"))
|
|
|
|
# Find the hide icon (should be wrapped by mk.icon)
|
|
hide_icons = find(right_content, Div(cls=Contains("mf-panel-hide-icon")))
|
|
assert len(hide_icons) == 1, "Right panel should contain exactly one hide icon"
|
|
|
|
# Verify it contains the subtract icon
|
|
expected = Div(
|
|
TestIconNotStr("subtract20_regular"),
|
|
cls=Contains("mf-panel-hide-icon")
|
|
)
|
|
assert matches(hide_icons[0], expected)
|
|
|
|
def test_show_icon_left_is_hidden_when_panel_visible(self, panel):
|
|
"""Test that show icon has 'hidden' class when left panel is visible.
|
|
|
|
Why these elements matter:
|
|
- cls Contains "hidden": Tailwind class to hide icon when panel is visible
|
|
- id: Required for HTMX swap-oob targeting
|
|
"""
|
|
main_panel = find_one(panel.render(), Div(cls=Contains("mf-panel-main")))
|
|
show_icon = find_one(main_panel, Div(id=f"{panel._id}_show_left"))
|
|
|
|
expected = Div(
|
|
cls=Contains("hidden"),
|
|
id=f"{panel._id}_show_left"
|
|
)
|
|
|
|
assert matches(show_icon, expected)
|
|
|
|
def test_show_icon_left_is_visible_when_panel_hidden(self, panel):
|
|
"""Test that show icon is positioned left when left panel is hidden.
|
|
|
|
Why these elements matter:
|
|
- cls Contains "mf-panel-show-icon-left": CSS class for left positioning in main panel
|
|
- TestIconNotStr("more_horizontal20_regular"): Verify correct icon is used for showing
|
|
"""
|
|
panel._state.left_visible = False
|
|
main_panel = find_one(panel.render(), Div(cls=Contains("mf-panel-main")))
|
|
show_icon = find_one(main_panel, Div(id=f"{panel._id}_show_left"))
|
|
|
|
expected = Div(
|
|
TestIconNotStr("more_horizontal20_regular"),
|
|
cls=Contains("mf-panel-show-icon-left"),
|
|
id=f"{panel._id}_show_left"
|
|
)
|
|
|
|
assert matches(show_icon, expected)
|
|
|
|
def test_show_icon_right_is_visible_when_panel_hidden(self, panel):
|
|
"""Test that show icon is positioned right when right panel is hidden.
|
|
|
|
Why these elements matter:
|
|
- cls Contains "mf-panel-show-icon-right": CSS class for right positioning in main panel
|
|
- TestIconNotStr("more_horizontal20_regular"): Verify correct icon is used for showing
|
|
"""
|
|
panel._state.right_visible = False
|
|
main_panel = find_one(panel.render(), Div(cls=Contains("mf-panel-main")))
|
|
show_icon = find_one(main_panel, Div(id=f"{panel._id}_show_right"))
|
|
|
|
expected = Div(
|
|
TestIconNotStr("more_horizontal20_regular"),
|
|
cls=Contains("mf-panel-show-icon-right"),
|
|
id=f"{panel._id}_show_right"
|
|
)
|
|
|
|
assert matches(show_icon, expected)
|
|
|
|
# 6. Main panel
|
|
|
|
def test_main_panel_contains_show_icons_and_content(self, panel):
|
|
"""Test that main panel contains show icons and content in correct order.
|
|
|
|
Why these elements matter:
|
|
- 3 children: show_icon_left + content + show_icon_right
|
|
- Order: Show icons must be positioned correctly (left then right)
|
|
- cls="mf-panel-main": CSS class for main panel styling
|
|
"""
|
|
main_panel = find_one(panel.render(), Div(cls=Contains("mf-panel-main")))
|
|
|
|
# Step 1: Validate main panel structure
|
|
expected = Div(
|
|
Div(id=f"{panel._id}_show_left"), # show icon left
|
|
Div("Main content"), # actual content
|
|
Div(id=f"{panel._id}_show_right"), # show icon right
|
|
cls="mf-panel-main"
|
|
)
|
|
|
|
assert matches(main_panel, expected)
|
|
|
|
# 7. Script
|
|
|
|
def test_init_resizer_script_is_present(self, panel):
|
|
"""Test that initResizer script is present with correct panel ID.
|
|
|
|
Why these elements matter:
|
|
- Script content: Must call initResizer with panel ID for resize functionality
|
|
"""
|
|
script = find_one(panel.render(), Script())
|
|
|
|
expected = TestScript(f"initResizer('{panel._id}');")
|
|
|
|
assert matches(script, expected)
|