From e34d675e38d72dedb4fbce27080a2271a75322a0 Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Sun, 30 Nov 2025 23:12:12 +0100 Subject: [PATCH] Working on Treeview unit tests --- docs/Layout.md | 24 ++-- tests/controls/test_treeview.py | 243 +++++++++++++++++++++++++++++++- 2 files changed, 249 insertions(+), 18 deletions(-) diff --git a/docs/Layout.md b/docs/Layout.md index 11b3c9d..52c0527 100644 --- a/docs/Layout.md +++ b/docs/Layout.md @@ -37,15 +37,17 @@ This is only one instance per session. ## High Level Hierarchical Structure ``` -MyFastHtml -├── src -│ ├── myfasthtml/ # Main library code -│ │ ├── core/commands.py # Command definitions -│ │ ├── controls/button.py # Control helpers -│ │ └── pages/LoginPage.py # Predefined Login page -│ └── ... -├── tests # Unit and integration tests -├── LICENSE # License file (MIT) -├── README.md # Project documentation -└── pyproject.toml # Build configuration +Div(id="layout") +├── Header +│ ├── Div(id="layout_hl") +│ │ ├── Icon # Left drawer icon button +│ │ └── Div # Left content for the header +│ └── Div(id="layout_hr") +│ ├── Div # Right content for the header +│ └── UserProfile # user profile icon button +├── Div # Left Drawer +├── Main # Main content +├── Div # Right Drawer +├── Footer # Footer +└── Script # To initialize the resizing ``` \ No newline at end of file diff --git a/tests/controls/test_treeview.py b/tests/controls/test_treeview.py index 5fa95c3..2e9641d 100644 --- a/tests/controls/test_treeview.py +++ b/tests/controls/test_treeview.py @@ -6,7 +6,7 @@ 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 myfasthtml.test.matcher import matches, TestObject, TestCommand, TestIcon, find_one, find, Contains, DoesNotContain from .conftest import root_instance @@ -382,17 +382,246 @@ class TestTreeViewRender: """Tests for TreeView HTML rendering.""" def test_empty_treeview_is_rendered(self, root_instance): - """Test that TreeView generates correct HTML structure.""" + """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 + """ + # 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) - def test_node_action_buttons_are_rendered(self): - """Test that action buttons are present in rendered HTML.""" - # Signature only - implementation later - pass + @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. + + 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() + 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") + ), + cls=Contains("mf-treenode"), + data_node_id=parent.id + ) + + # Step 3: Compare + assert matches(node_container, 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" + + 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 + """ + 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) + 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 + expected = Div( + TestIcon("chevron_down20_regular"), # Expanded toggle icon + cls=Contains("mf-treenode") + ) + + # 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(child_node, child_expected) + + 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_node = 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") + ), + cls=Contains("mf-treenode"), + data_node_id=leaf.id + ) + + # Step 3: Compare + assert matches(leaf_node, 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 + - 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 + expected = Div( + cls=Contains("mf-treenode", "selected"), + data_node_id=node.id + ) + + # Step 3: Compare + assert matches(selected_node, 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) + + # 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 + expected = Div( + Input( + name="node_label", + value="Edit Me", + cls=Contains("mf-treenode-input") + ), + cls=Contains("mf-treenode"), + data_node_id=node.id + ) + + # Step 3: Compare + assert matches(editing_node, expected) + + # Verify "selected" class is NOT present + no_selected = Div( + cls=DoesNotContain("selected") + ) + assert matches(editing_node, 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 + """ + 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() + + # 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) + + # 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) + + # 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") + ) + assert matches(grandchild_node, grandchild_expected)