Testing Layout
This commit is contained in:
@@ -199,7 +199,91 @@ class TestControlRender:
|
|||||||
|
|
||||||
**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.
|
**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
|
### UTR-11: Required Reading for Control Render Tests
|
||||||
|
|
||||||
|
**Before writing ANY render tests for Controls, you MUST:**
|
||||||
|
|
||||||
|
1. **Read the matcher documentation**: `docs/testing_rendered_components.md`
|
||||||
|
2. **Understand the key concepts**:
|
||||||
|
- How `matches()` and `find()` work
|
||||||
|
- When to use predicates (Contains, StartsWith, AnyValue, etc.)
|
||||||
|
- How to test only what matters (not every detail)
|
||||||
|
- How to read error messages with `^^^` markers
|
||||||
|
3. **Apply the best practices**:
|
||||||
|
- Test only important structural elements and attributes
|
||||||
|
- Use predicates for dynamic/generated values
|
||||||
|
- Don't over-specify tests with irrelevant details
|
||||||
|
- Structure tests in layers (overall structure, then details)
|
||||||
|
|
||||||
|
**Mandatory render test rules:**
|
||||||
|
|
||||||
|
1. **Test naming**: Use descriptive names like `test_empty_layout_is_rendered()` not `test_layout_renders_with_all_sections()`
|
||||||
|
2. **Documentation format**: Every render test MUST have a docstring with:
|
||||||
|
- First line: Brief description of what is being tested
|
||||||
|
- Blank line
|
||||||
|
- "Why these elements matter:" or "Why this test matters:" section
|
||||||
|
- List of important elements/attributes being tested with explanations (in English)
|
||||||
|
3. **No inline comments**: Do NOT add comments on each line of the expected structure
|
||||||
|
4. **Icon testing**: Use `Div(NotStr(name="icon_name"))` to test SVG icons
|
||||||
|
- Use the exact icon name from the import (e.g., `name="panel_right_expand20_regular"`)
|
||||||
|
- Use `name=""` (empty string) if the import is not explicit
|
||||||
|
- NEVER use `name="svg"` - it will cause test failures
|
||||||
|
5. **Component testing**: Use `TestObject(ComponentClass)` to test presence of components
|
||||||
|
6. **Explanation focus**: In "Why these elements matter", refer to the logical element (e.g., "Svg") not the technical implementation (e.g., "Div(NotStr(...))")
|
||||||
|
|
||||||
|
**Example of proper render test:**
|
||||||
|
```python
|
||||||
|
from myfasthtml.test.matcher import matches, find, Contains, NotStr, TestObject
|
||||||
|
from fasthtml.common import Div, Header, Button
|
||||||
|
|
||||||
|
class TestControlRender:
|
||||||
|
def test_empty_control_is_rendered(self, root_instance):
|
||||||
|
"""Test that control renders with main structural sections.
|
||||||
|
|
||||||
|
Why these elements matter:
|
||||||
|
- 3 children: Verifies header, body, and footer are all rendered
|
||||||
|
- _id: Essential for HTMX targeting and component identification
|
||||||
|
- cls="control-wrapper": Root CSS class for styling
|
||||||
|
"""
|
||||||
|
control = MyControl(root_instance)
|
||||||
|
|
||||||
|
expected = Div(
|
||||||
|
Header(),
|
||||||
|
Div(),
|
||||||
|
Div(),
|
||||||
|
_id=control._id,
|
||||||
|
cls="control-wrapper"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matches(control.render(), expected)
|
||||||
|
|
||||||
|
def test_header_with_icon_is_rendered(self, root_instance):
|
||||||
|
"""Test that header renders with action icon.
|
||||||
|
|
||||||
|
Why these elements matter:
|
||||||
|
- Svg: Action icon is essential for user interaction
|
||||||
|
- TestObject(ActionButton): ActionButton component must be present
|
||||||
|
- cls="header-bar": Required CSS class for header styling
|
||||||
|
"""
|
||||||
|
control = MyControl(root_instance)
|
||||||
|
header = control._mk_header()
|
||||||
|
|
||||||
|
expected = Header(
|
||||||
|
Div(NotStr(name="action_icon_20_regular")),
|
||||||
|
TestObject(ActionButton),
|
||||||
|
cls="header-bar"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matches(header, expected)
|
||||||
|
```
|
||||||
|
|
||||||
|
**When proposing render tests:**
|
||||||
|
- Reference specific patterns from the documentation
|
||||||
|
- Explain why you chose to test certain elements and not others
|
||||||
|
- Justify the use of predicates vs exact values
|
||||||
|
- Always include "Why these elements matter" documentation
|
||||||
|
|
||||||
|
### UTR-12: Test Workflow
|
||||||
|
|
||||||
1. **Receive code to test** - User provides file path or code section
|
1. **Receive code to test** - User provides file path or code section
|
||||||
2. **Check existing tests** - Look for corresponding test file and read it if it exists
|
2. **Check existing tests** - Look for corresponding test file and read it if it exists
|
||||||
|
|||||||
482
tests/controls/test_layout.py
Normal file
482
tests/controls/test_layout.py
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
"""Unit tests for Layout component."""
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fasthtml.components import *
|
||||||
|
|
||||||
|
from myfasthtml.controls.Layout import Layout, LayoutState
|
||||||
|
from myfasthtml.controls.UserProfile import UserProfile
|
||||||
|
from myfasthtml.test.matcher import matches, find, Contains, NotStr, TestObject
|
||||||
|
from .conftest import root_instance
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def cleanup_db():
|
||||||
|
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLayoutBehaviour:
|
||||||
|
"""Tests for Layout behavior and logic."""
|
||||||
|
|
||||||
|
def test_i_can_create_layout(self, root_instance):
|
||||||
|
"""Test basic layout creation with app_name."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
|
||||||
|
assert layout is not None
|
||||||
|
assert layout.app_name == "Test App"
|
||||||
|
assert layout._state is not None
|
||||||
|
|
||||||
|
def test_i_can_set_main_content(self, root_instance):
|
||||||
|
"""Test setting main content area."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
content = Div("Main content")
|
||||||
|
|
||||||
|
result = layout.set_main(content)
|
||||||
|
|
||||||
|
assert layout._main_content == content
|
||||||
|
assert result == layout # Should return self for chaining
|
||||||
|
|
||||||
|
def test_i_can_set_footer_content(self, root_instance):
|
||||||
|
"""Test setting footer content."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
content = Div("Footer content")
|
||||||
|
|
||||||
|
layout.set_footer(content)
|
||||||
|
|
||||||
|
assert layout._footer_content == content
|
||||||
|
|
||||||
|
def test_i_can_add_content_to_left_drawer(self, root_instance):
|
||||||
|
"""Test adding content to left drawer."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
content = Div("Left drawer content", id="drawer_item")
|
||||||
|
|
||||||
|
layout.left_drawer.add(content)
|
||||||
|
|
||||||
|
drawer_content = layout.left_drawer.get_content()
|
||||||
|
assert None in drawer_content
|
||||||
|
assert content in drawer_content[None]
|
||||||
|
|
||||||
|
def test_i_can_add_content_to_right_drawer(self, root_instance):
|
||||||
|
"""Test adding content to right drawer."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
content = Div("Right drawer content", id="drawer_item")
|
||||||
|
|
||||||
|
layout.right_drawer.add(content)
|
||||||
|
|
||||||
|
drawer_content = layout.right_drawer.get_content()
|
||||||
|
assert None in drawer_content
|
||||||
|
assert content in drawer_content[None]
|
||||||
|
|
||||||
|
def test_i_can_add_content_to_header_left(self, root_instance):
|
||||||
|
"""Test adding content to left side of header."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
content = Div("Header left content", id="header_item")
|
||||||
|
|
||||||
|
layout.header_left.add(content)
|
||||||
|
|
||||||
|
header_content = layout.header_left.get_content()
|
||||||
|
assert None in header_content
|
||||||
|
assert content in header_content[None]
|
||||||
|
|
||||||
|
def test_i_can_add_content_to_header_right(self, root_instance):
|
||||||
|
"""Test adding content to right side of header."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
content = Div("Header right content", id="header_item")
|
||||||
|
|
||||||
|
layout.header_right.add(content)
|
||||||
|
|
||||||
|
header_content = layout.header_right.get_content()
|
||||||
|
assert None in header_content
|
||||||
|
assert content in header_content[None]
|
||||||
|
|
||||||
|
def test_i_can_add_grouped_content(self, root_instance):
|
||||||
|
"""Test adding content with custom groups."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
group_name = "navigation"
|
||||||
|
content1 = Div("Nav item 1", id="nav1")
|
||||||
|
content2 = Div("Nav item 2", id="nav2")
|
||||||
|
|
||||||
|
layout.left_drawer.add(content1, group=group_name)
|
||||||
|
layout.left_drawer.add(content2, group=group_name)
|
||||||
|
|
||||||
|
drawer_content = layout.left_drawer.get_content()
|
||||||
|
assert group_name in drawer_content
|
||||||
|
assert content1 in drawer_content[group_name]
|
||||||
|
assert content2 in drawer_content[group_name]
|
||||||
|
|
||||||
|
def test_i_cannot_add_duplicate_content(self, root_instance):
|
||||||
|
"""Test that content with same ID is not added twice."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
content = Div("Content", id="unique_id")
|
||||||
|
|
||||||
|
layout.left_drawer.add(content)
|
||||||
|
layout.left_drawer.add(content) # Try to add again
|
||||||
|
|
||||||
|
drawer_content = layout.left_drawer.get_content()
|
||||||
|
# Content should appear only once
|
||||||
|
assert drawer_content[None].count(content) == 1
|
||||||
|
|
||||||
|
def test_i_can_toggle_left_drawer(self, root_instance):
|
||||||
|
"""Test toggling left drawer open/closed."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
|
||||||
|
# Initially open
|
||||||
|
assert layout._state.left_drawer_open is True
|
||||||
|
|
||||||
|
# Toggle to close
|
||||||
|
layout.toggle_drawer("left")
|
||||||
|
assert layout._state.left_drawer_open is False
|
||||||
|
|
||||||
|
# Toggle to open
|
||||||
|
layout.toggle_drawer("left")
|
||||||
|
assert layout._state.left_drawer_open is True
|
||||||
|
|
||||||
|
def test_i_can_toggle_right_drawer(self, root_instance):
|
||||||
|
"""Test toggling right drawer open/closed."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
|
||||||
|
# Initially open
|
||||||
|
assert layout._state.right_drawer_open is True
|
||||||
|
|
||||||
|
# Toggle to close
|
||||||
|
layout.toggle_drawer("right")
|
||||||
|
assert layout._state.right_drawer_open is False
|
||||||
|
|
||||||
|
# Toggle to open
|
||||||
|
layout.toggle_drawer("right")
|
||||||
|
assert layout._state.right_drawer_open is True
|
||||||
|
|
||||||
|
def test_i_can_update_left_drawer_width(self, root_instance):
|
||||||
|
"""Test updating left drawer width."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
new_width = 300
|
||||||
|
|
||||||
|
layout.update_drawer_width("left", new_width)
|
||||||
|
|
||||||
|
assert layout._state.left_drawer_width == new_width
|
||||||
|
|
||||||
|
def test_i_can_update_right_drawer_width(self, root_instance):
|
||||||
|
"""Test updating right drawer width."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
new_width = 400
|
||||||
|
|
||||||
|
layout.update_drawer_width("right", new_width)
|
||||||
|
|
||||||
|
assert layout._state.right_drawer_width == new_width
|
||||||
|
|
||||||
|
def test_i_cannot_set_drawer_width_below_minimum(self, root_instance):
|
||||||
|
"""Test that drawer width is constrained to minimum 150px."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
|
||||||
|
layout.update_drawer_width("left", 100) # Try to set below minimum
|
||||||
|
|
||||||
|
assert layout._state.left_drawer_width == 150 # Should be clamped to min
|
||||||
|
|
||||||
|
def test_i_cannot_set_drawer_width_above_maximum(self, root_instance):
|
||||||
|
"""Test that drawer width is constrained to maximum 600px."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
|
||||||
|
layout.update_drawer_width("right", 800) # Try to set above maximum
|
||||||
|
|
||||||
|
assert layout._state.right_drawer_width == 600 # Should be clamped to max
|
||||||
|
|
||||||
|
def test_i_cannot_toggle_invalid_drawer_side(self, root_instance):
|
||||||
|
"""Test that toggling invalid drawer side raises ValueError."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Invalid drawer side"):
|
||||||
|
layout.toggle_drawer("invalid")
|
||||||
|
|
||||||
|
def test_i_cannot_update_invalid_drawer_width(self, root_instance):
|
||||||
|
"""Test that updating invalid drawer side raises ValueError."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Invalid drawer side"):
|
||||||
|
layout.update_drawer_width("invalid", 250)
|
||||||
|
|
||||||
|
def test_layout_state_has_correct_defaults(self, root_instance):
|
||||||
|
"""Test that LayoutState initializes with correct default values."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
state = layout._state
|
||||||
|
|
||||||
|
assert state.left_drawer_open is True
|
||||||
|
assert state.right_drawer_open is True
|
||||||
|
assert state.left_drawer_width == 250
|
||||||
|
assert state.right_drawer_width == 250
|
||||||
|
|
||||||
|
def test_layout_is_single_instance(self, root_instance):
|
||||||
|
"""Test that Layout behaves as SingleInstance (same ID returns same instance)."""
|
||||||
|
layout1 = Layout(root_instance, app_name="Test App", _id="my_layout")
|
||||||
|
layout2 = Layout(root_instance, app_name="Test App", _id="my_layout")
|
||||||
|
|
||||||
|
# Should be the same instance
|
||||||
|
assert layout1 is layout2
|
||||||
|
|
||||||
|
def test_commands_are_created(self, root_instance):
|
||||||
|
"""Test that Layout creates necessary commands."""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
|
||||||
|
# Test toggle commands
|
||||||
|
left_toggle_cmd = layout.commands.toggle_drawer("left")
|
||||||
|
assert left_toggle_cmd is not None
|
||||||
|
assert left_toggle_cmd.id is not None
|
||||||
|
|
||||||
|
right_toggle_cmd = layout.commands.toggle_drawer("right")
|
||||||
|
assert right_toggle_cmd is not None
|
||||||
|
assert right_toggle_cmd.id is not None
|
||||||
|
|
||||||
|
# Test width update commands
|
||||||
|
left_width_cmd = layout.commands.update_drawer_width("left")
|
||||||
|
assert left_width_cmd is not None
|
||||||
|
assert left_width_cmd.id is not None
|
||||||
|
|
||||||
|
right_width_cmd = layout.commands.update_drawer_width("right")
|
||||||
|
assert right_width_cmd is not None
|
||||||
|
assert right_width_cmd.id is not None
|
||||||
|
|
||||||
|
|
||||||
|
class TestLayoutRender:
|
||||||
|
"""Tests for Layout HTML rendering."""
|
||||||
|
|
||||||
|
def test_empty_layout_is_rendered(self, root_instance):
|
||||||
|
"""Test that Layout renders with all main structural sections.
|
||||||
|
|
||||||
|
Why these elements matter:
|
||||||
|
- 6 children: Verifies all main sections are rendered (header, drawers, main, footer, script)
|
||||||
|
- _id: Essential for layout identification and resizer initialization
|
||||||
|
- cls="mf-layout": Root CSS class for layout styling
|
||||||
|
"""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
|
||||||
|
expected = Div(
|
||||||
|
Header(),
|
||||||
|
Div(),
|
||||||
|
Main(),
|
||||||
|
Div(),
|
||||||
|
Footer(),
|
||||||
|
Script(),
|
||||||
|
_id=layout._id,
|
||||||
|
cls="mf-layout"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matches(layout.render(), expected)
|
||||||
|
|
||||||
|
def test_header_with_drawer_icons_is_rendered(self, root_instance):
|
||||||
|
"""Test that header renders with drawer toggle icons.
|
||||||
|
|
||||||
|
Why these elements matter:
|
||||||
|
- 2 Div children: Left/right header structure for organizing controls
|
||||||
|
- Svg: Toggle icon is essential for user interaction with drawer
|
||||||
|
- TestObject(UserProfile): UserProfile component must be present in header
|
||||||
|
- cls="flex gap-1": CSS critical for horizontal alignment of header items
|
||||||
|
- cls="mf-layout-header": Root header class for styling
|
||||||
|
"""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
header = layout._mk_header()
|
||||||
|
|
||||||
|
expected = Header(
|
||||||
|
Div(
|
||||||
|
Div(NotStr(name="panel_right_expand20_regular")),
|
||||||
|
cls="flex gap-1"
|
||||||
|
),
|
||||||
|
Div(
|
||||||
|
TestObject(UserProfile),
|
||||||
|
cls="flex gap-1"
|
||||||
|
),
|
||||||
|
cls="mf-layout-header"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matches(header, expected)
|
||||||
|
|
||||||
|
def test_footer_is_rendered(self, root_instance):
|
||||||
|
"""Test that footer renders with correct structure.
|
||||||
|
|
||||||
|
Why these elements matter:
|
||||||
|
- cls Contains "mf-layout-footer": Root footer class for styling
|
||||||
|
- cls Contains "footer": DaisyUI base footer class
|
||||||
|
"""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
footer = layout._mk_footer()
|
||||||
|
|
||||||
|
expected = Footer(
|
||||||
|
cls=Contains("mf-layout-footer")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matches(footer, expected)
|
||||||
|
|
||||||
|
def test_main_content_is_rendered(self, root_instance):
|
||||||
|
"""Test that main content area renders correctly.
|
||||||
|
|
||||||
|
Why these elements matter:
|
||||||
|
- cls="mf-layout-main": Root main class for styling
|
||||||
|
"""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
main = layout._mk_main()
|
||||||
|
|
||||||
|
expected = Main(
|
||||||
|
cls="mf-layout-main"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matches(main, expected)
|
||||||
|
|
||||||
|
def test_left_drawer_is_rendered_when_open(self, root_instance):
|
||||||
|
"""Test that left drawer renders with correct classes when open.
|
||||||
|
|
||||||
|
Why these elements matter:
|
||||||
|
- _id: Required for targeting drawer in HTMX updates
|
||||||
|
- cls Contains "mf-layout-drawer": Base drawer class for styling
|
||||||
|
- cls Contains "mf-layout-left-drawer": Left-specific drawer positioning
|
||||||
|
- style Contains width: Drawer width must be applied for sizing
|
||||||
|
"""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
drawer = layout._mk_left_drawer()
|
||||||
|
|
||||||
|
expected = Div(
|
||||||
|
_id=f"{layout._id}_ld",
|
||||||
|
cls=Contains("mf-layout-drawer"),
|
||||||
|
#cls=Contains("mf-layout-left-drawer"),
|
||||||
|
style=Contains("width: 250px")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matches(drawer, expected)
|
||||||
|
|
||||||
|
def test_left_drawer_has_collapsed_class_when_closed(self, root_instance):
|
||||||
|
"""Test that left drawer renders with collapsed class when closed.
|
||||||
|
|
||||||
|
Why these elements matter:
|
||||||
|
- _id: Required for targeting drawer in HTMX updates
|
||||||
|
- cls Contains "collapsed": Class triggers CSS hiding animation
|
||||||
|
- style Contains "width: 0px": Zero width is crucial for collapse animation
|
||||||
|
"""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
layout._state.left_drawer_open = False
|
||||||
|
drawer = layout._mk_left_drawer()
|
||||||
|
|
||||||
|
expected = Div(
|
||||||
|
_id=f"{layout._id}_ld",
|
||||||
|
cls=Contains("collapsed"),
|
||||||
|
style=Contains("width: 0px")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matches(drawer, expected)
|
||||||
|
|
||||||
|
def test_right_drawer_is_rendered_when_open(self, root_instance):
|
||||||
|
"""Test that right drawer renders with correct classes when open.
|
||||||
|
|
||||||
|
Why these elements matter:
|
||||||
|
- _id: Required for targeting drawer in HTMX updates
|
||||||
|
- cls Contains "mf-layout-drawer": Base drawer class for styling
|
||||||
|
- cls Contains "mf-layout-right-drawer": Right-specific drawer positioning
|
||||||
|
- style Contains width: Drawer width must be applied for sizing
|
||||||
|
"""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
drawer = layout._mk_right_drawer()
|
||||||
|
|
||||||
|
expected = Div(
|
||||||
|
_id=f"{layout._id}_rd",
|
||||||
|
cls=Contains("mf-layout-drawer"),
|
||||||
|
#cls=Contains("mf-layout-right-drawer"),
|
||||||
|
style=Contains("width: 250px")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matches(drawer, expected)
|
||||||
|
|
||||||
|
def test_right_drawer_has_collapsed_class_when_closed(self, root_instance):
|
||||||
|
"""Test that right drawer renders with collapsed class when closed.
|
||||||
|
|
||||||
|
Why these elements matter:
|
||||||
|
- _id: Required for targeting drawer in HTMX updates
|
||||||
|
- cls Contains "collapsed": Class triggers CSS hiding animation
|
||||||
|
- style Contains "width: 0px": Zero width is crucial for collapse animation
|
||||||
|
"""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
layout._state.right_drawer_open = False
|
||||||
|
drawer = layout._mk_right_drawer()
|
||||||
|
|
||||||
|
expected = Div(
|
||||||
|
_id=f"{layout._id}_rd",
|
||||||
|
cls=Contains("collapsed"),
|
||||||
|
style=Contains("width: 0px")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matches(drawer, expected)
|
||||||
|
|
||||||
|
def test_drawer_width_is_applied_as_style(self, root_instance):
|
||||||
|
"""Test that custom drawer width is applied as inline style.
|
||||||
|
|
||||||
|
Why this test matters:
|
||||||
|
- style Contains "width: 300px": Verifies that width updates are reflected in style attribute
|
||||||
|
"""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
layout._state.left_drawer_width = 300
|
||||||
|
drawer = layout._mk_left_drawer()
|
||||||
|
|
||||||
|
expected = Div(
|
||||||
|
style=Contains("width: 300px")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matches(drawer, expected)
|
||||||
|
|
||||||
|
def test_left_drawer_has_resizer_element(self, root_instance):
|
||||||
|
"""Test that left drawer contains resizer element.
|
||||||
|
|
||||||
|
Why this test matters:
|
||||||
|
- Resizer element must be present for drawer width adjustment
|
||||||
|
- cls "mf-resizer-left": Left-specific resizer for correct edge positioning
|
||||||
|
"""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
drawer = layout._mk_left_drawer()
|
||||||
|
|
||||||
|
resizers = find(drawer, Div(cls=Contains("mf-resizer-left")))
|
||||||
|
assert len(resizers) == 1, "Left drawer should contain exactly one resizer element"
|
||||||
|
|
||||||
|
def test_right_drawer_has_resizer_element(self, root_instance):
|
||||||
|
"""Test that right drawer contains resizer element.
|
||||||
|
|
||||||
|
Why this test matters:
|
||||||
|
- Resizer element must be present for drawer width adjustment
|
||||||
|
- cls "mf-resizer-right": Right-specific resizer for correct edge positioning
|
||||||
|
"""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
drawer = layout._mk_right_drawer()
|
||||||
|
|
||||||
|
resizers = find(drawer, Div(cls=Contains("mf-resizer-right")))
|
||||||
|
assert len(resizers) == 1, "Right drawer should contain exactly one resizer element"
|
||||||
|
|
||||||
|
def test_drawer_groups_are_separated_by_dividers(self, root_instance):
|
||||||
|
"""Test that multiple groups in drawer are separated by divider elements.
|
||||||
|
|
||||||
|
Why this test matters:
|
||||||
|
- Dividers provide visual separation between content groups
|
||||||
|
- At least one divider should exist when multiple groups are present
|
||||||
|
"""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
|
||||||
|
layout.left_drawer.add(Div("Item 1"), group="group1")
|
||||||
|
layout.left_drawer.add(Div("Item 2"), group="group2")
|
||||||
|
|
||||||
|
drawer = layout._mk_left_drawer()
|
||||||
|
|
||||||
|
content_wrappers = find(drawer, Div(cls="mf-layout-drawer-content"))
|
||||||
|
assert len(content_wrappers) == 1
|
||||||
|
|
||||||
|
content = content_wrappers[0]
|
||||||
|
|
||||||
|
dividers = find(content, Div(cls="divider"))
|
||||||
|
assert len(dividers) >= 1, "Groups should be separated by dividers"
|
||||||
|
|
||||||
|
def test_resizer_script_is_included(self, root_instance):
|
||||||
|
"""Test that resizer initialization script is included in render.
|
||||||
|
|
||||||
|
Why this test matters:
|
||||||
|
- Script element: Required to initialize resizer functionality
|
||||||
|
- Script contains initResizer call: Ensures resizer is activated for this layout instance
|
||||||
|
"""
|
||||||
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
rendered = layout.render()
|
||||||
|
|
||||||
|
scripts = find(rendered, Script())
|
||||||
|
assert len(scripts) == 1, "Layout should contain exactly one script element"
|
||||||
|
|
||||||
|
script_content = str(scripts[0].children[0])
|
||||||
|
assert f"initResizer('{layout._id}')" in script_content, "Script must initialize resizer with layout ID"
|
||||||
@@ -381,7 +381,7 @@ class TestTreeviewBehaviour:
|
|||||||
class TestTreeViewRender:
|
class TestTreeViewRender:
|
||||||
"""Tests for TreeView HTML rendering."""
|
"""Tests for TreeView HTML rendering."""
|
||||||
|
|
||||||
def test_i_can_render_empty_treeview(self, root_instance):
|
def test_empty_treeview_is_rendered(self, root_instance):
|
||||||
"""Test that TreeView generates correct HTML structure."""
|
"""Test that TreeView generates correct HTML structure."""
|
||||||
tree_view = TreeView(root_instance)
|
tree_view = TreeView(root_instance)
|
||||||
expected = Div(
|
expected = Div(
|
||||||
|
|||||||
Reference in New Issue
Block a user