Working on Treeview unit tests
This commit is contained in:
@@ -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
|
||||
```
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user