2 Commits

Author SHA1 Message Date
f773fd1611 Keyboard.py : ajout de enabled dans add(), nouveau render() retournant (Script,
control_div), et méthodes mk_enable / mk_disable
  - keyboard.js : nouvelle signature add_keyboard_support(elementId, controlDivId,
  combinationsJson), fonction isCombinationEnabled(), vérification avant déclenchement
  - test_Keyboard.py : 8 tests couvrant les comportements et le rendu
2026-03-14 23:29:18 +01:00
af83f4b6dc Added unit test 2026-03-14 22:16:20 +01:00
4 changed files with 641 additions and 8 deletions

View File

@@ -217,10 +217,11 @@
anyHasLongerSequence = true; anyHasLongerSequence = true;
} }
// Collect matches, respecting require_inside flag // Collect matches, respecting require_inside and enabled flags
if (hasMatch) { if (hasMatch) {
const requireInside = currentNode.config["require_inside"] === true; const requireInside = currentNode.config["require_inside"] === true;
if (!requireInside || isInside) { const enabled = isCombinationEnabled(data.controlDivId, currentNode.combinationStr);
if (enabled && (!requireInside || isInside)) {
currentMatches.push({ currentMatches.push({
elementId: elementId, elementId: elementId,
config: currentNode.config, config: currentNode.config,
@@ -328,12 +329,29 @@
} }
} }
/**
* Check if a combination is enabled via the control div
* @param {string} controlDivId - The ID of the keyboard control div
* @param {string} combinationStr - The combination string (e.g., "esc")
* @returns {boolean} - True if enabled (default: true if entry not found)
*/
function isCombinationEnabled(controlDivId, combinationStr) {
const controlDiv = document.getElementById(controlDivId);
if (!controlDiv) return true;
const entry = controlDiv.querySelector(`[data-combination="${combinationStr}"]`);
if (!entry) return true;
return entry.dataset.enabled !== 'false';
}
/** /**
* Add keyboard support to an element * Add keyboard support to an element
* @param {string} elementId - The ID of the element * @param {string} elementId - The ID of the element
* @param {string} controlDivId - The ID of the keyboard control div
* @param {string} combinationsJson - JSON string of combinations mapping * @param {string} combinationsJson - JSON string of combinations mapping
*/ */
window.add_keyboard_support = function (elementId, combinationsJson) { window.add_keyboard_support = function (elementId, controlDivId, combinationsJson) {
// Parse the combinations JSON // Parse the combinations JSON
const combinations = JSON.parse(combinationsJson); const combinations = JSON.parse(combinationsJson);
@@ -350,7 +368,8 @@
// Add to registry // Add to registry
KeyboardRegistry.elements.set(elementId, { KeyboardRegistry.elements.set(elementId, {
tree: tree, tree: tree,
element: element element: element,
controlDivId: controlDivId
}); });
// Attach global listener if not already attached // Attach global listener if not already attached

View File

@@ -1,9 +1,11 @@
import json import json
from fasthtml.components import Div
from fasthtml.xtend import Script from fasthtml.xtend import Script
from myfasthtml.core.commands import Command from myfasthtml.core.commands import Command
from myfasthtml.core.instances import MultipleInstance from myfasthtml.core.instances import MultipleInstance
from myfasthtml.core.utils import make_html_id
class Keyboard(MultipleInstance): class Keyboard(MultipleInstance):
@@ -16,18 +18,38 @@ class Keyboard(MultipleInstance):
def __init__(self, parent, combinations=None, _id=None): def __init__(self, parent, combinations=None, _id=None):
super().__init__(parent, _id=_id) super().__init__(parent, _id=_id)
self.combinations = combinations or {} self.combinations = combinations or {}
def add(self, sequence: str, command: Command, require_inside: bool = True): def add(self, sequence: str, command: Command, require_inside: bool = True, enabled: bool = True):
self.combinations[sequence] = {"command": command, "require_inside": require_inside} self.combinations[sequence] = {"command": command, "require_inside": require_inside, "enabled": enabled}
return self return self
def render(self): def render(self):
str_combinations = {} str_combinations = {}
control_children = []
for sequence, value in self.combinations.items(): for sequence, value in self.combinations.items():
params = value["command"].get_htmx_params() params = value["command"].get_htmx_params()
params["require_inside"] = value.get("require_inside", True) params["require_inside"] = value.get("require_inside", True)
str_combinations[sequence] = params str_combinations[sequence] = params
return Script(f"add_keyboard_support('{self._parent.get_id()}', '{json.dumps(str_combinations)}')") control_children.append(
Div(id=f"{self.get_id()}-{make_html_id(sequence)}",
data_combination=sequence,
data_enabled="true" if value.get("enabled", True) else "false")
)
script = Script(f"add_keyboard_support('{self._parent.get_id()}', '{self.get_id()}', '{json.dumps(str_combinations)}')")
control_div = Div(*control_children, id=self.get_id(), name="keyboard")
return script, control_div
def mk_enable(self, sequence: str):
return Div(id=f"{self.get_id()}-{make_html_id(sequence)}",
data_combination=sequence,
data_enabled="true",
hx_swap_oob="true")
def mk_disable(self, sequence: str):
return Div(id=f"{self.get_id()}-{make_html_id(sequence)}",
data_combination=sequence,
data_enabled="false",
hx_swap_oob="true")
def __ft__(self): def __ft__(self):
return self.render() return self.render()

View File

@@ -0,0 +1,163 @@
"""Unit tests for the Keyboard control."""
import pytest
from fasthtml.components import Div
from myfasthtml.controls.Keyboard import Keyboard
from myfasthtml.core.commands import Command
from myfasthtml.core.utils import make_html_id
from myfasthtml.test.matcher import matches, find, find_one, AnyValue, TestScript
from .conftest import root_instance
@pytest.fixture
def cmd():
return Command("test_keyboard_cmd", "Test keyboard command", None, lambda: None)
class TestKeyboardBehaviour:
"""Tests for Keyboard behavior and logic."""
def test_i_can_add_combination_with_default_enabled(self, root_instance, cmd):
"""Test that enabled defaults to True when not specified in add().
Why this matters:
- All combinations should be active by default without requiring explicit opt-in.
"""
kb = Keyboard(root_instance)
kb.add("esc", cmd)
assert kb.combinations["esc"]["enabled"] is True
def test_i_can_add_combination_with_enabled_false(self, root_instance, cmd):
"""Test that enabled=False is correctly stored in the combination definition.
Why this matters:
- Combinations can be declared inactive at init time, which controls the
initial data-enabled value in the rendered DOM.
"""
kb = Keyboard(root_instance)
kb.add("esc", cmd, enabled=False)
assert kb.combinations["esc"]["enabled"] is False
class TestKeyboardRender:
"""Tests for Keyboard HTML rendering."""
@pytest.fixture
def keyboard(self, root_instance):
return Keyboard(root_instance)
def test_keyboard_layout_is_rendered(self, keyboard, cmd):
"""Test that render() returns a tuple of (Script, Div).
Why these elements matter:
- tuple length 2: render() must produce exactly a script and a control div
- script tag: the JavaScript call that registers keyboard shortcuts
- div tag: the DOM control div used by JS to check enabled state at runtime
"""
keyboard.add("esc", cmd)
result = keyboard.render()
assert len(result) == 2
script, control_div = result
assert script.tag == "script"
assert control_div.tag == "div"
def test_i_can_render_script_with_control_div_id(self, keyboard, cmd):
"""Test that the rendered script includes controlDivId as the second argument.
Why this matters:
- The JS function add_keyboard_support() now requires 3 args: elementId,
controlDivId, combinationsJson. The controlDivId links the keyboard
registry entry to its DOM control div for enabled-state lookups.
"""
keyboard.add("esc", cmd)
script, _ = keyboard.render()
expected_prefix = (
f"add_keyboard_support('{keyboard._parent.get_id()}', '{keyboard.get_id()}', "
)
assert matches(script, TestScript(expected_prefix))
def test_i_can_render_control_div_attributes(self, keyboard, cmd):
"""Test that the control div has the correct id and name attributes.
Why these attributes matter:
- id=keyboard.get_id(): the JS uses this ID to look up enabled state
- name="keyboard": semantic marker for readability and DOM inspection
"""
keyboard.add("esc", cmd)
_, control_div = keyboard.render()
expected = Div(id=keyboard.get_id(), name="keyboard")
assert matches(control_div, expected)
def test_i_can_render_one_child_per_combination(self, keyboard, cmd):
"""Test that the control div contains exactly one child div per combination.
Why this matters:
- Each combination needs its own div so the JS can check its enabled
state independently via data-combination attribute lookup.
"""
keyboard.add("esc", cmd)
keyboard.add("ctrl+s", cmd)
_, control_div = keyboard.render()
children = find(control_div, Div(data_combination=AnyValue()))
assert len(children) == 2, "Should have one child div per registered combination"
@pytest.mark.parametrize("enabled, expected_value", [
(True, "true"),
(False, "false"),
])
def test_i_can_render_combination_enabled_state(self, keyboard, cmd, enabled, expected_value):
"""Test that data-enabled reflects the enabled flag passed to add().
Why this matters:
- The JS reads data-enabled at keypress time to decide whether to
trigger the combination. The rendered value must match the Python flag.
"""
keyboard.add("esc", cmd, enabled=enabled)
_, control_div = keyboard.render()
child = find_one(control_div, Div(data_combination="esc"))
assert matches(child, Div(data_enabled=expected_value))
def test_i_can_render_child_id_sanitizes_combination(self, keyboard, cmd):
"""Test that the child div id is derived from make_html_id on the combination string.
Why this matters:
- Combination strings like 'ctrl+s' contain characters invalid in HTML IDs.
make_html_id() sanitizes them ('+''-'), enabling targeted OOB swaps
via mk_enable/mk_disable without referencing the full control div.
"""
keyboard.add("ctrl+s", cmd)
_, control_div = keyboard.render()
expected_id = f"{keyboard.get_id()}-{make_html_id('ctrl+s')}"
children = find(control_div, Div(id=expected_id))
assert len(children) == 1, f"Expected exactly one child with id '{expected_id}'"
@pytest.mark.parametrize("method_name, expected_enabled", [
("mk_enable", "true"),
("mk_disable", "false"),
])
def test_i_can_mk_enable_and_disable(self, keyboard, method_name, expected_enabled):
"""Test that mk_enable/mk_disable return a correct OOB swap div.
Why these attributes matter:
- id: must match the child div id so HTMX replaces the right element
- data-combination: allows the JS to identify the combination
- data-enabled: the new state to apply
- hx-swap-oob: triggers the out-of-band swap without a full page update
"""
result = getattr(keyboard, method_name)("esc")
expected = Div(
id=f"{keyboard.get_id()}-{make_html_id('esc')}",
data_combination="esc",
data_enabled=expected_enabled,
hx_swap_oob="true"
)
assert matches(result, expected)

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)))