"""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)