"""Unit tests for TreeView component.""" import shutil import pytest 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 .conftest import root_instance @pytest.fixture(autouse=True) def cleanup_db(): shutil.rmtree(".myFastHtmlDb", ignore_errors=True) class TestTreeviewBehaviour: """Tests for TreeView behavior and logic.""" def test_i_can_create_tree_node_with_auto_generated_id(self): """Test that TreeNode generates UUID automatically.""" node = TreeNode(label="Test Node", type="folder") assert node.id is not None assert isinstance(node.id, str) assert len(node.id) > 0 def test_i_can_create_tree_node_with_default_values(self): """Test that TreeNode has correct default values.""" node = TreeNode() assert node.label == "" assert node.type == "default" assert node.parent is None assert node.children == [] def test_i_can_initialize_tree_view_state(self, root_instance): """Test that TreeViewState initializes with default values.""" tree_view = TreeView(root_instance) state = tree_view._state assert isinstance(state.items, dict) assert len(state.items) == 0 assert state.opened == [] assert state.selected is None assert state.editing is None assert state.icon_config == {} def test_i_can_create_empty_treeview(self, root_instance): """Test creating an empty TreeView.""" tree_view = TreeView(root_instance) assert tree_view is not None assert len(tree_view._state.items) == 0 def test_i_can_add_node_to_treeview(self, root_instance): """Test adding a root node to the TreeView.""" tree_view = TreeView(root_instance) node = TreeNode(label="Root Node", type="folder") tree_view.add_node(node) assert node.id in tree_view._state.items assert tree_view._state.items[node.id].label == "Root Node" assert tree_view._state.items[node.id].parent is None def test_i_can_add_child_node(self, root_instance): """Test adding a child node to a parent.""" tree_view = TreeView(root_instance) 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) assert child.id in tree_view._state.items assert child.id in parent.children assert child.parent == parent.id def test_i_can_set_icon_config(self, root_instance): """Test setting icon configuration.""" tree_view = TreeView(root_instance) config = { "folder": "fluent.folder", "file": "fluent.document" } tree_view.set_icon_config(config) assert tree_view._state.icon_config == config assert tree_view._state.icon_config["folder"] == "fluent.folder" def test_i_can_toggle_node(self, root_instance): """Test expand/collapse a node.""" tree_view = TreeView(root_instance) node = TreeNode(label="Node", type="folder") tree_view.add_node(node) # Initially closed assert node.id not in tree_view._state.opened # Toggle to open tree_view._toggle_node(node.id) assert node.id in tree_view._state.opened # Toggle to close tree_view._toggle_node(node.id) assert node.id not in tree_view._state.opened def test_i_can_expand_all_nodes(self, root_instance): """Test that expand_all opens all nodes with children.""" tree_view = TreeView(root_instance) # Create hierarchy: root -> child1 -> grandchild # -> child2 (leaf) root = TreeNode(label="Root", type="folder") child1 = TreeNode(label="Child 1", type="folder") grandchild = TreeNode(label="Grandchild", type="file") child2 = TreeNode(label="Child 2", type="file") tree_view.add_node(root) tree_view.add_node(child1, parent_id=root.id) tree_view.add_node(grandchild, parent_id=child1.id) tree_view.add_node(child2, parent_id=root.id) # Initially all closed assert len(tree_view._state.opened) == 0 # Expand all tree_view.expand_all() # Nodes with children should be opened assert root.id in tree_view._state.opened assert child1.id in tree_view._state.opened # Leaf nodes should not be in opened list assert grandchild.id not in tree_view._state.opened assert child2.id not in tree_view._state.opened def test_i_can_select_node(self, root_instance): """Test selecting a node.""" tree_view = TreeView(root_instance) node = TreeNode(label="Node", type="folder") tree_view.add_node(node) tree_view._select_node(node.id) assert tree_view._state.selected == node.id def test_i_can_start_rename_node(self, root_instance): """Test starting rename mode for a node.""" tree_view = TreeView(root_instance) node = TreeNode(label="Old Name", type="folder") tree_view.add_node(node) tree_view._start_rename(node.id) assert tree_view._state.editing == node.id def test_i_can_save_rename_node(self, root_instance): """Test saving renamed node with new label.""" tree_view = TreeView(root_instance) node = TreeNode(label="Old Name", type="folder") tree_view.add_node(node) tree_view._start_rename(node.id) tree_view._save_rename(node.id, "New Name") assert tree_view._state.items[node.id].label == "New Name" assert tree_view._state.editing is None def test_i_can_cancel_rename_node(self, root_instance): """Test canceling rename operation.""" tree_view = TreeView(root_instance) node = TreeNode(label="Name", type="folder") tree_view.add_node(node) tree_view._start_rename(node.id) tree_view._cancel_rename() assert tree_view._state.editing is None assert tree_view._state.items[node.id].label == "Name" def test_i_can_delete_leaf_node(self, root_instance): """Test deleting a node without children.""" tree_view = TreeView(root_instance) 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) # Delete child (leaf node) tree_view._delete_node(child.id) assert child.id not in tree_view._state.items assert child.id not in parent.children def test_i_can_add_sibling_node(self, root_instance): """Test adding a sibling node.""" tree_view = TreeView(root_instance) parent = TreeNode(label="Parent", type="folder") child1 = TreeNode(label="Child 1", type="file") tree_view.add_node(parent) tree_view.add_node(child1, parent_id=parent.id) # Add sibling to child1 tree_view._add_sibling(child1.id, new_label="Child 2") assert len(parent.children) == 2 # Sibling should be after child1 assert parent.children.index(child1.id) < len(parent.children) - 1 def test_i_cannot_delete_node_with_children(self, root_instance): """Test that deleting a node with children raises an error.""" tree_view = TreeView(root_instance) 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) # Try to delete parent (has children) with pytest.raises(ValueError, match="Cannot delete node.*with children"): tree_view._delete_node(parent.id) def test_i_cannot_add_sibling_to_root(self, root_instance): """Test that adding sibling to root node raises an error.""" tree_view = TreeView(root_instance) root = TreeNode(label="Root", type="folder") tree_view.add_node(root) # Try to add sibling to root (no parent) with pytest.raises(ValueError, match="Cannot add sibling to root node"): tree_view._add_sibling(root.id) def test_i_cannot_select_nonexistent_node(self, root_instance): """Test that selecting a nonexistent node raises an error.""" tree_view = TreeView(root_instance) # Try to select node that doesn't exist with pytest.raises(ValueError, match="Node.*does not exist"): tree_view._select_node("nonexistent_id") def test_add_node_prevents_duplicate_children(self, root_instance): """Test that add_node prevents adding duplicate child IDs.""" tree_view = TreeView(root_instance) 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) # Try to add the same child again tree_view.add_node(child, parent_id=parent.id) # Child should appear only once in parent's children list assert parent.children.count(child.id) == 1 def test_sibling_is_inserted_at_correct_position(self, root_instance): """Test that _add_sibling inserts sibling exactly after reference node.""" tree_view = TreeView(root_instance) parent = TreeNode(label="Parent", type="folder") child1 = TreeNode(label="Child 1", type="file") child3 = TreeNode(label="Child 3", type="file") tree_view.add_node(parent) tree_view.add_node(child1, parent_id=parent.id) tree_view.add_node(child3, parent_id=parent.id) # Add sibling after child1 tree_view._add_sibling(child1.id, new_label="Child 2") # Get the newly added sibling sibling_id = parent.children[1] # Verify order: child1, sibling (child2), child3 assert parent.children[0] == child1.id assert tree_view._state.items[sibling_id].label == "Child 2" assert parent.children[2] == child3.id assert len(parent.children) == 3 def test_add_child_auto_expands_parent(self, root_instance): """Test that _add_child automatically expands the parent node.""" tree_view = TreeView(root_instance) parent = TreeNode(label="Parent", type="folder") tree_view.add_node(parent) # Parent should not be expanded initially assert parent.id not in tree_view._state.opened # Add child tree_view._add_child(parent.id, new_label="Child") # Parent should now be expanded assert parent.id in tree_view._state.opened def test_i_cannot_add_child_to_nonexistent_parent(self, root_instance): """Test that adding child to nonexistent parent raises error.""" tree_view = TreeView(root_instance) # Try to add child to parent that doesn't exist with pytest.raises(ValueError, match="Parent node.*does not exist"): tree_view._add_child("nonexistent_parent_id") def test_delete_node_clears_selection_if_selected(self, root_instance): """Test that deleting a selected node clears the selection.""" tree_view = TreeView(root_instance) 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) # Select the child tree_view._select_node(child.id) assert tree_view._state.selected == child.id # Delete the selected child tree_view._delete_node(child.id) # Selection should be cleared assert tree_view._state.selected is None def test_delete_node_removes_from_opened_if_expanded(self, root_instance): """Test that deleting an expanded node removes it from opened list.""" tree_view = TreeView(root_instance) 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) # Expand the parent tree_view._toggle_node(parent.id) assert parent.id in tree_view._state.opened # Delete the child (making parent a leaf) tree_view._delete_node(child.id) # Now delete the parent (now a leaf node) # First remove it from root by creating a grandparent grandparent = TreeNode(label="Grandparent", type="folder") tree_view.add_node(grandparent) parent.parent = grandparent.id grandparent.children.append(parent.id) tree_view._delete_node(parent.id) # Parent should be removed from opened list assert parent.id not in tree_view._state.opened def test_i_cannot_start_rename_nonexistent_node(self, root_instance): """Test that starting rename on nonexistent node raises error.""" tree_view = TreeView(root_instance) # Try to start rename on node that doesn't exist with pytest.raises(ValueError, match="Node.*does not exist"): tree_view._start_rename("nonexistent_id") def test_i_cannot_save_rename_nonexistent_node(self, root_instance): """Test that saving rename for nonexistent node raises error.""" tree_view = TreeView(root_instance) # Try to save rename for node that doesn't exist with pytest.raises(ValueError, match="Node.*does not exist"): tree_view._save_rename("nonexistent_id", "New Name") def test_i_cannot_add_sibling_to_nonexistent_node(self, root_instance): """Test that adding sibling to nonexistent node raises error.""" tree_view = TreeView(root_instance) # 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.""" @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: - TestObject Keyboard: Essential for keyboard shortcuts (Escape to cancel rename) - _id: Required for HTMX targeting and component identification - cls "mf-treeview": Root CSS class for TreeView styling """ expected = Div( TestObject(Keyboard, combinations={"esc": TestCommand("CancelRename")}), _id=tree_view.get_id(), cls="mf-treeview" ) assert matches(tree_view.__ft__(), expected) def test_node_with_children_collapsed_is_rendered(self, tree_view): """Test that a collapsed node with children renders correctly. Why these elements matter: - TestIcon chevron_right: Indicates visually that the node is collapsed - Span with label: Displays the node's text content - Action buttons (add_child, edit, delete): Enable user interactions - cls "mf-treenode": Required CSS class for node styling - data_node_id: Essential for identifying the node in DOM operations - No children in container: Verifies children are hidden when collapsed """ 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() # Step 2: Define expected structure expected = Div( 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 ), id=tree_view.get_id() ) # Step 3: Compare assert matches(rendered, expected) # Verify no children are rendered (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. Why these elements matter: - 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") child1 = TreeNode(label="Child1", type="file") child2 = TreeNode(label="Child2", type="file") tree_view.add_node(parent) 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_container = find_one(rendered, Div(data_node_id=parent.id)) expected = Div( 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_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_container, expected_child_container) def test_leaf_node_is_rendered(self, tree_view): """Test that a leaf node (no children) renders without toggle icon. Why these elements matter: - No toggle icon (or empty space): Leaf nodes don't need expand/collapse functionality - Span with label: Displays the node's text content - Action buttons present: Even leaf nodes can be edited, deleted, or receive children """ 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_container = find_one(rendered, Div(data_node_id=leaf.id)) # Step 2: Define expected structure expected = Div( 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_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) rendered = tree_view.render() selected_container = find_one(rendered, Div(data_node_id=node.id)) expected = Div( 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 ) 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. Why these elements matter: - Input element: Enables user to modify the node label inline - cls "mf-treenode-input": Required CSS class for input field styling - name "node_label": Essential for form data submission - value with current label: Pre-fills the input with existing text - cls does NOT contain "selected": Avoids double highlighting during editing """ node = TreeNode(label="Edit Me", type="file") tree_view.add_node(node) tree_view._start_rename(node.id) rendered = tree_view.render() editing_container = find_one(rendered, Div(data_node_id=node.id)) expected = Div( 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="mf-treenode-container", data_node_id=node.id ) 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_info, no_selected) def test_node_indentation_increases_with_level(self, tree_view): """Test that node indentation increases correctly with hierarchy level. Why these elements matter: - style Contains "padding-left: 0px": Root node has no indentation - 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"))) assert len(root_containers) == 2, "Should have two root-level containers" root1_container = find_one(rendered, Div(data_node_id=root1.id)) root2_container = find_one(rendered, Div(data_node_id=root2.id)) 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 ) 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)