Added tests for Layout and Treeview

This commit is contained in:
2025-12-05 17:46:15 +01:00
parent 7c701a9116
commit 8f2528787a
21 changed files with 1161 additions and 229 deletions

View File

@@ -5,7 +5,9 @@ import pytest
from fasthtml.components import *
from myfasthtml.controls.Layout import Layout
from myfasthtml.test.matcher import matches, find, Contains, find_one, TestIcon, TestScript
from myfasthtml.controls.UserProfile import UserProfile
from myfasthtml.test.matcher import matches, find, Contains, find_one, TestIcon, TestScript, TestObject, AnyValue, Skip, \
TestIconNotStr
from .conftest import root_instance
@@ -236,11 +238,12 @@ class TestLayoutRender:
"""Test that Layout renders with all main structural sections.
Why these elements matter:
- 6 children: Verifies all main sections are rendered (header, drawers, main, footer, script)
- 7 children: Verifies all main sections are rendered (tooltip container, header, drawers, main, footer, script)
- _id: Essential for layout identification and resizer initialization
- cls="mf-layout": Root CSS class for layout styling
"""
expected = Div(
Div(), # tooltip container
Header(),
Div(), # left drawer
Main(),
@@ -286,7 +289,7 @@ class TestLayoutRender:
expected = Header(
Div(
TestIcon("panel_right_expand20_regular"),
TestIcon("PanelLeftContract20Regular"),
cls="flex gap-1"
),
cls="mf-layout-header"
@@ -343,7 +346,7 @@ class TestLayoutRender:
expected = Div(
_id=f"{layout._id}_ld",
cls=Contains("collapsed"),
cls=Contains("mf-layout-drawer", "mf-layout-left-drawer", "collapsed"),
style=Contains("width: 0px")
)
@@ -382,7 +385,7 @@ class TestLayoutRender:
expected = Div(
_id=f"{layout._id}_rd",
cls=Contains("collapsed"),
cls=Contains("mf-layout-drawer", "mf-layout-right-drawer", "collapsed"),
style=Contains("width: 0px")
)
@@ -425,34 +428,250 @@ class TestLayoutRender:
resizers = find(drawer, Div(cls=Contains("mf-resizer-right")))
assert len(resizers) == 1, "Right drawer should contain exactly one resizer element"
def test_drawer_groups_are_separated_by_dividers(self, layout):
"""Test that multiple groups in drawer are separated by divider elements.
Why this test matters:
- Dividers provide visual separation between content groups
- At least one divider should exist when multiple groups are present
"""
layout.left_drawer.add(Div("Item 1"), group="group1")
layout.left_drawer.add(Div("Item 2"), group="group2")
drawer = find(layout.render(), Div(id=f"{layout._id}_ld"))
content_wrappers = find(drawer, Div(cls="mf-layout-drawer-content"))
assert len(content_wrappers) == 1
content = content_wrappers[0]
dividers = find(content, Div(cls="divider"))
assert len(dividers) >= 1, "Groups should be separated by dividers"
def test_resizer_script_is_included(self, layout):
"""Test that resizer initialization script is included in render.
Why this test matters:
- Script element: Required to initialize resizer functionality
- Script contains initResizer call: Ensures resizer is activated for this layout instance
- Script contains initLayout call: Ensures layout is activated for this layout instance
"""
script = find_one(layout.render(), Script())
expected = TestScript(f"initResizer('{layout._id}');")
expected = TestScript(f"initLayout('{layout._id}');")
assert matches(script, expected)
def test_left_drawer_renders_content_with_groups(self, layout):
"""Test that left drawer renders content organized by groups with proper wrappers.
Why these elements matter:
- mf-layout-drawer-content wrapper: Required container for drawer scrolling behavior
- divider elements: Visual separation between content groups
- Group count validation: Ensures all added groups are rendered
"""
layout.left_drawer.add(Div("Item 1", id="item1"), group="group1")
layout.left_drawer.add(Div("Item 2", id="item2"), group="group2")
drawer = find_one(layout.render(), Div(id=f"{layout._id}_ld"))
content_wrappers = find(drawer, Div(cls="mf-layout-drawer-content"))
assert len(content_wrappers) == 1, "Left drawer should contain exactly one content wrapper"
content = content_wrappers[0]
dividers = find(content, Div(cls="divider"))
assert len(dividers) == 1, "Two groups should be separated by exactly one divider"
def test_header_left_renders_custom_content(self, layout):
"""Test that custom content added to header_left is rendered in the left header section.
Why these elements matter:
- id="{layout._id}_hl": Essential for HTMX targeting during updates
- cls Contains "flex": Ensures horizontal layout of header items
- Icon presence: Toggle drawer icon must always be first element
- Custom content: Verifies header_left.add() correctly renders content
"""
custom_content = Div("Custom Header", id="custom_header")
layout.header_left.add(custom_content)
header_left = find_one(layout.render(), Div(id=f"{layout._id}_hl"))
expected = Div(
TestIcon(""),
Skip(None),
Div("Custom Header", id="custom_header"),
id=f"{layout._id}_hl",
cls=Contains("flex", "gap-1")
)
assert matches(header_left, expected)
def test_header_right_renders_custom_content(self, layout):
"""Test that custom content added to header_right is rendered in the right header section.
Why these elements matter:
- id="{layout._id}_hr": Essential for HTMX targeting during updates
- cls Contains "flex": Ensures horizontal layout of header items
- Custom content: Verifies header_right.add() correctly renders content
- UserProfile component: Must always be last element in right header
"""
custom_content = Div("Custom Header Right", id="custom_header_right")
layout.header_right.add(custom_content)
header_right = find_one(layout.render(), Div(id=f"{layout._id}_hr"))
expected = Div(
Skip(None),
Div("Custom Header Right", id="custom_header_right"),
TestObject(UserProfile),
id=f"{layout._id}_hr",
cls=Contains("flex", "gap-1")
)
assert matches(header_right, expected)
def test_footer_left_renders_custom_content(self, layout):
"""Test that custom content added to footer_left is rendered in the left footer section.
Why these elements matter:
- id="{layout._id}_fl": Essential for HTMX targeting during updates
- cls Contains "flex": Ensures horizontal layout of footer items
- Custom content: Verifies footer_left.add() correctly renders content
"""
custom_content = Div("Custom Footer Left", id="custom_footer_left")
layout.footer_left.add(custom_content)
footer_left = find_one(layout.render(), Div(id=f"{layout._id}_fl"))
expected = Div(
Skip(None),
Div("Custom Footer Left", id="custom_footer_left"),
id=f"{layout._id}_fl",
cls=Contains("flex", "gap-1")
)
assert matches(footer_left, expected)
def test_footer_right_renders_custom_content(self, layout):
"""Test that custom content added to footer_right is rendered in the right footer section.
Why these elements matter:
- id="{layout._id}_fr": Essential for HTMX targeting during updates
- cls Contains "flex": Ensures horizontal layout of footer items
- Custom content: Verifies footer_right.add() correctly renders content
"""
custom_content = Div("Custom Footer Right", id="custom_footer_right")
layout.footer_right.add(custom_content)
footer_right = find_one(layout.render(), Div(id=f"{layout._id}_fr"))
expected = Div(
Skip(None),
Div("Custom Footer Right", id="custom_footer_right"),
id=f"{layout._id}_fr",
cls=Contains("flex", "gap-1")
)
assert matches(footer_right, expected)
def test_left_drawer_resizer_has_command_data(self, layout):
"""Test that left drawer resizer has correct data attributes for command binding.
Why these elements matter:
- data_command_id: JavaScript uses this to trigger width update command
- data_side="left": JavaScript needs this to identify which drawer to resize
- cls Contains "mf-resizer-left": CSS uses this for left-specific positioning
"""
drawer = find_one(layout.render(), Div(id=f"{layout._id}_ld"))
resizer = find_one(drawer, Div(cls=Contains("mf-resizer-left")))
expected = Div(
cls=Contains("mf-resizer", "mf-resizer-left"),
data_command_id=AnyValue(),
data_side="left"
)
assert matches(resizer, expected)
def test_right_drawer_resizer_has_command_data(self, layout):
"""Test that right drawer resizer has correct data attributes for command binding.
Why these elements matter:
- data_command_id: JavaScript uses this to trigger width update command
- data_side="right": JavaScript needs this to identify which drawer to resize
- cls Contains "mf-resizer-right": CSS uses this for right-specific positioning
"""
drawer = find_one(layout.render(), Div(id=f"{layout._id}_rd"))
resizer = find_one(drawer, Div(cls=Contains("mf-resizer-right")))
expected = Div(
cls=Contains("mf-resizer", "mf-resizer-right"),
data_command_id=AnyValue(),
data_side="right"
)
assert matches(resizer, expected)
def test_left_drawer_icon_changes_when_closed(self, layout):
"""Test that left drawer toggle icon changes from expand to collapse when drawer is closed.
Why these elements matter:
- id="{layout._id}_ldi": Required for HTMX swap-oob updates when toggling
- Icon type: Visual feedback to user about drawer state (expand icon when closed)
- Icon change: Validates that toggle_drawer returns correct icon
"""
layout._state.left_drawer_open = False
icon_div = find_one(layout.render(), Div(id=f"{layout._id}_ldi"))
expected = Div(
TestIconNotStr("panel_left_expand20_regular"),
id=f"{layout._id}_ldi"
)
assert matches(icon_div, expected)
def test_left_drawer_icon_changes_when_opne(self, layout):
"""Test that left drawer toggle icon changes from collapse to expand when drawer is open..
Why these elements matter:
- id="{layout._id}_ldi": Required for HTMX swap-oob updates when toggling
- Icon type: Visual feedback to user about drawer state (expand icon when closed)
- Icon change: Validates that toggle_drawer returns correct icon
"""
layout._state.left_drawer_open = True
icon_div = find_one(layout.render(), Div(id=f"{layout._id}_ldi"))
expected = Div(
TestIconNotStr("panel_left_contract20_regular"),
id=f"{layout._id}_ldi"
)
assert matches(icon_div, expected)
def test_tooltip_container_is_rendered(self, layout):
"""Test that tooltip container is rendered at the top of the layout.
Why these elements matter:
- id="tt_{layout._id}": JavaScript uses this to append dynamically created tooltips
- cls Contains "mf-tooltip-container": CSS positioning for tooltip overlay layer
- Presence verification: Tooltips won't work if container is missing
"""
tooltip_container = find_one(layout.render(), Div(id=f"tt_{layout._id}"))
expected = Div(
id=f"tt_{layout._id}",
cls=Contains("mf-tooltip-container")
)
assert matches(tooltip_container, expected)
def test_header_right_contains_user_profile(self, layout):
"""Test that UserProfile component is rendered in the right header section.
Why these elements matter:
- UserProfile component: Provides authentication and user menu functionality
- Position in header right: Conventional placement for user profile controls
- Count verification: Ensures component is not duplicated
"""
header_right = find_one(layout.render(), Div(id=f"{layout._id}_hr"))
user_profiles = find(header_right, TestObject(UserProfile))
assert len(user_profiles) == 1, "Header right should contain exactly one UserProfile component"
def test_layout_initialization_script_is_included(self, layout):
"""Test that layout initialization script is included in render output.
Why these elements matter:
- Script presence: Required to initialize layout behavior (resizers, drawers)
- initLayout() call: Activates JavaScript functionality for this layout instance
- Layout ID parameter: Ensures initialization targets correct layout
"""
script = find_one(layout.render(), Script())
expected = TestScript(f"initLayout('{layout._id}');")
assert matches(script, expected)

View File

@@ -6,7 +6,8 @@ from fasthtml.components import *
from myfasthtml.controls.Keyboard import Keyboard
from myfasthtml.controls.TreeView import TreeView, TreeNode
from myfasthtml.test.matcher import matches, TestObject, TestCommand, TestIcon, find_one, find, Contains, DoesNotContain
from myfasthtml.test.matcher import matches, TestObject, TestCommand, TestIcon, find_one, find, Contains, \
DoesNotContain
from .conftest import root_instance
@@ -376,12 +377,30 @@ class TestTreeviewBehaviour:
# Try to add sibling to node that doesn't exist
with pytest.raises(ValueError, match="Node.*does not exist"):
tree_view._add_sibling("nonexistent_id")
def test_i_can_initialize_with_items_dict(self, root_instance):
"""Test that TreeView can be initialized with a dictionary of items."""
node1 = TreeNode(label="Node 1", type="folder")
node2 = TreeNode(label="Node 2", type="file")
items = {node1.id: node1, node2.id: node2}
tree_view = TreeView(root_instance, items=items)
assert len(tree_view._state.items) == 2
assert tree_view._state.items[node1.id].label == "Node 1"
assert tree_view._state.items[node1.id].type == "folder"
assert tree_view._state.items[node2.id].label == "Node 2"
assert tree_view._state.items[node2.id].type == "file"
class TestTreeViewRender:
"""Tests for TreeView HTML rendering."""
def test_empty_treeview_is_rendered(self, root_instance):
@pytest.fixture
def tree_view(self, root_instance):
return TreeView(root_instance)
def test_empty_treeview_is_rendered(self, tree_view):
"""Test that empty TreeView generates correct HTML structure.
Why these elements matter:
@@ -389,23 +408,14 @@ class TestTreeViewRender:
- _id: Required for HTMX targeting and component identification
- cls "mf-treeview": Root CSS class for TreeView styling
"""
# Step 1: Create empty TreeView
tree_view = TreeView(root_instance)
# Step 2: Define expected structure
expected = Div(
TestObject(Keyboard, combinations={"esc": TestCommand("CancelRename")}),
_id=tree_view.get_id(),
cls="mf-treeview"
)
# Step 3: Compare
assert matches(tree_view.__ft__(), expected)
@pytest.fixture
def tree_view(self, root_instance):
return TreeView(root_instance)
def test_node_with_children_collapsed_is_rendered(self, tree_view):
"""Test that a collapsed node with children renders correctly.
@@ -419,35 +429,40 @@ class TestTreeViewRender:
"""
parent = TreeNode(label="Parent", type="folder")
child = TreeNode(label="Child", type="file")
tree_view.add_node(parent)
tree_view.add_node(child, parent_id=parent.id)
# Step 1: Extract the node element to test
rendered = tree_view.render()
node_container = find_one(rendered, Div(data_node_id=parent.id))
# Step 2: Define expected structure
expected = Div(
TestIcon("chevron_right20_regular"), # Collapsed toggle icon
Span("Parent"), # Label
Div( # Action buttons
TestIcon("add_circle20_regular"),
TestIcon("edit20_regular"),
TestIcon("delete20_regular"),
cls=Contains("mf-treenode-actions")
Div(
Div(
TestIcon("chevron_right20_regular"), # Collapsed toggle icon
Span("Parent"), # Label
Div( # Action buttons
TestIcon("add_circle20_regular"),
TestIcon("edit20_regular"),
TestIcon("delete20_regular"),
cls=Contains("mf-treenode-actions")
),
cls=Contains("mf-treenode"),
),
cls="mf-treenode-container",
data_node_id=parent.id
),
cls=Contains("mf-treenode"),
data_node_id=parent.id
id=tree_view.get_id()
)
# Step 3: Compare
assert matches(node_container, expected)
assert matches(rendered, expected)
# Verify no children are rendered (collapsed)
child_containers = find(node_container, Div(data_node_id=child.id))
assert len(child_containers) == 0, "Children should not be rendered when node is collapsed"
child_containers = find(rendered, Div(data_node_id=parent.id))
assert len(child_containers) == 1, "Children should not be rendered when node is collapsed"
def test_node_with_children_expanded_is_rendered(self, tree_view):
"""Test that an expanded node with children renders correctly.
@@ -455,40 +470,48 @@ class TestTreeViewRender:
- TestIcon chevron_down: Indicates visually that the node is expanded
- Children rendered: Verifies that child nodes are visible when parent is expanded
- Child has its own node structure: Ensures recursive rendering works correctly
Rendered Structure :
Div (node_container with data_node_id)
├─ Div (information on current node - icon, label, actions)
└─ Div* (children - recursive containers, only if expanded)
"""
parent = TreeNode(label="Parent", type="folder")
child = TreeNode(label="Child", type="file")
child1 = TreeNode(label="Child1", type="file")
child2 = TreeNode(label="Child2", type="file")
tree_view.add_node(parent)
tree_view.add_node(child, parent_id=parent.id)
tree_view.add_node(child1, parent_id=parent.id)
tree_view.add_node(child2, parent_id=parent.id)
tree_view._toggle_node(parent.id) # Expand the parent
# Step 1: Extract the parent node element to test
rendered = tree_view.render()
parent_node = find_one(rendered, Div(data_node_id=parent.id))
# Step 2: Define expected structure for toggle icon
parent_container = find_one(rendered, Div(data_node_id=parent.id))
expected = Div(
TestIcon("chevron_down20_regular"), # Expanded toggle icon
cls=Contains("mf-treenode")
Div(), # parent info (see test_node_with_children_collapsed_is_rendered)
Div(data_node_id=child1.id),
Div(data_node_id=child2.id),
)
# Step 3: Compare
assert matches(parent_node, expected)
# Verify children ARE rendered (expanded)
child_containers = find(rendered, Div(data_node_id=child.id))
assert len(child_containers) == 1, "Child should be rendered when parent is expanded"
# Verify child has proper node structure
child_node = child_containers[0]
child_expected = Div(
Span("Child"),
cls=Contains("mf-treenode"),
data_node_id=child.id
assert matches(parent_container, expected)
# now check the child node structure
child_container = find_one(rendered, Div(data_node_id=child1.id))
expected_child_container = Div(
Div(
Div(None), # No icon, the div is empty
Span("Child1"),
Div(), # action buttons
cls=Contains("mf-treenode")
),
cls="mf-treenode-container",
data_node_id=child1.id,
)
assert matches(child_node, child_expected)
assert matches(child_container, expected_child_container)
def test_leaf_node_is_rendered(self, tree_view):
"""Test that a leaf node (no children) renders without toggle icon.
@@ -499,51 +522,53 @@ class TestTreeViewRender:
"""
leaf = TreeNode(label="Leaf Node", type="file")
tree_view.add_node(leaf)
# Step 1: Extract the leaf node element to test
rendered = tree_view.render()
leaf_node = find_one(rendered, Div(data_node_id=leaf.id))
leaf_container = find_one(rendered, Div(data_node_id=leaf.id))
# Step 2: Define expected structure
expected = Div(
Span("Leaf Node"), # Label
Div( # Action buttons still present
TestIcon("add_circle20_regular"),
TestIcon("edit20_regular"),
TestIcon("delete20_regular"),
cls=Contains("mf-treenode-actions")
Div(
Div(None), # No icon, the div is empty
Span("Leaf Node"), # Label
Div(), # Action buttons still present
),
cls=Contains("mf-treenode"),
data_node_id=leaf.id
)
# Step 3: Compare
assert matches(leaf_node, expected)
assert matches(leaf_container, expected)
def test_selected_node_has_selected_class(self, tree_view):
"""Test that a selected node has the 'selected' CSS class.
Why these elements matter:
- cls Contains "selected": Enables visual highlighting of the selected node
- Div with mf-treenode: The node information container with selected class
- data_node_id: Required for identifying which node is selected
"""
node = TreeNode(label="Selected Node", type="file")
tree_view.add_node(node)
tree_view._select_node(node.id)
# Step 1: Extract the selected node element to test
rendered = tree_view.render()
selected_node = find_one(rendered, Div(data_node_id=node.id))
# Step 2: Define expected structure
selected_container = find_one(rendered, Div(data_node_id=node.id))
expected = Div(
cls=Contains("mf-treenode", "selected"),
Div(
Div(None), # No icon, leaf node
Span("Selected Node"),
Div(), # Action buttons
cls=Contains("mf-treenode", "selected")
),
cls="mf-treenode-container",
data_node_id=node.id
)
# Step 3: Compare
assert matches(selected_node, expected)
assert matches(selected_container, expected)
def test_node_in_editing_mode_shows_input(self, tree_view):
"""Test that a node in editing mode renders an Input instead of Span.
@@ -557,31 +582,34 @@ class TestTreeViewRender:
node = TreeNode(label="Edit Me", type="file")
tree_view.add_node(node)
tree_view._start_rename(node.id)
# Step 1: Extract the editing node element to test
rendered = tree_view.render()
editing_node = find_one(rendered, Div(data_node_id=node.id))
# Step 2: Define expected structure
editing_container = find_one(rendered, Div(data_node_id=node.id))
expected = Div(
Input(
name="node_label",
value="Edit Me",
cls=Contains("mf-treenode-input")
Div(
Div(None), # No icon, leaf node
Input(
name="node_label",
value="Edit Me",
cls=Contains("mf-treenode-input")
),
Div(), # Action buttons
cls=Contains("mf-treenode")
),
cls=Contains("mf-treenode"),
cls="mf-treenode-container",
data_node_id=node.id
)
# Step 3: Compare
assert matches(editing_node, expected)
assert matches(editing_container, expected)
# Verify "selected" class is NOT present
editing_node_info = find_one(editing_container, Div(cls=Contains("mf-treenode", _word=True)))
no_selected = Div(
cls=DoesNotContain("selected")
)
assert matches(editing_node, no_selected)
assert matches(editing_node_info, no_selected)
def test_node_indentation_increases_with_level(self, tree_view):
"""Test that node indentation increases correctly with hierarchy level.
@@ -590,38 +618,243 @@ class TestTreeViewRender:
- style Contains "padding-left: 20px": Child is indented by 20px
- style Contains "padding-left: 40px": Grandchild is indented by 40px
- Progressive padding: Creates the visual hierarchy of the tree structure
- Padding is applied to the node info Div, not the container
"""
root = TreeNode(label="Root", type="folder")
child = TreeNode(label="Child", type="folder")
grandchild = TreeNode(label="Grandchild", type="file")
tree_view.add_node(root)
tree_view.add_node(child, parent_id=root.id)
tree_view.add_node(grandchild, parent_id=child.id)
# Expand all to make hierarchy visible
tree_view._toggle_node(root.id)
tree_view._toggle_node(child.id)
rendered = tree_view.render()
# Test root node (level 0)
root_container = find_one(rendered, Div(data_node_id=root.id))
root_expected = Div(
Div(
TestIcon("chevron_down20_regular"), # Expanded icon
Span("Root"),
Div(), # Action buttons
cls=Contains("mf-treenode"),
style=Contains("padding-left: 0px")
),
cls="mf-treenode-container",
data_node_id=root.id
)
assert matches(root_container, root_expected)
# Test child node (level 1)
child_container = find_one(rendered, Div(data_node_id=child.id))
child_expected = Div(
Div(
TestIcon("chevron_down20_regular"), # Expanded icon
Span("Child"),
Div(), # Action buttons
cls=Contains("mf-treenode"),
style=Contains("padding-left: 20px")
),
cls="mf-treenode-container",
data_node_id=child.id
)
assert matches(child_container, child_expected)
# Test grandchild node (level 2)
grandchild_container = find_one(rendered, Div(data_node_id=grandchild.id))
grandchild_expected = Div(
Div(
Div(None), # No icon, leaf node
Span("Grandchild"),
Div(), # Action buttons
cls=Contains("mf-treenode"),
style=Contains("padding-left: 40px")
),
cls="mf-treenode-container",
data_node_id=grandchild.id
)
assert matches(grandchild_container, grandchild_expected)
@pytest.mark.skip(reason="Not possible to validate if the command unicity is not fixed")
def test_toggle_icon_has_correct_command(self, tree_view):
"""Test that toggle icon has ToggleNode command.
Why these elements matter:
- Div wrapper with command: mk.icon() wraps SVG in Div with HTMX attributes
- TestIcon inside Div: Verifies correct chevron icon is displayed
- TestCommand "ToggleNode": Essential for HTMX to route to correct handler
- Command targets correct node_id: Ensures the right node is toggled
"""
parent = TreeNode(label="Parent", type="folder")
child = TreeNode(label="Child", type="file")
tree_view.add_node(parent)
tree_view.add_node(child, parent_id=parent.id)
# Step 1: Extract the parent node element
rendered = tree_view.render()
parent_node = find_one(rendered, Div(data_node_id=parent.id))
# Step 2: Define expected structure
expected = Div(
Div(
TestIcon("chevron_right20_regular", command=tree_view.commands.toggle_node(parent.id)),
),
data_node_id=parent.id
)
# Step 3: Compare
assert matches(parent_node, expected)
@pytest.mark.skip(reason="Not possible to validate if the command unicity is not fixed")
def test_action_buttons_have_correct_commands(self, tree_view):
"""Test that action buttons have correct commands.
Why these elements matter:
- add_circle icon with AddChild: Enables adding child nodes via HTMX
- edit icon with StartRename: Triggers inline editing mode
- delete icon with DeleteNode: Enables node deletion
- cls "mf-treenode-actions": Required CSS class for button container styling
"""
node = TreeNode(label="Node", type="folder")
tree_view.add_node(node)
# Step 1: Extract the action buttons container
rendered = tree_view.render()
actions = find_one(rendered, Div(cls=Contains("mf-treenode-actions")))
# Step 2: Define expected structure
expected = Div(
TestIcon("add_circle20_regular", command=tree_view.commands.add_child(node.id)),
TestIcon("edit20_regular", command=tree_view.commands.start_rename(node.id)),
TestIcon("delete20_regular", command=tree_view.commands.delete_node(node.id)),
cls=Contains("mf-treenode-actions")
)
# Step 3: Compare
assert matches(actions, expected)
@pytest.mark.skip(reason="Not possible to validate if the command unicity is not fixed")
def test_label_has_select_command(self, tree_view):
"""Test that node label has SelectNode command.
Why these elements matter:
- Span with node label: Displays the node text
- TestCommand "SelectNode": Clicking label selects the node via HTMX
- cls "mf-treenode-label": Required CSS class for label styling
"""
node = TreeNode(label="Clickable Node", type="file")
tree_view.add_node(node)
# Step 1: Extract the label element
rendered = tree_view.render()
label = find_one(rendered, Span(cls=Contains("mf-treenode-label")))
# Step 2: Define expected structure
expected = Span(
"Clickable Node",
command=tree_view.commands.select_node(node.id),
cls=Contains("mf-treenode-label")
)
# Step 3: Compare
assert matches(label, expected)
@pytest.mark.skip(reason="Not possible to validate if the command unicity is not fixed")
def test_input_has_save_rename_command(self, tree_view):
"""Test that editing input has SaveRename command.
Why these elements matter:
- Input element: Enables inline editing of node label
- TestCommand "SaveRename": Submits new label via HTMX on form submission
- name "node_label": Required for form data to include the new label value
- value with current label: Pre-fills input with existing node text
"""
node = TreeNode(label="Edit Me", type="file")
tree_view.add_node(node)
tree_view._start_rename(node.id)
# Step 1: Extract the input element
rendered = tree_view.render()
input_elem = find_one(rendered, Input(name="node_label"))
# Step 2: Define expected structure
expected = Input(
name="node_label",
value="Edit Me",
command=TestCommand(tree_view.commands.save_rename(node.id)),
cls=Contains("mf-treenode-input")
)
# Step 3: Compare
assert matches(input_elem, expected)
def test_keyboard_has_cancel_rename_command(self, tree_view):
"""Test that Keyboard component has Escape key bound to CancelRename.
Why these elements matter:
- TestObject Keyboard: Verifies keyboard shortcuts component is present
- esc combination with CancelRename: Enables canceling rename with Escape key
- Essential for UX: Users expect Escape to cancel inline editing
"""
# Step 1: Extract the Keyboard component
rendered = tree_view.render()
keyboard = find_one(rendered, TestObject(Keyboard))
# Step 2: Define expected structure
expected = TestObject(Keyboard, combinations={"esc": TestCommand("CancelRename")})
# Step 3: Compare
assert matches(keyboard, expected)
def test_multiple_root_nodes_are_rendered(self, tree_view):
"""Test that multiple root nodes are rendered at the same level.
Why these elements matter:
- Multiple root nodes: Verifies TreeView supports forest structure (multiple trees)
- All at same level: No artificial parent wrapping root nodes
- Each root has its own container: Proper structure for multiple independent trees
"""
root1 = TreeNode(label="Root 1", type="folder")
root2 = TreeNode(label="Root 2", type="folder")
tree_view.add_node(root1)
tree_view.add_node(root2)
rendered = tree_view.render()
root_containers = find(rendered, Div(cls=Contains("mf-treenode-container")))
# Step 1 & 2 & 3: Test root node (level 0)
root_node = find_one(rendered, Div(data_node_id=root.id))
root_expected = Div(
style=Contains("padding-left: 0px")
)
assert matches(root_node, root_expected)
assert len(root_containers) == 2, "Should have two root-level containers"
# Step 1 & 2 & 3: Test child node (level 1)
child_node = find_one(rendered, Div(data_node_id=child.id))
child_expected = Div(
style=Contains("padding-left: 20px")
)
assert matches(child_node, child_expected)
root1_container = find_one(rendered, Div(data_node_id=root1.id))
root2_container = find_one(rendered, Div(data_node_id=root2.id))
# Step 1 & 2 & 3: Test grandchild node (level 2)
grandchild_node = find_one(rendered, Div(data_node_id=grandchild.id))
grandchild_expected = Div(
style=Contains("padding-left: 40px")
expected_root1 = Div(
Div(
Div(None), # No icon, leaf node
Span("Root 1"),
Div(), # Action buttons
cls=Contains("mf-treenode")
),
cls="mf-treenode-container",
data_node_id=root1.id
)
assert matches(grandchild_node, grandchild_expected)
expected_root2 = Div(
Div(
Div(None), # No icon, leaf node
Span("Root 2"),
Div(), # Action buttons
cls=Contains("mf-treenode")
),
cls="mf-treenode-container",
data_node_id=root2.id
)
assert matches(root1_container, expected_root1)
assert matches(root2_container, expected_root2)