Added unit test

This commit is contained in:
2026-03-14 22:16:20 +01:00
parent a4ebd6d61b
commit af83f4b6dc

View File

@@ -0,0 +1,429 @@
"""Unit tests for DataGridFormattingManager."""
import shutil
import uuid
import pytest
from fasthtml.common import Div, Form, Input, Span
from myfasthtml.controls.DataGridFormattingManager import DataGridFormattingManager
from myfasthtml.controls.DslEditor import DslEditor
from myfasthtml.controls.Menu import Menu
from myfasthtml.controls.Panel import Panel
from myfasthtml.core.formatting.dataclasses import FormatRule, RulePreset, Style
from myfasthtml.core.formatting.presets import DEFAULT_RULE_PRESETS
from myfasthtml.test.matcher import Contains, TestObject, find_one, matches
from .conftest import root_instance
@pytest.fixture(autouse=True)
def cleanup_db():
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
@pytest.fixture
def fmgr(root_instance):
uid = uuid.uuid4().hex[:8]
return DataGridFormattingManager(root_instance, _id=f"fmgr-{uid}")
@pytest.fixture
def user_preset():
return RulePreset(name="my_preset", description="My preset", rules=[], dsl="")
class TestDataGridFormattingManagerBehaviour:
"""Tests for DataGridFormattingManager behavior and logic."""
# ------------------------------------------------------------------
# Helper methods
# ------------------------------------------------------------------
def test_i_can_get_all_presets_includes_builtins_and_user(self, fmgr, user_preset):
"""Test that _get_all_presets() returns builtins first, then user presets.
Why: Builtins must appear before user presets to maintain consistent ordering
in the search list.
"""
fmgr._state.presets.append(user_preset)
all_presets = fmgr._get_all_presets()
assert all_presets[:len(DEFAULT_RULE_PRESETS)] == list(DEFAULT_RULE_PRESETS.values())
assert all_presets[-1] == user_preset
def test_i_can_check_builtin_preset(self, fmgr):
"""Test that _is_builtin() returns True for a builtin preset name."""
builtin_name = next(iter(DEFAULT_RULE_PRESETS))
assert fmgr._is_builtin(builtin_name) is True
def test_i_cannot_check_user_preset_as_builtin(self, fmgr, user_preset):
"""Test that _is_builtin() returns False for a user preset name."""
fmgr._state.presets.append(user_preset)
assert fmgr._is_builtin(user_preset.name) is False
def test_i_can_get_selected_preset(self, fmgr, user_preset):
"""Test that _get_selected_preset() returns the correct preset when one is selected."""
fmgr._state.presets.append(user_preset)
fmgr._state.selected_name = user_preset.name
result = fmgr._get_selected_preset()
assert result == user_preset
def test_i_get_none_when_no_preset_selected(self, fmgr):
"""Test that _get_selected_preset() returns None when no preset is selected."""
fmgr._state.selected_name = None
assert fmgr._get_selected_preset() is None
def test_i_can_get_user_preset_by_name(self, fmgr, user_preset):
"""Test that _get_user_preset() finds an existing user preset by name."""
fmgr._state.presets.append(user_preset)
result = fmgr._get_user_preset(user_preset.name)
assert result == user_preset
def test_i_get_none_for_missing_user_preset(self, fmgr):
"""Test that _get_user_preset() returns None for an unknown preset name."""
result = fmgr._get_user_preset("nonexistent")
assert result is None
# ------------------------------------------------------------------
# Mode changes
# ------------------------------------------------------------------
def test_i_can_switch_to_new_mode(self, fmgr):
"""Test that handle_new_preset() sets ns_mode to 'new'.
Why: The mode controls which form/view is rendered in _mk_main_content().
"""
fmgr.handle_new_preset()
assert fmgr._state.ns_mode == "new"
def test_i_can_cancel_to_view_mode(self, fmgr):
"""Test that handle_cancel() resets ns_mode to 'view'."""
fmgr._state.ns_mode = "new"
fmgr.handle_cancel()
assert fmgr._state.ns_mode == "view"
def test_i_can_switch_to_rename_mode_for_user_preset(self, fmgr, user_preset):
"""Test that handle_rename_preset() sets ns_mode to 'rename' for a user preset."""
fmgr._state.presets.append(user_preset)
fmgr._state.selected_name = user_preset.name
fmgr.handle_rename_preset()
assert fmgr._state.ns_mode == "rename"
def test_i_cannot_rename_builtin_preset(self, fmgr):
"""Test that handle_rename_preset() does NOT change mode for a builtin preset.
Why: Builtin presets are read-only and should not be renameable.
"""
builtin_name = next(iter(DEFAULT_RULE_PRESETS))
fmgr._state.selected_name = builtin_name
fmgr.handle_rename_preset()
assert fmgr._state.ns_mode == "view"
def test_i_cannot_rename_when_no_selection(self, fmgr):
"""Test that handle_rename_preset() does NOT change mode without a selection."""
fmgr._state.selected_name = None
fmgr.handle_rename_preset()
assert fmgr._state.ns_mode == "view"
# ------------------------------------------------------------------
# Deletion
# ------------------------------------------------------------------
def test_i_can_delete_user_preset(self, fmgr, user_preset):
"""Test that handle_delete_preset() removes the preset and clears the selection.
Why: Deletion must remove from state.presets and reset selected_name to avoid
referencing a deleted preset.
"""
fmgr._state.presets.append(user_preset)
fmgr._state.selected_name = user_preset.name
fmgr.handle_delete_preset()
assert user_preset not in fmgr._state.presets
assert fmgr._state.selected_name is None
def test_i_cannot_delete_builtin_preset(self, fmgr):
"""Test that handle_delete_preset() does NOT delete a builtin preset.
Why: Builtin presets are immutable and must always be available.
"""
builtin_name = next(iter(DEFAULT_RULE_PRESETS))
fmgr._state.selected_name = builtin_name
initial_count = len(fmgr._state.presets)
fmgr.handle_delete_preset()
assert len(fmgr._state.presets) == initial_count
assert fmgr._state.selected_name == builtin_name
def test_i_cannot_delete_when_no_selection(self, fmgr):
"""Test that handle_delete_preset() does nothing without a selection."""
fmgr._state.selected_name = None
initial_count = len(fmgr._state.presets)
fmgr.handle_delete_preset()
assert len(fmgr._state.presets) == initial_count
# ------------------------------------------------------------------
# Selection
# ------------------------------------------------------------------
def test_i_can_select_user_preset_as_editable(self, fmgr, user_preset):
"""Test that handle_select_preset() sets readonly=False for user presets.
Why: User presets are editable, so the editor must not be read-only.
"""
fmgr._state.presets.append(user_preset)
fmgr.handle_select_preset(user_preset.name)
assert fmgr._state.selected_name == user_preset.name
assert fmgr._editor.conf.readonly is False
def test_i_can_select_builtin_preset_as_readonly(self, fmgr):
"""Test that handle_select_preset() sets readonly=True for builtin presets.
Why: Builtin presets must be protected from edits.
"""
builtin_name = next(iter(DEFAULT_RULE_PRESETS))
fmgr.handle_select_preset(builtin_name)
assert fmgr._state.selected_name == builtin_name
assert fmgr._editor.conf.readonly is True
# ------------------------------------------------------------------
# Preset creation
# ------------------------------------------------------------------
def test_i_can_confirm_new_preset(self, fmgr):
"""Test that handle_confirm_new() creates a preset, selects it, and returns to 'view'.
Why: After confirmation, the new preset must appear in the list, be selected,
and the mode must return to 'view'.
"""
fmgr._state.ns_mode = "new"
fmgr.handle_confirm_new({"name": "alpha", "description": "Alpha preset"})
names = [p.name for p in fmgr._state.presets]
assert "alpha" in names
assert fmgr._state.selected_name == "alpha"
assert fmgr._state.ns_mode == "view"
def test_i_cannot_confirm_new_preset_with_empty_name(self, fmgr):
"""Test that handle_confirm_new() returns to view without creating if name is empty.
Why: A preset without a name is invalid and must be rejected silently.
"""
fmgr._state.ns_mode = "new"
initial_count = len(fmgr._state.presets)
fmgr.handle_confirm_new({"name": " ", "description": ""})
assert len(fmgr._state.presets) == initial_count
assert fmgr._state.ns_mode == "view"
def test_i_cannot_confirm_new_preset_with_duplicate_name(self, fmgr, user_preset):
"""Test that handle_confirm_new() rejects a name that already exists.
Why: Preset names must be unique across builtins and user presets.
"""
fmgr._state.presets.append(user_preset)
fmgr._state.ns_mode = "new"
initial_count = len(fmgr._state.presets)
fmgr.handle_confirm_new({"name": user_preset.name, "description": ""})
assert len(fmgr._state.presets) == initial_count
assert fmgr._state.ns_mode == "view"
# ------------------------------------------------------------------
# Preset renaming
# ------------------------------------------------------------------
def test_i_can_confirm_rename_preset(self, fmgr, user_preset):
"""Test that handle_confirm_rename() renames the preset and updates the selection.
Why: After renaming, selected_name must point to the new name and the original
name must no longer exist.
"""
fmgr._state.presets.append(user_preset)
fmgr._state.selected_name = user_preset.name
original_name = user_preset.name
fmgr.handle_confirm_rename({"name": "renamed", "description": "New description"})
assert fmgr._state.selected_name == "renamed"
assert fmgr._get_user_preset("renamed") is not None
assert fmgr._get_user_preset(original_name) is None
def test_i_cannot_confirm_rename_to_existing_name(self, fmgr, user_preset):
"""Test that handle_confirm_rename() rejects a rename to an already existing name.
Why: Allowing duplicate names would break preset lookup by name.
"""
other_preset = RulePreset(name="other_preset", description="", rules=[], dsl="")
fmgr._state.presets.extend([user_preset, other_preset])
fmgr._state.selected_name = user_preset.name
fmgr.handle_confirm_rename({"name": other_preset.name, "description": ""})
assert fmgr._get_user_preset(user_preset.name) is not None
assert fmgr._state.ns_mode == "view"
def test_i_can_confirm_rename_with_same_name(self, fmgr, user_preset):
"""Test that handle_confirm_rename() allows keeping the same name (description-only update).
Why: Renaming to the same name should succeed — it allows updating description only.
"""
fmgr._state.presets.append(user_preset)
fmgr._state.selected_name = user_preset.name
fmgr.handle_confirm_rename({"name": user_preset.name, "description": "Updated"})
assert fmgr._state.selected_name == user_preset.name
assert fmgr._get_user_preset(user_preset.name).description == "Updated"
class TestDataGridFormattingManagerRender:
"""Tests for DataGridFormattingManager HTML rendering."""
@pytest.fixture
def fmgr(self, root_instance):
uid = uuid.uuid4().hex[:8]
return DataGridFormattingManager(root_instance, _id=f"fmgr-render-{uid}")
@pytest.fixture
def user_preset(self):
return RulePreset(
name="my_preset",
description="My preset",
rules=[FormatRule(style=Style(preset="info"))],
dsl="",
)
def test_render_global_structure(self, fmgr):
"""Test that DataGridFormattingManager renders with correct global structure.
Why these elements matter:
- id=fmgr._id: Root identifier required for HTMX outerHTML swap targeting
- cls Contains "mf-formatting-manager": Root CSS class for layout styling
- Menu + Panel children: Both are always present regardless of state
"""
html = fmgr.render()
expected = Div(
TestObject(Menu), # menu
TestObject(Panel), # panel
id=fmgr._id,
cls=Contains("mf-formatting-manager"),
)
assert matches(html, expected)
@pytest.mark.parametrize("text, expected_cls", [
("format", "badge-secondary"),
("style", "badge-primary"),
("built-in", "badge-ghost"),
])
def test_i_can_render_badge(self, fmgr, text, expected_cls):
"""Test that _mk_badge() renders a Span with the correct badge class.
Why these elements matter:
- Span text content: Identifies the badge type in the UI
- cls Contains expected_cls: Badge variant determines the visual style applied to the cell
"""
badge = fmgr._mk_badge(text)
expected = Span(text, cls=Contains(expected_cls))
assert matches(badge, expected)
def test_i_can_render_editor_placeholder_when_no_preset_selected(self, fmgr):
"""Test that _mk_editor_view() shows a placeholder when no preset is selected.
Why these elements matter:
- cls Contains "mf-fmgr-placeholder": Allows CSS targeting of the empty state
- Text content: Guides the user to select a preset
"""
fmgr._state.selected_name = None
view = fmgr._mk_editor_view()
expected = Div("Select a preset to edit", cls=Contains("mf-fmgr-placeholder"))
assert matches(view, expected)
def test_i_can_render_editor_view_when_preset_selected(self, fmgr, user_preset):
"""Test that _mk_editor_view() renders the preset header and DSL editor.
Why these elements matter:
- cls Contains "mf-fmgr-editor-view": Root class for the editor area
- Header Div: Shows preset metadata above the editor
- DslEditor: The editing surface for the preset DSL
"""
fmgr._state.presets.append(user_preset)
fmgr._state.selected_name = user_preset.name
view = fmgr._mk_editor_view()
expected = Div(
Div(cls=Contains("mf-fmgr-editor-meta")), # preset header
TestObject(DslEditor),
cls=Contains("mf-fmgr-editor-view"),
)
assert matches(view, expected)
def test_i_can_render_new_form_structure(self, fmgr):
"""Test that _mk_new_form() contains name and description inputs inside a form.
Why these elements matter:
- cls Contains "mf-fmgr-form": Root class for form styling
- Input name="name": Required field for the new preset identifier
- Input name="description": Optional field for preset description
"""
# Step 1: validate root wrapper
form_div = fmgr._mk_new_form()
assert matches(form_div, Div(cls=Contains("mf-fmgr-form")))
# Step 2: find the form and validate inputs
expected_form = Form()
del expected_form.attrs["enctype"]
form = find_one(form_div, expected_form)
find_one(form, Input(name="name"))
find_one(form, Input(name="description"))
def test_i_can_render_rename_form_with_preset_values(self, fmgr, user_preset):
"""Test that _mk_rename_form() pre-fills the name input with the selected preset's name.
Why these elements matter:
- Input value=preset.name: Pre-filled so the user edits rather than retypes the name
- Input name="name": The field submitted on confirm
"""
fmgr._state.presets.append(user_preset)
fmgr._state.selected_name = user_preset.name
# Step 1: find the form
form_div = fmgr._mk_rename_form()
expected_form = Form()
del expected_form.attrs["enctype"]
form = find_one(form_div, expected_form)
# Step 2: validate pre-filled name input
name_input = find_one(form, Input(name="name"))
assert matches(name_input, Input(name="name", value=user_preset.name))
@pytest.mark.parametrize("mode, expected_cls", [
("new", "mf-fmgr-form"),
("rename", "mf-fmgr-form"),
("view", "mf-fmgr-editor-view"),
])
def test_i_can_render_main_content_per_mode(self, fmgr, user_preset, mode, expected_cls):
"""Test that _mk_main_content() delegates to the correct sub-view per mode.
Why these elements matter:
- cls Contains expected_cls: Verifies that the correct form/view is rendered
based on the current ns_mode state
"""
fmgr._state.presets.append(user_preset)
fmgr._state.selected_name = user_preset.name
fmgr._state.ns_mode = mode
content = fmgr._mk_main_content()
assert matches(content, Div(cls=Contains(expected_cls)))