From af83f4b6dc1318f9644700feb198d43f05b1fd99 Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Sat, 14 Mar 2026 22:16:20 +0100 Subject: [PATCH] Added unit test --- .../test_datagrids_formatting_manager.py | 429 ++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 tests/controls/test_datagrids_formatting_manager.py diff --git a/tests/controls/test_datagrids_formatting_manager.py b/tests/controls/test_datagrids_formatting_manager.py new file mode 100644 index 0000000..3cf97e2 --- /dev/null +++ b/tests/controls/test_datagrids_formatting_manager.py @@ -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)))