"""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 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") class TestTreeViewRender: """Tests for TreeView HTML rendering.""" def test_empty_treeview_is_rendered(self, root_instance): """Test that TreeView generates correct HTML structure.""" tree_view = TreeView(root_instance) 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_action_buttons_are_rendered(self): """Test that action buttons are present in rendered HTML.""" # Signature only - implementation later pass