"""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 False 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.set_side_visible("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.set_side_visible("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.set_side_visible("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.set_side_visible("right", True) assert panel._state.right_visible is True def test_set_side_visible_returns_panel_and_icon(self, root_instance): """Test that set_side_visible() returns a tuple (panel_element, show_icon_element).""" panel = Panel(root_instance) result = panel.set_side_visible("left", False) assert isinstance(result, tuple) assert len(result) == 2 @pytest.mark.parametrize("side, initial_visible, expected_visible", [ ("left", True, False), # left visible → hidden ("left", False, True), # left hidden → visible ("right", True, False), # right visible → hidden ("right", False, True), # right hidden → visible ]) def test_i_can_toggle_panel_visibility(self, root_instance, side, initial_visible, expected_visible): """Test that toggle_side() inverts the visibility state.""" panel = Panel(root_instance) if side == "left": panel._state.left_visible = initial_visible else: panel._state.right_visible = initial_visible panel.toggle_side(side) if side == "left": assert panel._state.left_visible is expected_visible else: assert panel._state.right_visible is expected_visible 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") 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("right", 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 @pytest.mark.parametrize("side, conf_kwargs", [ ("left", {"show_display_left": False}), ("right", {"show_display_right": False}), ]) def test_show_icon_returns_none_when_show_display_disabled(self, root_instance, side, conf_kwargs): """Test that _mk_show_icon() returns None when show_display is disabled.""" custom_conf = PanelConf(left=True, right=True, **conf_kwargs) panel = Panel(root_instance, conf=custom_conf) result = panel._mk_show_icon(side) assert result is None class TestPanelRender: """Tests for Panel HTML rendering.""" @pytest.fixture def panel(self, root_instance): """Panel with titles (default behavior).""" panel = Panel(root_instance, PanelConf(left=True, right=True)) panel.set_main(Div("Main content")) panel.set_left(Div("Left content")) panel.set_right(Div("Right content")) return panel @pytest.fixture def panel_no_title(self, root_instance): """Panel without titles (legacy behavior).""" panel = Panel(root_instance, PanelConf( left=True, right=True, show_left_title=False, show_right_title=False )) 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_title_structure(self, panel): """Test that left panel with title has header + scrollable content. Why these elements matter: - mf-panel-body: Grid container for header + content layout - mf-panel-header: Contains title and hide icon - mf-panel-content: Scrollable content area - mf-panel-with-title: Removes default padding-top """ left_panel = find_one(panel.render(), Div(id=panel.get_ids().panel("left"))) expected = Div( Div(cls=Contains("mf-panel-body")), # body with header + content Div(cls=Contains("mf-resizer-left")), # resizer id=panel.get_ids().panel("left"), cls=Contains("mf-panel-left", "mf-panel-with-title") ) assert matches(left_panel, expected) def test_left_panel_header_contains_title_and_icon(self, panel): """Test that left panel header has title and hide icon. Why these elements matter: - Title: Displays the panel title (left aligned) - Hide icon: Allows user to collapse the panel (right aligned) """ left_panel = find_one(panel.render(), Div(id=panel.get_ids().panel("left"))) header = find_one(left_panel, Div(cls=Contains("mf-panel-header"))) expected = Div( Div("Left"), # title Div(cls=Contains("mf-panel-hide-icon")), # hide icon cls="mf-panel-header" ) assert matches(header, expected) def test_left_panel_renders_without_title_structure(self, panel_no_title): """Test that left panel without title has legacy structure. Why these elements matter: - Order (hide icon, content, resizer): Legacy layout without header - No mf-panel-with-title class """ left_panel = find_one(panel_no_title.render(), Div(id=panel_no_title.get_ids().panel("left"))) expected = Div( TestIcon("subtract20_regular"), Div(id=panel_no_title.get_ids().left), Div(cls=Contains("mf-resizer-left")), id=panel_no_title.get_ids().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=panel.get_ids().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=panel.get_ids().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_title_structure(self, panel): """Test that right panel with title has header + scrollable content. Why these elements matter: - mf-panel-body: Grid container for header + content layout - mf-panel-header: Contains title and hide icon - mf-panel-content: Scrollable content area - mf-panel-with-title: Removes default padding-top """ right_panel = find_one(panel.render(), Div(id=panel.get_ids().panel("right"))) expected = Div( Div(cls=Contains("mf-resizer-right")), # resizer Div(cls=Contains("mf-panel-body")), # body with header + content id=panel.get_ids().panel("right"), cls=Contains("mf-panel-right", "mf-panel-with-title") ) assert matches(right_panel, expected) def test_right_panel_header_contains_title_and_icon(self, panel): """Test that right panel header has title and hide icon. Why these elements matter: - Title: Displays the panel title (left aligned) - Hide icon: Allows user to collapse the panel (right aligned) """ right_panel = find_one(panel.render(), Div(id=panel.get_ids().panel("right"))) header = find_one(right_panel, Div(cls=Contains("mf-panel-header"))) expected = Div( Div("Right"), # title Div(cls=Contains("mf-panel-hide-icon")), # hide icon cls="mf-panel-header" ) assert matches(header, expected) def test_right_panel_renders_without_title_structure(self, panel_no_title): """Test that right panel without title has legacy structure. Why these elements matter: - Order (resizer, hide icon, content): Legacy layout without header - No mf-panel-with-title class """ right_panel = find_one(panel_no_title.render(), Div(id=panel_no_title.get_ids().panel("right"))) expected = Div( Div(cls=Contains("mf-resizer-right")), TestIcon("subtract20_regular"), Div(id=panel_no_title.get_ids().right), id=panel_no_title.get_ids().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=panel.get_ids().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=panel.get_ids().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=panel.get_ids().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=panel.get_ids().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_header(self, panel): """Test that hide icon in left panel header has correct structure. 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 styling - Icon is inside header when title is shown """ left_panel = find_one(panel.render(), Div(id=panel.get_ids().panel("left"))) header = find_one(left_panel, Div(cls=Contains("mf-panel-header"))) hide_icons = find(header, Div(cls=Contains("mf-panel-hide-icon"))) assert len(hide_icons) == 1, "Header should contain exactly one hide 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_header(self, panel): """Test that hide icon in right panel header has correct structure. 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 styling - Icon is inside header when title is shown """ right_panel = find_one(panel.render(), Div(id=panel.get_ids().panel("right"))) header = find_one(right_panel, Div(cls=Contains("mf-panel-header"))) hide_icons = find(header, Div(cls=Contains("mf-panel-hide-icon"))) assert len(hide_icons) == 1, "Header should contain exactly one hide icon" expected = Div( TestIconNotStr("subtract20_regular"), cls=Contains("mf-panel-hide-icon") ) assert matches(hide_icons[0], expected) def test_hide_icon_in_panel_without_title(self, panel_no_title): """Test that hide icon is at root level when no title. Why these elements matter: - Hide icon should be direct child of panel (legacy behavior) """ left_panel = find_one(panel_no_title.render(), Div(id=panel_no_title.get_ids().panel("left"))) hide_icons = find(left_panel, Div(cls=Contains("mf-panel-hide-icon"))) assert len(hide_icons) == 1, "Panel should contain exactly one hide 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 """ show_icon = find_one(panel.render(), 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 show_icon = find_one(panel.render(), 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 show_icon = find_one(panel.render(), 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) @pytest.mark.parametrize("side, conf_kwargs", [ ("left", {"show_display_left": False}), ("right", {"show_display_right": False}), ]) def test_show_icon_not_in_main_panel_when_show_display_disabled(self, root_instance, side, conf_kwargs): """Test that show icon is not rendered when show_display is disabled. Why these elements matter: - Absence of show icon: When show_display_* is False, the icon should not exist - This prevents users from showing the panel via UI (only programmatically) """ custom_conf = PanelConf(left=True, right=True, **conf_kwargs) panel = Panel(root_instance, conf=custom_conf) panel.set_main(Div("Main content")) rendered = panel.render() show_icons = find(rendered, Div(id=f"{panel._id}_show_{side}")) assert len(show_icons) == 0, f"Show icon for {side} should not be present when show_display_{side}=False" # 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 + inner main div + show_icon_right - Order: Show icons must be positioned correctly (left then right) - cls="mf-panel-main": CSS class for main panel styling - Inner div with id: Main content wrapper for HTMX targeting """ # Find all Divs with cls="mf-panel-main" (there are 2: outer wrapper and inner content) main_panels = find(panel.render(), Div(cls=Contains("mf-panel-main"))) assert len(main_panels) == 2, "Should find outer wrapper and inner content div" # The outer wrapper is the first one (depth-first search) main_panel = main_panels[0] # Step 1: Validate main panel structure expected = Div( Div(id=f"{panel._id}_show_left"), # show icon left Div(id=panel.get_ids().main), # inner main content wrapper 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)