Added tests for Layout and Treeview
This commit is contained in:
@@ -5,7 +5,9 @@ import pytest
|
||||
from fasthtml.components import *
|
||||
|
||||
from myfasthtml.controls.Layout import Layout
|
||||
from myfasthtml.test.matcher import matches, find, Contains, find_one, TestIcon, TestScript
|
||||
from myfasthtml.controls.UserProfile import UserProfile
|
||||
from myfasthtml.test.matcher import matches, find, Contains, find_one, TestIcon, TestScript, TestObject, AnyValue, Skip, \
|
||||
TestIconNotStr
|
||||
from .conftest import root_instance
|
||||
|
||||
|
||||
@@ -236,11 +238,12 @@ class TestLayoutRender:
|
||||
"""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)
|
||||
- 7 children: Verifies all main sections are rendered (tooltip container, header, drawers, main, footer, script)
|
||||
- _id: Essential for layout identification and resizer initialization
|
||||
- cls="mf-layout": Root CSS class for layout styling
|
||||
"""
|
||||
expected = Div(
|
||||
Div(), # tooltip container
|
||||
Header(),
|
||||
Div(), # left drawer
|
||||
Main(),
|
||||
@@ -286,7 +289,7 @@ class TestLayoutRender:
|
||||
|
||||
expected = Header(
|
||||
Div(
|
||||
TestIcon("panel_right_expand20_regular"),
|
||||
TestIcon("PanelLeftContract20Regular"),
|
||||
cls="flex gap-1"
|
||||
),
|
||||
cls="mf-layout-header"
|
||||
@@ -343,7 +346,7 @@ class TestLayoutRender:
|
||||
|
||||
expected = Div(
|
||||
_id=f"{layout._id}_ld",
|
||||
cls=Contains("collapsed"),
|
||||
cls=Contains("mf-layout-drawer", "mf-layout-left-drawer", "collapsed"),
|
||||
style=Contains("width: 0px")
|
||||
)
|
||||
|
||||
@@ -382,7 +385,7 @@ class TestLayoutRender:
|
||||
|
||||
expected = Div(
|
||||
_id=f"{layout._id}_rd",
|
||||
cls=Contains("collapsed"),
|
||||
cls=Contains("mf-layout-drawer", "mf-layout-right-drawer", "collapsed"),
|
||||
style=Contains("width: 0px")
|
||||
)
|
||||
|
||||
@@ -425,34 +428,250 @@ class TestLayoutRender:
|
||||
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, layout):
|
||||
"""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.left_drawer.add(Div("Item 1"), group="group1")
|
||||
layout.left_drawer.add(Div("Item 2"), group="group2")
|
||||
|
||||
drawer = find(layout.render(), Div(id=f"{layout._id}_ld"))
|
||||
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, layout):
|
||||
"""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
|
||||
- Script contains initLayout call: Ensures layout is activated for this layout instance
|
||||
"""
|
||||
script = find_one(layout.render(), Script())
|
||||
expected = TestScript(f"initResizer('{layout._id}');")
|
||||
expected = TestScript(f"initLayout('{layout._id}');")
|
||||
|
||||
assert matches(script, expected)
|
||||
|
||||
def test_left_drawer_renders_content_with_groups(self, layout):
|
||||
"""Test that left drawer renders content organized by groups with proper wrappers.
|
||||
|
||||
Why these elements matter:
|
||||
- mf-layout-drawer-content wrapper: Required container for drawer scrolling behavior
|
||||
- divider elements: Visual separation between content groups
|
||||
- Group count validation: Ensures all added groups are rendered
|
||||
"""
|
||||
layout.left_drawer.add(Div("Item 1", id="item1"), group="group1")
|
||||
layout.left_drawer.add(Div("Item 2", id="item2"), group="group2")
|
||||
|
||||
drawer = find_one(layout.render(), Div(id=f"{layout._id}_ld"))
|
||||
|
||||
content_wrappers = find(drawer, Div(cls="mf-layout-drawer-content"))
|
||||
assert len(content_wrappers) == 1, "Left drawer should contain exactly one content wrapper"
|
||||
|
||||
content = content_wrappers[0]
|
||||
dividers = find(content, Div(cls="divider"))
|
||||
assert len(dividers) == 1, "Two groups should be separated by exactly one divider"
|
||||
|
||||
|
||||
def test_header_left_renders_custom_content(self, layout):
|
||||
"""Test that custom content added to header_left is rendered in the left header section.
|
||||
|
||||
Why these elements matter:
|
||||
- id="{layout._id}_hl": Essential for HTMX targeting during updates
|
||||
- cls Contains "flex": Ensures horizontal layout of header items
|
||||
- Icon presence: Toggle drawer icon must always be first element
|
||||
- Custom content: Verifies header_left.add() correctly renders content
|
||||
"""
|
||||
custom_content = Div("Custom Header", id="custom_header")
|
||||
layout.header_left.add(custom_content)
|
||||
|
||||
header_left = find_one(layout.render(), Div(id=f"{layout._id}_hl"))
|
||||
|
||||
expected = Div(
|
||||
TestIcon(""),
|
||||
Skip(None),
|
||||
Div("Custom Header", id="custom_header"),
|
||||
id=f"{layout._id}_hl",
|
||||
cls=Contains("flex", "gap-1")
|
||||
)
|
||||
|
||||
assert matches(header_left, expected)
|
||||
|
||||
def test_header_right_renders_custom_content(self, layout):
|
||||
"""Test that custom content added to header_right is rendered in the right header section.
|
||||
|
||||
Why these elements matter:
|
||||
- id="{layout._id}_hr": Essential for HTMX targeting during updates
|
||||
- cls Contains "flex": Ensures horizontal layout of header items
|
||||
- Custom content: Verifies header_right.add() correctly renders content
|
||||
- UserProfile component: Must always be last element in right header
|
||||
"""
|
||||
custom_content = Div("Custom Header Right", id="custom_header_right")
|
||||
layout.header_right.add(custom_content)
|
||||
|
||||
header_right = find_one(layout.render(), Div(id=f"{layout._id}_hr"))
|
||||
|
||||
expected = Div(
|
||||
Skip(None),
|
||||
Div("Custom Header Right", id="custom_header_right"),
|
||||
TestObject(UserProfile),
|
||||
id=f"{layout._id}_hr",
|
||||
cls=Contains("flex", "gap-1")
|
||||
)
|
||||
|
||||
assert matches(header_right, expected)
|
||||
|
||||
def test_footer_left_renders_custom_content(self, layout):
|
||||
"""Test that custom content added to footer_left is rendered in the left footer section.
|
||||
|
||||
Why these elements matter:
|
||||
- id="{layout._id}_fl": Essential for HTMX targeting during updates
|
||||
- cls Contains "flex": Ensures horizontal layout of footer items
|
||||
- Custom content: Verifies footer_left.add() correctly renders content
|
||||
"""
|
||||
custom_content = Div("Custom Footer Left", id="custom_footer_left")
|
||||
layout.footer_left.add(custom_content)
|
||||
|
||||
footer_left = find_one(layout.render(), Div(id=f"{layout._id}_fl"))
|
||||
|
||||
expected = Div(
|
||||
Skip(None),
|
||||
Div("Custom Footer Left", id="custom_footer_left"),
|
||||
id=f"{layout._id}_fl",
|
||||
cls=Contains("flex", "gap-1")
|
||||
)
|
||||
|
||||
assert matches(footer_left, expected)
|
||||
|
||||
def test_footer_right_renders_custom_content(self, layout):
|
||||
"""Test that custom content added to footer_right is rendered in the right footer section.
|
||||
|
||||
Why these elements matter:
|
||||
- id="{layout._id}_fr": Essential for HTMX targeting during updates
|
||||
- cls Contains "flex": Ensures horizontal layout of footer items
|
||||
- Custom content: Verifies footer_right.add() correctly renders content
|
||||
"""
|
||||
custom_content = Div("Custom Footer Right", id="custom_footer_right")
|
||||
layout.footer_right.add(custom_content)
|
||||
|
||||
footer_right = find_one(layout.render(), Div(id=f"{layout._id}_fr"))
|
||||
|
||||
expected = Div(
|
||||
Skip(None),
|
||||
Div("Custom Footer Right", id="custom_footer_right"),
|
||||
id=f"{layout._id}_fr",
|
||||
cls=Contains("flex", "gap-1")
|
||||
)
|
||||
|
||||
assert matches(footer_right, expected)
|
||||
|
||||
def test_left_drawer_resizer_has_command_data(self, layout):
|
||||
"""Test that left drawer resizer has correct data attributes for command binding.
|
||||
|
||||
Why these elements matter:
|
||||
- data_command_id: JavaScript uses this to trigger width update command
|
||||
- data_side="left": JavaScript needs this to identify which drawer to resize
|
||||
- cls Contains "mf-resizer-left": CSS uses this for left-specific positioning
|
||||
"""
|
||||
drawer = find_one(layout.render(), Div(id=f"{layout._id}_ld"))
|
||||
|
||||
resizer = find_one(drawer, Div(cls=Contains("mf-resizer-left")))
|
||||
|
||||
expected = Div(
|
||||
cls=Contains("mf-resizer", "mf-resizer-left"),
|
||||
data_command_id=AnyValue(),
|
||||
data_side="left"
|
||||
)
|
||||
|
||||
assert matches(resizer, expected)
|
||||
|
||||
def test_right_drawer_resizer_has_command_data(self, layout):
|
||||
"""Test that right drawer resizer has correct data attributes for command binding.
|
||||
|
||||
Why these elements matter:
|
||||
- data_command_id: JavaScript uses this to trigger width update command
|
||||
- data_side="right": JavaScript needs this to identify which drawer to resize
|
||||
- cls Contains "mf-resizer-right": CSS uses this for right-specific positioning
|
||||
"""
|
||||
drawer = find_one(layout.render(), Div(id=f"{layout._id}_rd"))
|
||||
|
||||
resizer = find_one(drawer, Div(cls=Contains("mf-resizer-right")))
|
||||
|
||||
expected = Div(
|
||||
cls=Contains("mf-resizer", "mf-resizer-right"),
|
||||
data_command_id=AnyValue(),
|
||||
data_side="right"
|
||||
)
|
||||
|
||||
assert matches(resizer, expected)
|
||||
|
||||
def test_left_drawer_icon_changes_when_closed(self, layout):
|
||||
"""Test that left drawer toggle icon changes from expand to collapse when drawer is closed.
|
||||
|
||||
Why these elements matter:
|
||||
- id="{layout._id}_ldi": Required for HTMX swap-oob updates when toggling
|
||||
- Icon type: Visual feedback to user about drawer state (expand icon when closed)
|
||||
- Icon change: Validates that toggle_drawer returns correct icon
|
||||
"""
|
||||
layout._state.left_drawer_open = False
|
||||
|
||||
icon_div = find_one(layout.render(), Div(id=f"{layout._id}_ldi"))
|
||||
|
||||
expected = Div(
|
||||
TestIconNotStr("panel_left_expand20_regular"),
|
||||
id=f"{layout._id}_ldi"
|
||||
)
|
||||
|
||||
assert matches(icon_div, expected)
|
||||
|
||||
def test_left_drawer_icon_changes_when_opne(self, layout):
|
||||
"""Test that left drawer toggle icon changes from collapse to expand when drawer is open..
|
||||
|
||||
Why these elements matter:
|
||||
- id="{layout._id}_ldi": Required for HTMX swap-oob updates when toggling
|
||||
- Icon type: Visual feedback to user about drawer state (expand icon when closed)
|
||||
- Icon change: Validates that toggle_drawer returns correct icon
|
||||
"""
|
||||
layout._state.left_drawer_open = True
|
||||
|
||||
icon_div = find_one(layout.render(), Div(id=f"{layout._id}_ldi"))
|
||||
|
||||
expected = Div(
|
||||
TestIconNotStr("panel_left_contract20_regular"),
|
||||
id=f"{layout._id}_ldi"
|
||||
)
|
||||
|
||||
assert matches(icon_div, expected)
|
||||
|
||||
def test_tooltip_container_is_rendered(self, layout):
|
||||
"""Test that tooltip container is rendered at the top of the layout.
|
||||
|
||||
Why these elements matter:
|
||||
- id="tt_{layout._id}": JavaScript uses this to append dynamically created tooltips
|
||||
- cls Contains "mf-tooltip-container": CSS positioning for tooltip overlay layer
|
||||
- Presence verification: Tooltips won't work if container is missing
|
||||
"""
|
||||
tooltip_container = find_one(layout.render(), Div(id=f"tt_{layout._id}"))
|
||||
|
||||
expected = Div(
|
||||
id=f"tt_{layout._id}",
|
||||
cls=Contains("mf-tooltip-container")
|
||||
)
|
||||
|
||||
assert matches(tooltip_container, expected)
|
||||
|
||||
def test_header_right_contains_user_profile(self, layout):
|
||||
"""Test that UserProfile component is rendered in the right header section.
|
||||
|
||||
Why these elements matter:
|
||||
- UserProfile component: Provides authentication and user menu functionality
|
||||
- Position in header right: Conventional placement for user profile controls
|
||||
- Count verification: Ensures component is not duplicated
|
||||
"""
|
||||
header_right = find_one(layout.render(), Div(id=f"{layout._id}_hr"))
|
||||
|
||||
user_profiles = find(header_right, TestObject(UserProfile))
|
||||
|
||||
assert len(user_profiles) == 1, "Header right should contain exactly one UserProfile component"
|
||||
|
||||
def test_layout_initialization_script_is_included(self, layout):
|
||||
"""Test that layout initialization script is included in render output.
|
||||
|
||||
Why these elements matter:
|
||||
- Script presence: Required to initialize layout behavior (resizers, drawers)
|
||||
- initLayout() call: Activates JavaScript functionality for this layout instance
|
||||
- Layout ID parameter: Ensures initialization targets correct layout
|
||||
"""
|
||||
script = find_one(layout.render(), Script())
|
||||
|
||||
expected = TestScript(f"initLayout('{layout._id}');")
|
||||
|
||||
assert matches(script, expected)
|
||||
|
||||
Reference in New Issue
Block a user