Working version of DataGridColumnsManager.py where columns can be updated
This commit is contained in:
469
tests/controls/test_datagrid_columns_manager.py
Normal file
469
tests/controls/test_datagrid_columns_manager.py
Normal file
@@ -0,0 +1,469 @@
|
||||
import shutil
|
||||
from dataclasses import dataclass, field
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from fasthtml.common import Div, Input, Label, Form, Fieldset, Select
|
||||
|
||||
from myfasthtml.controls.DataGridColumnsManager import DataGridColumnsManager, Commands
|
||||
from myfasthtml.controls.Search import Search
|
||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState
|
||||
from myfasthtml.core.constants import ColumnType
|
||||
from myfasthtml.core.instances import InstancesManager, MultipleInstance
|
||||
from myfasthtml.test.matcher import (
|
||||
matches, find_one, find, Contains, TestIcon, TestObject
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockDatagridState:
|
||||
"""Mock state object that mimics DatagridState."""
|
||||
columns: list = field(default_factory=list)
|
||||
|
||||
|
||||
class MockDataGrid(MultipleInstance):
|
||||
"""Mock DataGrid parent for testing DataGridColumnsManager."""
|
||||
|
||||
def __init__(self, parent, columns=None, _id=None):
|
||||
super().__init__(parent, _id=_id)
|
||||
self._state = MockDatagridState(columns=columns or [])
|
||||
self._save_state_called = False
|
||||
|
||||
def get_state(self):
|
||||
return self._state
|
||||
|
||||
def save_state(self):
|
||||
self._save_state_called = True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_datagrid(root_instance):
|
||||
"""Create a mock DataGrid with sample columns."""
|
||||
columns = [
|
||||
DataGridColumnState(col_id="name", col_index=0, title="Name", type=ColumnType.Text, visible=True),
|
||||
DataGridColumnState(col_id="age", col_index=1, title="Age", type=ColumnType.Number, visible=True),
|
||||
DataGridColumnState(col_id="email", col_index=2, title="Email", type=ColumnType.Text, visible=False),
|
||||
]
|
||||
yield MockDataGrid(root_instance, columns=columns, _id="test-datagrid")
|
||||
InstancesManager.reset()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def columns_manager(mock_datagrid):
|
||||
"""Create a DataGridColumnsManager instance for testing."""
|
||||
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
|
||||
yield DataGridColumnsManager(mock_datagrid)
|
||||
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
|
||||
|
||||
|
||||
class TestDataGridColumnsManagerBehaviour:
|
||||
"""Tests for DataGridColumnsManager behavior and logic."""
|
||||
|
||||
# =========================================================================
|
||||
# Initialization
|
||||
# =========================================================================
|
||||
|
||||
def test_i_can_create_columns_manager(self, mock_datagrid):
|
||||
"""Test that DataGridColumnsManager can be created with a DataGrid parent."""
|
||||
cm = DataGridColumnsManager(mock_datagrid)
|
||||
|
||||
assert cm is not None
|
||||
assert cm._parent == mock_datagrid
|
||||
assert isinstance(cm.commands, Commands)
|
||||
|
||||
# =========================================================================
|
||||
# Columns Property
|
||||
# =========================================================================
|
||||
|
||||
def test_columns_property_returns_parent_state_columns(self, columns_manager, mock_datagrid):
|
||||
"""Test that columns property returns columns from parent's state."""
|
||||
columns = columns_manager.columns
|
||||
|
||||
assert columns == mock_datagrid.get_state().columns
|
||||
assert len(columns) == 3
|
||||
assert columns[0].col_id == "name"
|
||||
|
||||
# =========================================================================
|
||||
# Get Column Definition
|
||||
# =========================================================================
|
||||
|
||||
def test_i_can_get_existing_column_by_id(self, columns_manager):
|
||||
"""Test finding an existing column by its ID."""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
|
||||
assert col_def is not None
|
||||
assert col_def.col_id == "name"
|
||||
assert col_def.title == "Name"
|
||||
|
||||
def test_i_cannot_get_nonexistent_column(self, columns_manager):
|
||||
"""Test that getting a nonexistent column returns None."""
|
||||
col_def = columns_manager._get_col_def_from_col_id("nonexistent")
|
||||
|
||||
assert col_def is None
|
||||
|
||||
# =========================================================================
|
||||
# Toggle Column Visibility
|
||||
# =========================================================================
|
||||
|
||||
@pytest.mark.parametrize("col_id, initial_visible, expected_visible", [
|
||||
("name", True, False), # visible -> hidden
|
||||
("email", False, True), # hidden -> visible
|
||||
])
|
||||
def test_i_can_toggle_column_visibility(self, columns_manager, col_id, initial_visible, expected_visible):
|
||||
"""Test toggling column visibility from visible to hidden and vice versa."""
|
||||
col_def = columns_manager._get_col_def_from_col_id(col_id)
|
||||
assert col_def.visible == initial_visible
|
||||
|
||||
columns_manager.toggle_column(col_id)
|
||||
|
||||
assert col_def.visible == expected_visible
|
||||
|
||||
def test_toggle_column_saves_state(self, columns_manager, mock_datagrid):
|
||||
"""Test that toggle_column calls save_state on parent."""
|
||||
mock_datagrid._save_state_called = False
|
||||
|
||||
columns_manager.toggle_column("name")
|
||||
|
||||
assert mock_datagrid._save_state_called is True
|
||||
|
||||
def test_toggle_column_returns_column_label(self, columns_manager):
|
||||
"""Test that toggle_column returns the updated column label."""
|
||||
result = columns_manager.toggle_column("name")
|
||||
|
||||
# Result should be a Div with the column label structure
|
||||
assert result is not None
|
||||
assert hasattr(result, 'tag')
|
||||
|
||||
def test_i_cannot_toggle_nonexistent_column(self, columns_manager):
|
||||
"""Test that toggling a nonexistent column returns an error message."""
|
||||
result = columns_manager.toggle_column("nonexistent")
|
||||
|
||||
expected = Div("Column 'nonexistent' not found")
|
||||
assert matches(result, expected)
|
||||
|
||||
# =========================================================================
|
||||
# Show Column Details
|
||||
# =========================================================================
|
||||
|
||||
def test_i_can_show_column_details_for_existing_column(self, columns_manager):
|
||||
"""Test that show_column_details returns the details form for an existing column."""
|
||||
result = columns_manager.show_column_details("name")
|
||||
|
||||
# Should contain a Form - check by finding form tag in children
|
||||
expected = Form()
|
||||
del(expected.attrs["enctype"]) # hack. We don't know why enctype is added
|
||||
forms = find(result, expected)
|
||||
assert len(forms) == 1, "Should contain exactly one form"
|
||||
|
||||
def test_i_cannot_show_details_for_nonexistent_column(self, columns_manager):
|
||||
"""Test that showing details for nonexistent column returns error message."""
|
||||
result = columns_manager.show_column_details("nonexistent")
|
||||
|
||||
expected = Div("Column 'nonexistent' not found")
|
||||
assert matches(result, expected)
|
||||
|
||||
# =========================================================================
|
||||
# Show All Columns
|
||||
# =========================================================================
|
||||
|
||||
def test_show_all_columns_returns_search_component(self, columns_manager):
|
||||
"""Test that show_all_columns returns a Search component."""
|
||||
result = columns_manager.show_all_columns()
|
||||
|
||||
assert isinstance(result, Search)
|
||||
|
||||
def test_show_all_columns_contains_all_columns(self, columns_manager):
|
||||
"""Test that show_all_columns Search contains all columns."""
|
||||
result = columns_manager.show_all_columns()
|
||||
|
||||
assert len(result.items) == 3
|
||||
|
||||
# =========================================================================
|
||||
# Update Column
|
||||
# =========================================================================
|
||||
|
||||
def test_i_can_update_column_title(self, columns_manager):
|
||||
"""Test updating a column's title via client_response."""
|
||||
client_response = {"title": "New Name"}
|
||||
|
||||
columns_manager.update_column("name", client_response)
|
||||
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
assert col_def.title == "New Name"
|
||||
|
||||
def test_i_can_update_column_visibility_via_form(self, columns_manager):
|
||||
"""Test updating column visibility via checkbox form value."""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
assert col_def.visible is True
|
||||
|
||||
# Unchecked checkbox sends nothing, checked sends "on"
|
||||
client_response = {"visible": "off"} # Not "on" means unchecked
|
||||
columns_manager.update_column("name", client_response)
|
||||
|
||||
assert col_def.visible is False
|
||||
|
||||
# Check it back on
|
||||
client_response = {"visible": "on"}
|
||||
columns_manager.update_column("name", client_response)
|
||||
|
||||
assert col_def.visible is True
|
||||
|
||||
def test_i_can_update_column_type(self, columns_manager):
|
||||
"""Test updating a column's type."""
|
||||
client_response = {"type": "Number"}
|
||||
|
||||
columns_manager.update_column("name", client_response)
|
||||
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
assert col_def.type == ColumnType.Number
|
||||
|
||||
def test_i_can_update_column_width(self, columns_manager):
|
||||
"""Test updating a column's width."""
|
||||
client_response = {"width": "200"}
|
||||
|
||||
columns_manager.update_column("name", client_response)
|
||||
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
assert col_def.width == 200
|
||||
|
||||
def test_update_column_saves_state(self, columns_manager, mock_datagrid):
|
||||
"""Test that update_column calls save_state on parent."""
|
||||
mock_datagrid._save_state_called = False
|
||||
|
||||
columns_manager.update_column("name", {"title": "Updated"})
|
||||
|
||||
assert mock_datagrid._save_state_called is True
|
||||
|
||||
def test_update_column_ignores_unknown_attributes(self, columns_manager):
|
||||
"""Test that update_column ignores attributes not in DataGridColumnState."""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
original_title = col_def.title
|
||||
|
||||
client_response = {"unknown_attr": "value", "title": "New Title"}
|
||||
columns_manager.update_column("name", client_response)
|
||||
|
||||
# unknown_attr should be ignored, title should be updated
|
||||
assert col_def.title == "New Title"
|
||||
assert not hasattr(col_def, "unknown_attr")
|
||||
|
||||
def test_i_cannot_update_nonexistent_column(self, columns_manager):
|
||||
"""Test that updating nonexistent column returns mk_all_columns result."""
|
||||
result = columns_manager.update_column("nonexistent", {"title": "Test"})
|
||||
|
||||
# Should return the all columns view (Search component)
|
||||
assert isinstance(result, Search)
|
||||
|
||||
|
||||
class TestDataGridColumnsManagerRender:
|
||||
"""Tests for DataGridColumnsManager HTML rendering."""
|
||||
|
||||
@pytest.fixture
|
||||
def columns_manager(self, mock_datagrid):
|
||||
"""Create a fresh DataGridColumnsManager for render tests."""
|
||||
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
|
||||
cm = DataGridColumnsManager(mock_datagrid)
|
||||
yield cm
|
||||
shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
|
||||
|
||||
# =========================================================================
|
||||
# Global Structure
|
||||
# =========================================================================
|
||||
|
||||
def test_i_can_render_columns_manager_with_columns(self, columns_manager):
|
||||
"""Test that DataGridColumnsManager renders with correct global structure.
|
||||
|
||||
Why these elements matter:
|
||||
- id: Required for HTMX targeting in commands
|
||||
- Contains Search component: Main content for column list
|
||||
"""
|
||||
html = columns_manager.render()
|
||||
|
||||
expected = Div(
|
||||
TestObject(Search), # Search component
|
||||
id=columns_manager._id,
|
||||
)
|
||||
|
||||
assert matches(html, expected)
|
||||
|
||||
# =========================================================================
|
||||
# mk_column_label
|
||||
# =========================================================================
|
||||
|
||||
def test_column_label_has_checkbox_and_details_navigation(self, columns_manager):
|
||||
"""Test that column label contains checkbox and navigation to details.
|
||||
|
||||
Why these elements matter:
|
||||
- Checkbox (Input type=checkbox): Controls column visibility
|
||||
- Label with column ID: Identifies the column
|
||||
- Chevron icon: Indicates navigation to details
|
||||
- id with tcolman_ prefix: Required for HTMX swap targeting
|
||||
"""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
label = columns_manager.mk_column_label(col_def)
|
||||
|
||||
# Should have the correct ID pattern
|
||||
expected = Div(
|
||||
id=f"tcolman_{columns_manager._id}-name",
|
||||
cls=Contains("flex"),
|
||||
)
|
||||
assert matches(label, expected)
|
||||
|
||||
# Should contain a checkbox
|
||||
checkbox = find_one(label, Input(type="checkbox"))
|
||||
assert checkbox is not None
|
||||
|
||||
# Should contain chevron icon for navigation
|
||||
chevron = find_one(label, TestIcon("chevron_right20_regular"))
|
||||
assert chevron is not None
|
||||
|
||||
def test_column_label_checkbox_is_checked_when_visible(self, columns_manager):
|
||||
"""Test that checkbox is checked when column is visible.
|
||||
|
||||
Why this matters:
|
||||
- checked attribute: Reflects current visibility state
|
||||
- User can see which columns are visible
|
||||
"""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
assert col_def.visible is True
|
||||
|
||||
label = columns_manager.mk_column_label(col_def)
|
||||
checkbox = find_one(label, Input(type="checkbox"))
|
||||
|
||||
# Checkbox should have checked attribute
|
||||
assert checkbox.attrs.get("checked") is True
|
||||
|
||||
def test_column_label_checkbox_is_unchecked_when_hidden(self, columns_manager):
|
||||
"""Test that checkbox is unchecked when column is hidden.
|
||||
|
||||
Why this matters:
|
||||
- No checked attribute: Indicates column is hidden
|
||||
- Visual feedback for user
|
||||
"""
|
||||
col_def = columns_manager._get_col_def_from_col_id("email")
|
||||
assert col_def.visible is False
|
||||
|
||||
label = columns_manager.mk_column_label(col_def)
|
||||
checkbox = find_one(label, Input(type="checkbox"))
|
||||
|
||||
# Checkbox should not have checked attribute (or it should be False/None)
|
||||
checked = checkbox.attrs.get("checked")
|
||||
assert checked is None or checked is False
|
||||
|
||||
# =========================================================================
|
||||
# mk_column_details
|
||||
# =========================================================================
|
||||
|
||||
def test_column_details_contains_all_form_fields(self, columns_manager):
|
||||
"""Test that column details form contains all required fields.
|
||||
|
||||
Why these elements matter:
|
||||
- col_id field (readonly): Shows column identifier
|
||||
- title field: Editable column display name
|
||||
- visible checkbox: Toggle visibility
|
||||
- type select: Change column type
|
||||
- width input: Set column width
|
||||
"""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
details = columns_manager.mk_column_details(col_def)
|
||||
|
||||
# Should contain Form
|
||||
form = Form()
|
||||
del form.attrs["enctype"]
|
||||
form = find_one(details, form)
|
||||
assert form is not None
|
||||
|
||||
# Should contain all required input fields
|
||||
col_id_input = find_one(form, Input(name="col_id"))
|
||||
assert col_id_input is not None
|
||||
assert col_id_input.attrs.get("readonly") is True
|
||||
|
||||
title_input = find_one(form, Input(name="title"))
|
||||
assert title_input is not None
|
||||
|
||||
visible_checkbox = find_one(form, Input(name="visible", type="checkbox"))
|
||||
assert visible_checkbox is not None
|
||||
|
||||
type_select = find_one(form, Select(name="type"))
|
||||
assert type_select is not None
|
||||
|
||||
width_input = find_one(form, Input(name="width", type="number"))
|
||||
assert width_input is not None
|
||||
|
||||
def test_column_details_has_back_button(self, columns_manager):
|
||||
"""Test that column details has a back button to return to all columns.
|
||||
|
||||
Why this matters:
|
||||
- Back navigation: User can return to column list
|
||||
- Chevron left icon: Visual indicator of back action
|
||||
"""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
details = columns_manager.mk_column_details(col_def)
|
||||
|
||||
# Should contain back chevron icon
|
||||
back_icon = find_one(details, TestIcon("chevron_left20_regular", wrapper="span"))
|
||||
assert back_icon is not None
|
||||
|
||||
def test_column_details_form_has_fieldset_with_legend(self, columns_manager):
|
||||
"""Test that column details form has a fieldset with legend.
|
||||
|
||||
Why this matters:
|
||||
- Fieldset groups related fields
|
||||
- Legend provides context ("Column details")
|
||||
"""
|
||||
col_def = columns_manager._get_col_def_from_col_id("name")
|
||||
details = columns_manager.mk_column_details(col_def)
|
||||
|
||||
fieldset = find_one(details, Fieldset(legend="Column details"))
|
||||
assert fieldset is not None
|
||||
|
||||
# =========================================================================
|
||||
# mk_all_columns
|
||||
# =========================================================================
|
||||
|
||||
def test_all_columns_uses_search_component(self, columns_manager):
|
||||
"""Test that mk_all_columns returns a Search component.
|
||||
|
||||
Why this matters:
|
||||
- Search component: Enables filtering columns by name
|
||||
- items_names="Columns": Labels the search appropriately
|
||||
"""
|
||||
result = columns_manager.mk_all_columns()
|
||||
|
||||
assert isinstance(result, Search)
|
||||
assert result.items_names == "Columns"
|
||||
|
||||
def test_all_columns_search_has_correct_configuration(self, columns_manager):
|
||||
"""Test that Search component is configured correctly.
|
||||
|
||||
Why these elements matter:
|
||||
- items: Contains all column definitions
|
||||
- get_attr: Extracts col_id for search matching
|
||||
- template: Uses mk_column_label for rendering
|
||||
"""
|
||||
result = columns_manager.mk_all_columns()
|
||||
|
||||
# Should have all 3 columns
|
||||
assert len(result.items) == 3
|
||||
|
||||
# get_attr should return col_id
|
||||
col_def = result.items[0]
|
||||
assert result.get_attr(col_def) == col_def.col_id
|
||||
|
||||
def test_all_columns_renders_all_column_labels(self, columns_manager):
|
||||
"""Test that all columns render produces labels for all columns.
|
||||
|
||||
Why this matters:
|
||||
- All columns visible in list
|
||||
- Each column has its label rendered
|
||||
"""
|
||||
search = columns_manager.mk_all_columns()
|
||||
rendered = search.render()
|
||||
|
||||
# Should find 3 column labels in the results
|
||||
results_div = find_one(rendered, Div(id=f"{search._id}-results"))
|
||||
assert results_div is not None
|
||||
|
||||
# Each column should have a label with tcolman_ prefix
|
||||
for col_id in ["name", "age", "email"]:
|
||||
label = find_one(results_div, Div(id=f"tcolman_{columns_manager._id}-{col_id}"))
|
||||
assert label is not None, f"Column label for '{col_id}' should be present"
|
||||
Reference in New Issue
Block a user