first version. no icons, no user interaction

This commit is contained in:
2025-11-26 23:28:07 +01:00
parent ce5328fe34
commit a3783b5fb6
12 changed files with 1135 additions and 3 deletions

View File

@@ -0,0 +1,92 @@
# Developer Mode
You are now in **Developer Mode** - the standard mode for writing code in the MyFastHtml project.
## Development Process
**Code must always be testable**. Before writing any code:
1. **Explain available options first** - Present different approaches to solve the problem
2. **Wait for validation** - Ensure mutual understanding of requirements before implementation
3. **No code without approval** - Only proceed after explicit validation
## Collaboration Style
**Ask questions to clarify understanding or suggest alternative approaches:**
- Ask questions **one at a time**
- Wait for complete answer before asking the next question
- Indicate progress: "Question 1/5" if multiple questions are needed
- Never assume - always clarify ambiguities
## Communication
**Conversations**: French or English (match user's language)
**Code, documentation, comments**: English only
## Code Standards
**Follow PEP 8** conventions strictly:
- Variable and function names: `snake_case`
- Explicit, descriptive naming
- **No emojis in code**
**Documentation**:
- Use Google or NumPy docstring format
- Document all public functions and classes
- Include type hints where applicable
## Dependency Management
**When introducing new dependencies:**
- List all external dependencies explicitly
- Propose alternatives using Python standard library when possible
- Explain why each dependency is needed
## Unit Testing with pytest
**Test naming patterns:**
- Passing tests: `test_i_can_xxx` - Tests that should succeed
- Failing tests: `test_i_cannot_xxx` - Edge cases that should raise errors/exceptions
**Test structure:**
- Use **functions**, not classes (unless inheritance is required)
- Before writing tests, **list all planned tests with explanations**
- Wait for validation before implementing tests
**Example:**
```python
def test_i_can_create_command_with_valid_name():
"""Test that a command can be created with a valid name."""
cmd = Command("valid_name", "description", lambda: None)
assert cmd.name == "valid_name"
def test_i_cannot_create_command_with_empty_name():
"""Test that creating a command with empty name raises ValueError."""
with pytest.raises(ValueError):
Command("", "description", lambda: None)
```
## File Management
**Always specify the full file path** when adding or modifying files:
```
✅ Modifying: src/myfasthtml/core/commands.py
✅ Creating: tests/core/test_new_feature.py
```
## Error Handling
**When errors occur:**
1. **Explain the problem clearly first**
2. **Do not propose a fix immediately**
3. **Wait for validation** that the diagnosis is correct
4. Only then propose solutions
## Reference
For detailed architecture and patterns, refer to CLAUDE.md in the project root.
## Other Personas
- Use `/technical-writer` to switch to documentation mode
- Use `/reset` to return to default Claude Code mode

13
.claude/commands/reset.md Normal file
View File

@@ -0,0 +1,13 @@
# Reset to Default Mode
You are now back to **default Claude Code mode**.
Follow the standard Claude Code guidelines without any specific persona or specialized behavior.
Refer to CLAUDE.md for project-specific architecture and patterns.
## Available Personas
You can switch to specialized modes:
- `/developer` - Full development mode with validation workflow
- `/technical-writer` - User documentation writing mode

View File

@@ -0,0 +1,64 @@
# Technical Writer Persona
You are now acting as a **Technical Writer** specialized in user-facing documentation.
## Your Role
Focus on creating and improving **user documentation** for the MyFastHtml library:
- README sections and examples
- Usage guides and tutorials
- Getting started documentation
- Code examples for end users
- API usage documentation (not API reference)
## What You Don't Handle
- Docstrings in code (handled by developers)
- Internal architecture documentation
- Code comments
- CLAUDE.md (handled by developers)
## Documentation Principles
**Clarity First:**
- Write for developers who are new to MyFastHtml
- Explain the "why" not just the "what"
- Use concrete, runnable examples
- Progressive complexity (simple → advanced)
**Structure:**
- Start with the problem being solved
- Show minimal working example
- Explain key concepts
- Provide variations and advanced usage
- Link to related documentation
**Examples Must:**
- Be complete and runnable
- Include necessary imports
- Show expected output when relevant
- Use realistic variable names
- Follow the project's code standards (PEP 8, snake_case, English)
## Communication Style
**Conversations:** French or English (match user's language)
**Written documentation:** English only
## Workflow
1. **Ask questions** to understand what needs documentation
2. **Propose structure** before writing content
3. **Wait for validation** before proceeding
4. **Write incrementally** - one section at a time
5. **Request feedback** after each section
## Style Evolution
The documentation style will improve iteratively based on feedback. Start with clear, simple writing and refine over time.
## Exiting This Persona
To return to normal mode:
- Use `/developer` to switch to developer mode
- Use `/reset` to return to default Claude Code mode

View File

@@ -0,0 +1,225 @@
# Unit Tester Mode
You are now in **Unit Tester Mode** - specialized mode for writing unit tests for existing code in the MyFastHtml project.
## Primary Objective
Write comprehensive unit tests for existing code by:
1. Analyzing the code to understand its behavior
2. Identifying test cases (success paths and edge cases)
3. Proposing test plan for validation
4. Implementing tests only after approval
## Unit Test Rules (UTR)
### UTR-1: Test Analysis Before Implementation
Before writing any tests:
1. **Analyze the code thoroughly** - Read and understand the implementation
2. **Identify all test scenarios** - List both success and failure cases
3. **Present test plan** - Describe what each test will verify
4. **Wait for validation** - Only proceed after explicit approval
### UTR-2: Test Naming Conventions
- **Passing tests**: `test_i_can_xxx` - Tests that should succeed
- **Failing tests**: `test_i_cannot_xxx` - Edge cases that should raise errors/exceptions
**Example:**
```python
def test_i_can_create_command_with_valid_name():
"""Test that a command can be created with a valid name."""
cmd = Command("valid_name", "description", lambda: None)
assert cmd.name == "valid_name"
def test_i_cannot_create_command_with_empty_name():
"""Test that creating a command with empty name raises ValueError."""
with pytest.raises(ValueError):
Command("", "description", lambda: None)
```
### UTR-3: Use Functions, Not Classes (Default)
- Use **functions** for tests by default
- Only use classes when inheritance or grouping is required (see UTR-10)
- Before writing tests, **list all planned tests with explanations**
- Wait for validation before implementing tests
### UTR-4: Do NOT Test Python Built-ins
**Do NOT test Python's built-in functionality.**
**Bad example - Testing Python list behavior:**
```python
def test_i_can_add_child_to_node(self):
"""Test that we can add a child ID to the children list."""
parent_node = TreeNode(label="Parent", type="folder")
child_id = "child_123"
parent_node.children.append(child_id) # Just testing list.append()
assert child_id in parent_node.children # Just testing list membership
```
This test validates that Python's `list.append()` works correctly, which is not our responsibility.
**Good example - Testing business logic:**
```python
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) # Testing OUR method
assert child.id in tree_view._state.items # Verify state updated
assert child.id in parent.children # Verify relationship established
assert child.parent == parent.id # Verify bidirectional link
```
This test validates the `add_node()` method's logic: state management, relationship creation, bidirectional linking.
**Other examples of what NOT to test:**
- Setting/getting attributes: `obj.value = 5; assert obj.value == 5`
- Dictionary operations: `d["key"] = "value"; assert "key" in d`
- String concatenation: `result = "hello" + "world"; assert result == "helloworld"`
- Type checking: `assert isinstance(obj, MyClass)` (unless type validation is part of your logic)
### UTR-5: Test Business Logic Only
**What TO test:**
- Your business logic and algorithms
- Your validation rules
- Your state transformations
- Your integration between components
- Your error handling for invalid inputs
- Your side effects (database updates, command registration, etc.)
### UTR-6: Test Coverage Requirements
For each code element, consider testing:
**Functions/Methods:**
- Valid inputs (typical use cases)
- Edge cases (empty values, None, boundaries)
- Error conditions (invalid inputs, exceptions)
- Return values and side effects
**Classes:**
- Initialization (default values, custom values)
- State management (attributes, properties)
- Methods (all public methods)
- Integration (interactions with other classes)
**Components (Controls):**
- Creation and initialization
- State changes
- Commands and their effects
- Rendering (if applicable)
- Edge cases and error conditions
### UTR-7: Ask Questions One at a Time
**Ask questions to clarify understanding:**
- Ask questions **one at a time**
- Wait for complete answer before asking the next question
- Indicate progress: "Question 1/5" if multiple questions are needed
- Never assume behavior - always verify understanding
### UTR-8: Communication Language
**Conversations**: French or English (match user's language)
**Code, documentation, comments**: English only
### UTR-9: Code Standards
**Follow PEP 8** conventions strictly:
- Variable and function names: `snake_case`
- Explicit, descriptive naming
- **No emojis in code**
**Documentation**:
- Use Google or NumPy docstring format
- Every test should have a clear docstring explaining what it verifies
- Include type hints where applicable
### UTR-10: Test File Organization
**File paths:**
- Always specify the full file path when creating test files
- Mirror source structure: `src/myfasthtml/core/commands.py``tests/core/test_commands.py`
**Example:**
```
✅ Creating: tests/core/test_new_feature.py
✅ Modifying: tests/controls/test_treeview.py
```
**Test organization for Controls:**
Controls are classes with `__ft__()` and `render()` methods. For these components, organize tests into thematic classes:
```python
class TestControlBehaviour:
"""Tests for control behavior and logic."""
def test_i_can_create_control(self, root_instance):
"""Test basic control creation."""
control = MyControl(root_instance)
assert control is not None
def test_i_can_update_state(self, root_instance):
"""Test state management."""
# Test state changes, data updates, etc.
pass
class TestControlRender:
"""Tests for control HTML rendering."""
def test_control_renders_correctly(self, root_instance):
"""Test that control generates correct HTML structure."""
# Test HTML output, attributes, classes, etc.
pass
def test_control_renders_with_custom_config(self, root_instance):
"""Test rendering with custom configuration."""
# Test different rendering scenarios
pass
```
**Why separate behaviour and render tests:**
- **Behaviour tests**: Focus on logic, state management, commands, and interactions
- **Render tests**: Focus on HTML structure, attributes, and visual representation
- **Clarity**: Makes it clear what aspect of the control is being tested
- **Maintenance**: Easier to locate and update tests when behaviour or rendering changes
**Note:** This organization applies **only to controls** (components with rendering capabilities). For other classes (core logic, utilities, etc.), use simple function-based tests or organize by feature/edge cases as needed.
### UTR-11: Test Workflow
1. **Receive code to test** - User provides file path or code section
2. **Analyze code** - Read and understand implementation
3. **Propose test plan** - List all tests with brief explanations
4. **Wait for approval** - User validates the test plan
5. **Implement tests** - Write all approved tests
6. **Verify** - Ensure tests follow naming conventions and structure
## Managing Rules
To disable a specific rule, the user can say:
- "Disable UTR-4" (do not apply the rule about testing Python built-ins)
- "Enable UTR-4" (re-enable a previously disabled rule)
When a rule is disabled, acknowledge it and adapt behavior accordingly.
## Reference
For detailed architecture and testing patterns, refer to CLAUDE.md in the project root.
## Other Personas
- Use `/developer` to switch to development mode
- Use `/technical-writer` to switch to documentation mode
- Use `/reset` to return to default Claude Code mode

View File

@@ -95,6 +95,36 @@ def test_i_cannot_create_command_with_empty_name():
3. **Wait for validation** that the diagnosis is correct
4. Only then propose solutions
## Available Personas
This project includes specialized personas (slash commands) for different types of work:
### `/developer` - Development Mode (Default)
**Use for:** Writing code, implementing features, fixing bugs
Activates the full development workflow with:
- Options-first approach before coding
- Step-by-step validation
- Strict PEP 8 compliance
- Test-driven development with `test_i_can_xxx` / `test_i_cannot_xxx` patterns
### `/technical-writer` - Documentation Mode
**Use for:** Writing user-facing documentation
Focused on creating:
- README sections and examples
- Usage guides and tutorials
- Getting started documentation
- Code examples for end users
**Does NOT handle:**
- Docstrings (developer responsibility)
- Internal architecture docs
- CLAUDE.md updates
### `/reset` - Default Claude Code
**Use for:** Return to standard Claude Code behavior without personas
## Development Commands
### Testing

View File

@@ -18,6 +18,7 @@ clean-tests:
rm -rf .sesskey
rm -rf tests/.sesskey
rm -rf tests/*.db
rm -rf tests/.myFastHtmlDb
# Alias to clean everything
clean: clean-build clean-tests

View File

@@ -10,6 +10,7 @@ from myfasthtml.controls.InstancesDebugger import InstancesDebugger
from myfasthtml.controls.Keyboard import Keyboard
from myfasthtml.controls.Layout import Layout
from myfasthtml.controls.TabsManager import TabsManager
from myfasthtml.controls.TreeView import TreeView, TreeNode
from myfasthtml.controls.helpers import Ids, mk
from myfasthtml.core.instances import UniqueInstance
from myfasthtml.icons.carbon import volume_object_storage
@@ -31,6 +32,52 @@ app, rt = create_app(protect_routes=True,
base_url="http://localhost:5003")
def create_sample_treeview(parent):
"""
Create a sample TreeView with a file structure for testing.
Args:
parent: Parent instance for the TreeView
Returns:
TreeView: Configured TreeView instance with sample data
"""
tree_view = TreeView(parent, _id="-treeview")
# Create sample file structure
projects = TreeNode(label="Projects", type="folder")
tree_view.add_node(projects)
myfasthtml = TreeNode(label="MyFastHtml", type="folder")
tree_view.add_node(myfasthtml, parent_id=projects.id)
app_py = TreeNode(label="app.py", type="file")
tree_view.add_node(app_py, parent_id=myfasthtml.id)
readme = TreeNode(label="README.md", type="file")
tree_view.add_node(readme, parent_id=myfasthtml.id)
src_folder = TreeNode(label="src", type="folder")
tree_view.add_node(src_folder, parent_id=myfasthtml.id)
controls_py = TreeNode(label="controls.py", type="file")
tree_view.add_node(controls_py, parent_id=src_folder.id)
documents = TreeNode(label="Documents", type="folder")
tree_view.add_node(documents, parent_id=projects.id)
notes = TreeNode(label="notes.txt", type="file")
tree_view.add_node(notes, parent_id=documents.id)
todo = TreeNode(label="todo.md", type="file")
tree_view.add_node(todo, parent_id=documents.id)
# Expand all nodes to show the full structure
tree_view.expand_all()
return tree_view
@rt("/")
def index(session):
session_instance = UniqueInstance(session=session, _id=Ids.UserSession)
@@ -62,13 +109,17 @@ def index(session):
btn_popup = mk.label("Popup",
command=add_tab("Popup", Dropdown(layout, "Content", button="button", _id="-dropdown")))
# Create TreeView with sample data
tree_view = create_sample_treeview(layout)
layout.header_left.add(tabs_manager.add_tab_btn())
layout.header_right.add(btn_show_right_drawer)
layout.left_drawer.add(btn_show_instances_debugger, "Debugger")
layout.left_drawer.add(btn_show_commands_debugger, "Debugger")
layout.left_drawer.add(btn_file_upload, "Test")
layout.left_drawer.add(btn_popup, "Test")
layout.left_drawer.add(tree_view, "TreeView")
layout.set_main(tabs_manager)
keyboard = Keyboard(layout, _id="-keyboard").add("ctrl+o",
add_tab("File Open", FileUpload(layout, _id="-file_upload")))

View File

@@ -28,7 +28,6 @@ class Dropdown(MultipleInstance):
self.content = content
self.commands = Commands(self)
self._state = DropdownState()
self._toggle_command = self.commands.toggle()
def toggle(self):
self._state.opened = not self._state.opened

View File

@@ -0,0 +1,395 @@
"""
TreeView component for hierarchical data visualization with inline editing.
This component provides an interactive tree structure with expand/collapse,
selection, and inline editing capabilities.
"""
import uuid
from dataclasses import dataclass, field
from typing import Optional
from fasthtml.components import Div, Input, Span, Button
from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.core.commands import Command
from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance
@dataclass
class TreeNode:
"""
Represents a node in the tree structure.
Attributes:
id: Unique identifier (auto-generated UUID if not provided)
label: Display text for the node
type: Node type for icon mapping
parent: ID of parent node (None for root)
children: List of child node IDs
"""
id: str = field(default_factory=lambda: str(uuid.uuid4()))
label: str = ""
type: str = "default"
parent: Optional[str] = None
children: list[str] = field(default_factory=list)
class TreeViewState(DbObject):
"""
Persistent state for TreeView component.
Attributes:
items: Dictionary mapping node IDs to TreeNode instances
opened: List of expanded node IDs
selected: Currently selected node ID
editing: Node ID currently being edited (None if not editing)
icon_config: Mapping of node types to icon identifiers
"""
def __init__(self, owner):
super().__init__(owner)
with self.initializing():
self.items: dict[str, TreeNode] = {}
self.opened: list[str] = []
self.selected: Optional[str] = None
self.editing: Optional[str] = None
self.icon_config: dict[str, str] = {}
class Commands(BaseCommands):
"""Command handlers for TreeView actions."""
def toggle_node(self, node_id: str):
"""Create command to expand/collapse a node."""
return Command(
"ToggleNode",
f"Toggle node {node_id}",
self._owner._toggle_node,
node_id
)
def add_child(self, parent_id: str):
"""Create command to add a child node."""
return Command(
"AddChild",
f"Add child to {parent_id}",
self._owner._add_child,
parent_id
)
def add_sibling(self, node_id: str):
"""Create command to add a sibling node."""
return Command(
"AddSibling",
f"Add sibling to {node_id}",
self._owner._add_sibling,
node_id
)
def start_rename(self, node_id: str):
"""Create command to start renaming a node."""
return Command(
"StartRename",
f"Start renaming {node_id}",
self._owner._start_rename,
node_id
)
def save_rename(self, node_id: str):
"""Create command to save renamed node."""
return Command(
"SaveRename",
f"Save rename for {node_id}",
self._owner._save_rename,
node_id
)
def cancel_rename(self):
"""Create command to cancel renaming."""
return Command(
"CancelRename",
"Cancel rename",
self._owner._cancel_rename
)
def delete_node(self, node_id: str):
"""Create command to delete a node."""
return Command(
"DeleteNode",
f"Delete node {node_id}",
self._owner._delete_node,
node_id
)
def select_node(self, node_id: str):
"""Create command to select a node."""
return Command(
"SelectNode",
f"Select node {node_id}",
self._owner._select_node,
node_id
)
class TreeView(MultipleInstance):
"""
Interactive TreeView component with hierarchical data visualization.
Supports:
- Expand/collapse nodes
- Add child/sibling nodes
- Inline rename
- Delete nodes
- Node selection
"""
def __init__(self, parent, items: Optional[dict] = None, _id: Optional[str] = None):
"""
Initialize TreeView component.
Args:
parent: Parent instance
items: Optional initial items dictionary {node_id: TreeNode}
_id: Optional custom ID
"""
super().__init__(parent, _id=_id)
self._state = TreeViewState(self)
self.commands = Commands(self)
if items:
self._state.items = items
def set_icon_config(self, config: dict[str, str]):
"""
Set icon configuration for node types.
Args:
config: Dictionary mapping node types to icon identifiers
Format: {type: "provider.icon_name"}
"""
self._state.icon_config = config
def add_node(self, node: TreeNode, parent_id: Optional[str] = None):
"""
Add a node to the tree.
Args:
node: TreeNode instance to add
parent_id: Optional parent node ID (None for root)
"""
self._state.items[node.id] = node
node.parent = parent_id
if parent_id and parent_id in self._state.items:
parent = self._state.items[parent_id]
if node.id not in parent.children:
parent.children.append(node.id)
def expand_all(self):
"""Expand all nodes that have children."""
for node_id, node in self._state.items.items():
if node.children and node_id not in self._state.opened:
self._state.opened.append(node_id)
def _toggle_node(self, node_id: str):
"""Toggle expand/collapse state of a node."""
if node_id in self._state.opened:
self._state.opened.remove(node_id)
else:
self._state.opened.append(node_id)
return self.render()
def _add_child(self, parent_id: str, new_label: Optional[str] = None):
"""Add a child node to a parent."""
if parent_id not in self._state.items:
raise ValueError(f"Parent node {parent_id} does not exist")
parent = self._state.items[parent_id]
new_node = TreeNode(
label=new_label or "New Node",
type=parent.type,
parent=parent_id
)
self._state.items[new_node.id] = new_node
parent.children.append(new_node.id)
# Auto-expand parent
if parent_id not in self._state.opened:
self._state.opened.append(parent_id)
return self.render()
def _add_sibling(self, node_id: str, new_label: Optional[str] = None):
"""Add a sibling node next to a node."""
if node_id not in self._state.items:
raise ValueError(f"Node {node_id} does not exist")
node = self._state.items[node_id]
if node.parent is None:
raise ValueError("Cannot add sibling to root node")
parent = self._state.items[node.parent]
new_node = TreeNode(
label=new_label or "New Node",
type=node.type,
parent=node.parent
)
self._state.items[new_node.id] = new_node
# Insert after current node
idx = parent.children.index(node_id)
parent.children.insert(idx + 1, new_node.id)
return self.render()
def _start_rename(self, node_id: str):
"""Start renaming a node (sets editing state)."""
if node_id not in self._state.items:
raise ValueError(f"Node {node_id} does not exist")
self._state.editing = node_id
return self.render()
def _save_rename(self, node_id: str, new_label: str):
"""Save renamed node with new label."""
if node_id not in self._state.items:
raise ValueError(f"Node {node_id} does not exist")
self._state.items[node_id].label = new_label
self._state.editing = None
return self.render()
def _cancel_rename(self):
"""Cancel renaming operation."""
self._state.editing = None
return self.render()
def _delete_node(self, node_id: str):
"""Delete a node (only if it has no children)."""
if node_id not in self._state.items:
raise ValueError(f"Node {node_id} does not exist")
node = self._state.items[node_id]
if node.children:
raise ValueError(f"Cannot delete node {node_id} with children")
# Remove from parent's children list
if node.parent and node.parent in self._state.items:
parent = self._state.items[node.parent]
parent.children.remove(node_id)
# Remove from state
del self._state.items[node_id]
if node_id in self._state.opened:
self._state.opened.remove(node_id)
if self._state.selected == node_id:
self._state.selected = None
return self.render()
def _select_node(self, node_id: str):
"""Select a node."""
if node_id not in self._state.items:
raise ValueError(f"Node {node_id} does not exist")
self._state.selected = node_id
return self.render()
def _render_action_buttons(self, node_id: str):
"""Render action buttons for a node (visible on hover)."""
return Div(
Button("+child", data_action="add_child"),
Button("+sibling", data_action="add_sibling"),
Button("rename", data_action="rename"),
Button("delete", data_action="delete"),
cls="mf-treenode-actions"
)
def _render_node(self, node_id: str, level: int = 0):
"""
Render a single node and its children recursively.
Args:
node_id: ID of node to render
level: Indentation level
Returns:
Div containing the node and its children
"""
node = self._state.items[node_id]
is_expanded = node_id in self._state.opened
is_selected = node_id == self._state.selected
is_editing = node_id == self._state.editing
has_children = len(node.children) > 0
# Toggle icon
toggle = Span(
"" if is_expanded else "" if has_children else " ",
cls="mf-treenode-toggle"
)
# Label or input for editing
if is_editing:
label_element = Input(
value=node.label,
cls="mf-treenode-input"
)
else:
label_element = Span(
node.label,
cls="mf-treenode-label"
)
# Node element
node_element = Div(
toggle,
label_element,
self._render_action_buttons(node_id),
cls=f"mf-treenode {'selected' if is_selected else ''}",
data_node_id=node_id,
data_level=str(level)
)
# Children (if expanded)
children_elements = []
if is_expanded and has_children:
for child_id in node.children:
children_elements.append(
self._render_node(child_id, level + 1)
)
return Div(
node_element,
*children_elements,
cls="mf-treenode-container"
)
def render(self):
"""
Render the complete TreeView.
Returns:
Div: Complete TreeView HTML structure
"""
# Find root nodes (nodes without parent)
root_nodes = [
node_id for node_id, node in self._state.items.items()
if node.parent is None
]
return Div(
*[self._render_node(node_id) for node_id in root_nodes],
id=self._id,
cls="mf-treeview"
)
def __ft__(self):
"""FastHTML magic method for rendering."""
return self.render()

View File

@@ -10,6 +10,7 @@ from myfasthtml.core.utils import retrieve_user_info
class DbManager(SingleInstance):
def __init__(self, parent, root=".myFastHtmlDb", auto_register: bool = True):
super().__init__(parent, auto_register=auto_register)
self.db = DbEngine(root=root)
def save(self, entry, obj):

View File

@@ -3,6 +3,11 @@ import pytest
from myfasthtml.core.instances import SingleInstance
class RootInstanceForTests(SingleInstance):
def __init__(self, session):
super().__init__(None, session, _id="TestRoot")
@pytest.fixture(scope="session")
def session():
return {
@@ -20,4 +25,4 @@ def session():
@pytest.fixture(scope="session")
def root_instance(session):
return SingleInstance(None, session, "TestRoot")
return RootInstanceForTests(session=session)

View File

@@ -0,0 +1,256 @@
"""Unit tests for TreeView component."""
import shutil
import pytest
from myfasthtml.controls.TreeView import TreeView, TreeNode
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")
class TestTreeViewRender:
"""Tests for TreeView HTML rendering."""
def test_treeview_renders_correctly(self):
"""Test that TreeView generates correct HTML structure."""
# Signature only - implementation later
pass
def test_node_action_buttons_are_rendered(self):
"""Test that action buttons are present in rendered HTML."""
# Signature only - implementation later
pass