Fixed unit tests

This commit is contained in:
2026-03-11 20:59:07 +01:00
parent e01d2cd74b
commit e704dad62c
15 changed files with 353 additions and 679 deletions

View File

@@ -78,7 +78,7 @@ def index(session):
layout.left_drawer.add(btn_popup, "Test") layout.left_drawer.add(btn_popup, "Test")
# data grids # data grids
dgs_manager = DataGridsManager(session_instance) dgs_manager = DataGridsManager(session_instance, save_state=True)
layout.left_drawer.add_group("Documents", Div("Documents", layout.left_drawer.add_group("Documents", Div("Documents",
dgs_manager.mk_main_icons(), dgs_manager.mk_main_icons(),
cls="mf-layout-group flex gap-3")) cls="mf-layout-group flex gap-3"))

View File

@@ -224,6 +224,10 @@ class DataGrid(MultipleInstance):
self._columns = None self._columns = None
self.commands = Commands(self) self.commands = Commands(self)
# reset
self._state.selection.selected = None
self._state.selection.last_selected = None
# Obtain DataService from DataServicesManager (no parent hierarchy) # Obtain DataService from DataServicesManager (no parent hierarchy)
data_services_manager = InstancesManager.get_by_type(self._session, DataServicesManager) data_services_manager = InstancesManager.get_by_type(self._session, DataServicesManager)
data_service_id = self.get_data_service_id_from_data_grid_id(self._id) data_service_id = self.get_data_service_id_from_data_grid_id(self._id)
@@ -264,7 +268,7 @@ class DataGrid(MultipleInstance):
# self._columns_manager.bind_command("SaveColumnDetails", self.commands.on_column_changed()) # self._columns_manager.bind_command("SaveColumnDetails", self.commands.on_column_changed())
if self._settings.enable_formatting: if self._settings.enable_formatting:
provider = InstancesManager.get_by_type(self._session, DatagridMetadataProvider, None) provider = DatagridMetadataProvider(self._parent)
completion_engine = FormattingCompletionEngine(provider, self.get_table_name()) completion_engine = FormattingCompletionEngine(provider, self.get_table_name())
editor_conf = DslEditorConf(engine_id=completion_engine.get_id()) editor_conf = DslEditorConf(engine_id=completion_engine.get_id())
dsl = FormattingDSL() dsl = FormattingDSL()
@@ -305,6 +309,10 @@ class DataGrid(MultipleInstance):
def _fast_access(self): def _fast_access(self):
return self._data_service.get_store().ns_fast_access return self._data_service.get_store().ns_fast_access
@property
def _row_data(self):
return self._data_service.get_store().ns_row_data
def _apply_sort(self, df): def _apply_sort(self, df):
if df is None: if df is None:
return None return None
@@ -413,9 +421,9 @@ class DataGrid(MultipleInstance):
if self._state.table_format: if self._state.table_format:
return self._state.table_format return self._state.table_format
# Get global tables formatting from DatagridMetadataProvider # Get global tables formatting from DataGridsManager
provider = InstancesManager.get_by_type(self._session, DatagridMetadataProvider, None) dgm = self.get_parent()
return provider.all_tables_formats if provider is not None else [] return dgm.get_state().all_tables_formats if dgm is not None else []
def _init_columns(self): def _init_columns(self):
# Populate UI state from DataService columns when creating a new grid # Populate UI state from DataService columns when creating a new grid
@@ -494,22 +502,6 @@ class DataGrid(MultipleInstance):
self._state.save() self._state.save()
def handle_reorder_columns(self, order: list[str]):
"""Reorder columns based on a full ordered list of column IDs.
Args:
order: List of col_id strings in the desired display order.
Columns not present in order are appended at the end.
"""
logger.debug(f"handle_reorder_columns: {order=}")
columns_by_id = {c.col_id: c for c in self._state.columns}
new_order = [columns_by_id[col_id] for col_id in order if col_id in columns_by_id]
remaining = [c for c in self._state.columns if c.col_id not in order]
self._state.columns = new_order + remaining
self._init_columns()
self._state.save()
return self.render_partial("table")
def calculate_optimal_column_width(self, col_id: str) -> int: def calculate_optimal_column_width(self, col_id: str) -> int:
""" """
Calculate optimal width for a column based on content. Calculate optimal width for a column based on content.
@@ -625,6 +617,42 @@ class DataGrid(MultipleInstance):
self._state.save() self._state.save()
return self.render_partial() return self.render_partial()
def handle_columns_reorder(self, order: list[str]):
"""Reorder columns based on a full ordered list of column IDs.
Args:
order: List of col_id strings in the desired display order.
Columns not present in order are appended at the end.
"""
logger.debug(f"handle_reorder_columns: {order=}")
columns_by_id = {c.col_id: c for c in self._state.columns}
new_order = [columns_by_id[col_id] for col_id in order if col_id in columns_by_id]
remaining = [c for c in self._state.columns if c.col_id not in order]
self._state.columns = new_order + remaining
self._init_columns()
self._state.save()
return self.render_partial("table")
def handle_columns_updates(self, updates: dict[str, dict]):
logger.debug(f"handle_columns_update: {updates=}")
def _update(col_def):
need_saving = False
for key, value in update.items():
if hasattr(col_def, key):
setattr(col_def, key, value)
need_saving = True
return need_saving
for col_id, update in updates.items():
column_state = [col_def for col_def in self._columns if col_def.col_id == col_id][0]
if _update(column_state.get_col_ui_state()):
self._state.save()
if _update(column_state.get_col_def()):
self._data_service.save_state()
return self.render_partial("table")
def handle_toggle_columns_manager(self): def handle_toggle_columns_manager(self):
logger.debug(f"toggle_columns_manager") logger.debug(f"toggle_columns_manager")
self._panel.set_title(side="right", title="Columns") self._panel.set_title(side="right", title="Columns")
@@ -803,7 +831,7 @@ class DataGrid(MultipleInstance):
formatted_value = None formatted_value = None
rules = self._get_format_rules(col_pos, row_index, col_def) rules = self._get_format_rules(col_pos, row_index, col_def)
if rules: if rules:
row_data = self._df_store.ns_row_data[row_index] if row_index < len(self._df_store.ns_row_data) else None row_data = self._row_data[row_index] if row_index < len(self._row_data) else None
style, formatted_value = self._formatting_engine.apply_format(rules, value, row_data) style, formatted_value = self._formatting_engine.apply_format(rules, value, row_data)
# Use formatted value or convert to string # Use formatted value or convert to string

View File

@@ -24,6 +24,14 @@ class Commands(BaseCommands):
self._owner.handle_on_reorder self._owner.handle_on_reorder
).htmx(target=f"#{self._id}") ).htmx(target=f"#{self._id}")
def toggle_column_visibility(self, col_id: str):
return Command("ToggleColumnVisibility",
"Toggle column visibility",
self._owner,
self._owner.handle_toggle_column_visibility,
kwargs={"col_id": col_id}
).htmx(target=f"#{self._id}")
class DataGridColumnsList(MultipleInstance): class DataGridColumnsList(MultipleInstance):
""" """
@@ -43,7 +51,14 @@ class DataGridColumnsList(MultipleInstance):
def handle_on_reorder(self, order: list): def handle_on_reorder(self, order: list):
logger.debug(f"on_reorder {order=}") logger.debug(f"on_reorder {order=}")
ret = self._parent.handle_reorder_columns(order) ret = self._parent.handle_columns_reorder(order)
return self.render(), ret
def handle_toggle_column_visibility(self, col_id):
logger.debug(f"handle_toggle_column_visibility {col_id=}")
col_def = [c for c in self.columns if c.col_id == col_id][0]
updates = {col_id: {"visible": not col_def.visible}}
ret = self._parent.handle_columns_updates(updates)
return self.render(), ret return self.render(), ret
def mk_column_label(self, col_def: DataGridColumnState): def mk_column_label(self, col_def: DataGridColumnState):
@@ -51,7 +66,7 @@ class DataGridColumnsList(MultipleInstance):
mk.icon(grip_horizontal, cls="mf-drag-handle cursor-grab mr-1 opacity-40"), mk.icon(grip_horizontal, cls="mf-drag-handle cursor-grab mr-1 opacity-40"),
mk.mk( mk.mk(
Input(type="checkbox", cls="checkbox checkbox-sm", checked=col_def.visible), Input(type="checkbox", cls="checkbox checkbox-sm", checked=col_def.visible),
# command=self.commands.toggle_column(col_def.col_id) command=self.commands.toggle_column_visibility(col_def.col_id)
), ),
mk.mk( mk.mk(
Div( Div(

View File

@@ -127,7 +127,7 @@ class DataGridFormattingEditor(DslEditor):
from myfasthtml.controls.DataGridsManager import DataGridsManager from myfasthtml.controls.DataGridsManager import DataGridsManager
manager = InstancesManager.get_by_type(self._session, DataGridsManager) manager = InstancesManager.get_by_type(self._session, DataGridsManager)
if manager: if manager:
manager.all_tables_formats = tables_rules manager.get_state().all_tables_formats = tables_rules
# Step 6: Update state atomically # Step 6: Update state atomically
self._parent.get_state().update(state) self._parent.get_state().update(state)

View File

@@ -15,6 +15,7 @@ from myfasthtml.core.DataGridsRegistry import DataGridsRegistry
from myfasthtml.core.commands import Command from myfasthtml.core.commands import Command
from myfasthtml.core.data.DataServicesManager import DataServicesManager from myfasthtml.core.data.DataServicesManager import DataServicesManager
from myfasthtml.core.dbmanager import DbObject from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.formatting.dataclasses import FormatRule
from myfasthtml.core.instances import InstancesManager, SingleInstance from myfasthtml.core.instances import InstancesManager, SingleInstance
from myfasthtml.icons.fluent_p1 import table_add20_regular from myfasthtml.icons.fluent_p1 import table_add20_regular
from myfasthtml.icons.fluent_p3 import folder_open20_regular from myfasthtml.icons.fluent_p3 import folder_open20_regular
@@ -31,10 +32,11 @@ class DocumentDefinition:
class DataGridsState(DbObject): class DataGridsState(DbObject):
def __init__(self, owner, name=None): def __init__(self, owner, save_state, name=None):
super().__init__(owner, name=name) super().__init__(owner, save_state=save_state, name=name)
with self.initializing(): with self.initializing():
self.elements: list[DocumentDefinition] = [] self.elements: list[DocumentDefinition] = []
self.all_tables_formats: list[FormatRule] = []
class Commands(BaseCommands): class Commands(BaseCommands):
@@ -87,12 +89,12 @@ class DataGridsManager(SingleInstance):
by DataServicesManager and DataService. by DataServicesManager and DataService.
""" """
def __init__(self, parent, _id=None): def __init__(self, parent, _id=None, save_state=None):
if not getattr(self, "_is_new_instance", False): if not getattr(self, "_is_new_instance", False):
return return
super().__init__(parent, _id=_id) super().__init__(parent, _id=_id)
self.commands = Commands(self) self.commands = Commands(self)
self._state = DataGridsState(self) self._state = DataGridsState(self, save_state)
self._tree = self._mk_tree() self._tree = self._mk_tree()
self._tree.bind_command("SelectNode", self.commands.show_document()) self._tree.bind_command("SelectNode", self.commands.show_document())
self._tree.bind_command("DeleteNode", self.commands.delete_grid(), when="before") self._tree.bind_command("DeleteNode", self.commands.delete_grid(), when="before")
@@ -102,6 +104,9 @@ class DataGridsManager(SingleInstance):
# Data layer — session-scoped singletons # Data layer — session-scoped singletons
self._data_services_manager = DataServicesManager(self._parent) self._data_services_manager = DataServicesManager(self._parent)
def get_state(self):
return self._state
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Grid lifecycle # Grid lifecycle
# ------------------------------------------------------------------ # ------------------------------------------------------------------

View File

@@ -78,6 +78,12 @@ class DataGridColumnState:
self._col_def = col_def self._col_def = col_def
self._col_ui_state = col_ui_state self._col_ui_state = col_ui_state
def get_col_def(self):
return self._col_def
def get_col_ui_state(self):
return self._col_ui_state
@property @property
def col_id(self): def col_id(self):
return self._col_def.col_id return self._col_def.col_id

View File

@@ -84,6 +84,9 @@ class DataService(MultipleInstance):
self._store = DataStore(self, save_state=save_state) self._store = DataStore(self, save_state=save_state)
self._init_store() self._init_store()
def save_state(self):
self._state.save()
@property @property
def columns(self) -> list[ColumnDefinition]: def columns(self) -> list[ColumnDefinition]:
"""Return the list of column definitions.""" """Return the list of column definitions."""

View File

@@ -120,3 +120,10 @@ class DataServicesManager(SingleInstance):
return service.get_store() return service.get_store()
logger.warning(f"DataServicesManager: table '{table_name}' not found") logger.warning(f"DataServicesManager: table '{table_name}' not found")
return None return None
def clear(self):
"""
For test purposes only.
:return:
"""
self._services.clear()

View File

@@ -3,6 +3,7 @@ Completion engine for the formatting DSL.
Implements the BaseCompletionEngine for DataGrid formatting rules. Implements the BaseCompletionEngine for DataGrid formatting rules.
""" """
import logging
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine from myfasthtml.core.dsl.base_completion import BaseCompletionEngine
from myfasthtml.core.dsl.types import Position, Suggestion, CompletionResult from myfasthtml.core.dsl.types import Position, Suggestion, CompletionResult
@@ -11,6 +12,7 @@ from . import presets
from .contexts import Context, DetectedScope, detect_scope, detect_context from .contexts import Context, DetectedScope, detect_scope, detect_context
from .provider import DatagridMetadataProvider from .provider import DatagridMetadataProvider
logger = logging.getLogger("FormattingCompletionEngine")
class FormattingCompletionEngine(BaseCompletionEngine): class FormattingCompletionEngine(BaseCompletionEngine):
""" """
@@ -222,8 +224,8 @@ class FormattingCompletionEngine(BaseCompletionEngine):
if tables: if tables:
columns = self.provider.list_columns(self.table_name) columns = self.provider.list_columns(self.table_name)
return [Suggestion(col, "Column", "column") for col in columns] return [Suggestion(col, "Column", "column") for col in columns]
except Exception: except Exception as ex:
pass logger.error(f"Error getting column suggestions: {ex}")
return [] return []
def _get_column_suggestions_with_closing_quote(self) -> list[Suggestion]: def _get_column_suggestions_with_closing_quote(self) -> list[Suggestion]:

View File

@@ -36,7 +36,6 @@ class DatagridMetadataProvider(SingleInstance, BaseMetadataProvider):
def __init__(self, parent=None, session: Optional[dict] = None, def __init__(self, parent=None, session: Optional[dict] = None,
_id: Optional[str] = None): _id: Optional[str] = None):
super().__init__(parent, session, _id) super().__init__(parent, session, _id)
with self.initializing():
self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy() self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy()
self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy() self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy()
self.all_tables_formats: list = [] self.all_tables_formats: list = []

View File

@@ -1,5 +1,10 @@
import pytest import pytest
from pandas import DataFrame
from myfasthtml.controls.DataGrid import DataGrid, DatagridConf
from myfasthtml.controls.DataGridsManager import DataGridsManager
from myfasthtml.controls.TabsManager import TabsManager
from myfasthtml.core.data.DataServicesManager import DataServicesManager
from myfasthtml.core.instances import SingleInstance, InstancesManager from myfasthtml.core.instances import SingleInstance, InstancesManager
@@ -27,3 +32,27 @@ def session():
def root_instance(session): def root_instance(session):
InstancesManager.reset() InstancesManager.reset()
return RootInstanceForTests(session=session) return RootInstanceForTests(session=session)
@pytest.fixture
def datagrids_manager(root_instance):
return DataGridsManager(root_instance)
@pytest.fixture
def dataservices_manager(root_instance):
return InstancesManager.get_by_type(root_instance._session, DataServicesManager)
def get_data_grid(root_instance, df: DataFrame, table_name: str = "test.grid1", save_state: bool = False):
TabsManager(root_instance) # just define it
dgm = DataGridsManager(root_instance)
dsm = InstancesManager.get_by_type(root_instance._session, DataServicesManager)
data_service = dsm.create_service(table_name, save_state=save_state)
data_service.load_dataframe(df)
grid_id = DataGrid.get_grid_id_from_data_service_id(data_service.get_id())
namespace, table_name = table_name.split(".")
conf = DatagridConf(namespace=namespace, name=table_name)
return DataGrid(dgm, conf=conf, save_state=save_state, _id=grid_id)

View File

@@ -3,6 +3,7 @@ import pytest
from fastcore.basics import NotStr from fastcore.basics import NotStr
from fasthtml.components import Div, Script from fasthtml.components import Div, Script
from controls.conftest import get_data_grid
from myfasthtml.controls.DataGrid import DataGrid, DatagridConf from myfasthtml.controls.DataGrid import DataGrid, DatagridConf
from myfasthtml.controls.DataGridsManager import DataGridsManager from myfasthtml.controls.DataGridsManager import DataGridsManager
from myfasthtml.controls.TabsManager import TabsManager from myfasthtml.controls.TabsManager import TabsManager
@@ -358,6 +359,31 @@ class TestDataGridBehaviour:
dg.change_selection_mode() dg.change_selection_mode()
assert dg._state.selection.selection_mode == "cell" assert dg._state.selection.selection_mode == "cell"
def test_i_can_handle_column_update(self, root_instance):
df = pd.DataFrame({
"name": ["Alice", "Bob", "Charlie"],
"age": [25, 30, 35],
"active": [True, False, True],
})
dg = get_data_grid(root_instance, df, save_state=True)
dg.handle_columns_updates({"name": {"title": "Name",
"width": 150,
"formula": "{age} + 1",
"type": ColumnType.Number,
"visible": False}})
# check the ui_state
col_ui_state = dg._state.columns[0]
assert col_ui_state.width == 150
assert col_ui_state.visible is False
# check the colum def
col_def = dg._data_service._state.columns[0]
assert col_def.title == "Name"
assert col_def.type == ColumnType.Number
assert col_def.formula == "{age} + 1"
class TestDataGridRender: class TestDataGridRender:

View File

@@ -1,423 +0,0 @@
import shutil
from dataclasses import dataclass, field
import pytest
from fasthtml.common import Div, FT, Input, Form, Fieldset, Select
from myfasthtml.controls.DataGridColumnsManager import DataGridColumnsManager
from myfasthtml.controls.Search import Search
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridColumnUiState
from myfasthtml.core.constants import ColumnType
from myfasthtml.core.data.ColumnDefinition import ColumnDefinition
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
def get_table_name(self):
return "mock_table"
def get_formula_engine(self):
return None
# col_def: ColumnDefinition, col_ui_state: DataGridColumnUiState
@pytest.fixture
def mock_datagrid(root_instance):
"""Create a mock DataGrid with sample columns."""
columns = [
DataGridColumnState(ColumnDefinition(col_id="name", col_index=0, title="Name", type=ColumnType.Text),
DataGridColumnUiState(col_id="name", visible=True)),
DataGridColumnState(ColumnDefinition(col_id="age", col_index=1, title="Age", type=ColumnType.Number),
DataGridColumnUiState(col_id="age", visible=True)),
DataGridColumnState(ColumnDefinition(col_id="email", col_index=2, title="Email", type=ColumnType.Text),
DataGridColumnUiState(col_id="email", 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."""
# =========================================================================
# 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
def test_i_can_get_and_update_column_state(self, columns_manager):
"""Test that get_col_def_from_col_id updates the column state."""
updates = {"title": "New Name", "visible": "on", "type": "Number", "width": 200}
col_def = columns_manager._get_updated_col_def_from_col_id("name", updates)
assert col_def.title == "New Name"
assert col_def.visible is True
assert col_def.type == ColumnType.Number
assert col_def.width == 200
def test_i_can_get_and_update_column_state_visible_false(self, columns_manager):
"""Test that get_col_def_from_col_id updates the column state."""
updates = {} # visible is missing in the update => It must be set to False
col_def = columns_manager._get_updated_col_def_from_col_id("name", updates)
assert col_def.visible is False
# =========================================================================
# 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)
col_def = columns_manager._get_col_def_from_col_id(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 an HTML element."""
result = columns_manager.toggle_column("name")
assert isinstance(result, FT)
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 All Columns
# =========================================================================
def test_show_all_columns_returns_search_component(self, columns_manager):
"""Test that mk_all_columns returns a Search component."""
result = columns_manager.mk_all_columns()
assert isinstance(result, Search)
def test_show_all_columns_returns_configured_search(self, columns_manager):
"""Test that mk_all_columns returns a correctly configured Search component."""
result = columns_manager.mk_all_columns()
assert result.items_names == "Columns"
assert len(result.items) == 3
col_def = result.items[0]
assert result.get_attr(col_def) == col_def.col_id
# =========================================================================
# Update Column
# =========================================================================
def test_i_can_update_column_title(self, columns_manager):
"""Test updating a column's title via client_response."""
columns_manager.save_column_details("name", {"title": "New Name"})
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"
columns_manager.save_column_details("name", {"visible": "off"}) # Not "on" means unchecked
col_def = columns_manager._get_col_def_from_col_id("name")
assert col_def.visible is False
# Check it back on
columns_manager.save_column_details("name", {"visible": "on"})
col_def = columns_manager._get_col_def_from_col_id("name")
assert col_def.visible is True
def test_i_can_update_column_type(self, columns_manager):
"""Test updating a column's type."""
columns_manager.save_column_details("name", {"type": "Number"})
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."""
columns_manager.save_column_details("name", {"width": "200"})
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 save_column_details calls save_state on parent."""
mock_datagrid._save_state_called = False
columns_manager.save_column_details("name", {"title": "Updated"})
assert mock_datagrid._save_state_called is True
def test_update_column_ignores_unknown_attributes(self, columns_manager):
"""Test that save_column_details ignores attributes not in DataGridColumnState."""
columns_manager.save_column_details("name", {"unknown_attr": "value", "title": "New Title"})
col_def = columns_manager._get_col_def_from_col_id("name")
assert col_def.title == "New Title"
assert not hasattr(col_def, "unknown_attr")
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 (column list)
Div(), # New column button
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
# =========================================================================
# show_column_details
# =========================================================================
def test_i_can_show_column_details_for_existing_column(self, columns_manager):
"""Test that show_column_details returns a form-based view for an existing column.
Why these elements matter:
- Form element: show_column_details must return an editable form view
- Exactly one form: Ensures the response is unambiguous (not multiple forms)
"""
result = columns_manager.show_column_details("name")
expected = Form()
del expected.attrs["enctype"]
forms = find(result, expected)
assert len(forms) == 1, "Should contain exactly one form"
# =========================================================================
# mk_all_columns
# =========================================================================
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"

View File

@@ -5,44 +5,39 @@ Tests the complete formatting flow: DSL → Storage → Application.
""" """
import pytest import pytest
from pandas import DataFrame
from myfasthtml.controls.DataGrid import DataGrid from controls.conftest import get_data_grid
from myfasthtml.controls.DataGridFormattingEditor import DataGridFormattingEditor from myfasthtml.controls.DataGridFormattingEditor import DataGridFormattingEditor
from myfasthtml.controls.DataGridsManager import DataGridsManager from myfasthtml.controls.datagrid_objects import DataGridRowUiState
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridColumnUiState from myfasthtml.core.data.DataServicesManager import DataServicesManager
from myfasthtml.core.constants import ColumnType
from myfasthtml.core.data.ColumnDefinition import ColumnDefinition
from myfasthtml.core.formatting.dataclasses import FormatRule, Style from myfasthtml.core.formatting.dataclasses import FormatRule, Style
from myfasthtml.core.formatting.dsl.definition import FormattingDSL from myfasthtml.core.formatting.dsl.definition import FormattingDSL
from myfasthtml.core.instances import InstancesManager from myfasthtml.core.instances import InstancesManager
@pytest.fixture @pytest.fixture
def manager(root_instance): def datagrid(root_instance):
"""Create a DataGridsManager instance.""" """Create a DataGrid instance."""
mgr = DataGridsManager(root_instance, _id="test-manager") df = DataFrame(
yield mgr {"amount": [100, 200, 300],
InstancesManager.reset() "status": ["active", "inactive", "active"]}
)
dg = get_data_grid(root_instance, df, table_name="app.products")
dg.handle_columns_updates({"amount": {"title": "Amount"},
"status": {"title": "Status"}})
yield dg
dsm = InstancesManager.get_by_type(root_instance._session, DataServicesManager)
dsm.clear()
dgm = dg.get_parent()
dgm.get_state().all_tables_formats.clear()
@pytest.fixture @pytest.fixture
def datagrid(manager): def datagrids_manager(datagrid):
"""Create a DataGrid instance.""" """Create a DataGridsManager instance."""
from myfasthtml.controls.DataGrid import DatagridConf return datagrid.get_parent()
conf = DatagridConf(namespace="app", name="products")
grid = DataGrid(manager, conf=conf, save_state=False, _id="mf-data_grid-test-datagrid")
# ColumnDefinition, col_ui_state: DataGridColumnUiState
# Add some columns
grid.columns = [
DataGridColumnState(ColumnDefinition(col_id="amount", col_index=0, title="Amount", type=ColumnType.Number),
DataGridColumnUiState(col_id="amount", visible=True)),
DataGridColumnState(ColumnDefinition(col_id="status", col_index=1, title="Status", type=ColumnType.Text),
DataGridColumnUiState(col_id="status", visible=True))
]
yield grid
InstancesManager.reset()
@pytest.fixture @pytest.fixture
@@ -65,7 +60,7 @@ class TestFormatRulesHierarchy:
column_rules = [FormatRule(style=Style(preset="success"))] column_rules = [FormatRule(style=Style(preset="success"))]
table_rules = [FormatRule(style=Style(preset="info"))] table_rules = [FormatRule(style=Style(preset="info"))]
datagrid._state.cell_formats["tcell_test-datagrid-0-0"] = cell_rules datagrid._state.cell_formats[f"tcell_{datagrid.get_id()}-0-0"] = cell_rules
datagrid._state.columns[0].format = column_rules datagrid._state.columns[0].format = column_rules
datagrid._state.table_format = table_rules datagrid._state.table_format = table_rules
@@ -82,7 +77,8 @@ class TestFormatRulesHierarchy:
column_rules = [FormatRule(style=Style(preset="success"))] column_rules = [FormatRule(style=Style(preset="success"))]
table_rules = [FormatRule(style=Style(preset="info"))] table_rules = [FormatRule(style=Style(preset="info"))]
datagrid._state.rows[0].format = row_rules datagrid._state.rows.append(DataGridRowUiState(1, format=table_rules)) # this one should be skipped
datagrid._state.rows.append(DataGridRowUiState(0, format=row_rules))
datagrid._state.columns[0].format = column_rules datagrid._state.columns[0].format = column_rules
datagrid._state.table_format = table_rules datagrid._state.table_format = table_rules
@@ -107,14 +103,14 @@ class TestFormatRulesHierarchy:
# Should return column rules # Should return column rules
assert rules == column_rules assert rules == column_rules
def test_i_can_get_table_level_rules(self, datagrid, manager): def test_i_can_get_table_level_rules(self, datagrid, datagrids_manager):
"""Test that table-level rules have fourth priority.""" """Test that table-level rules have fourth priority."""
# Setup rules at different levels # Setup rules at different levels
table_rules = [FormatRule(style=Style(preset="info"))] table_rules = [FormatRule(style=Style(preset="info"))]
tables_rules = [FormatRule(style=Style(preset="neutral"))] tables_rules = [FormatRule(style=Style(preset="neutral"))]
datagrid._state.table_format = table_rules datagrid._state.table_format = table_rules
manager.all_tables_formats = tables_rules datagrids_manager.get_state().all_tables_formats = tables_rules
# Get rules for cell (no higher level rules) # Get rules for cell (no higher level rules)
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0]) rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
@@ -122,11 +118,11 @@ class TestFormatRulesHierarchy:
# Should return table rules # Should return table rules
assert rules == table_rules assert rules == table_rules
def test_i_can_get_tables_level_rules(self, datagrid, manager): def test_i_can_get_tables_level_rules(self, datagrid, datagrids_manager):
"""Test that tables-level rules have lowest priority.""" """Test that tables-level rules have lowest priority."""
# Setup global rules # Setup global rules
tables_rules = [FormatRule(style=Style(preset="neutral"))] tables_rules = [FormatRule(style=Style(preset="neutral"))]
manager.all_tables_formats = tables_rules datagrids_manager.get_state().all_tables_formats = tables_rules
# Get rules for cell (no other rules) # Get rules for cell (no other rules)
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0]) rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
@@ -139,25 +135,6 @@ class TestFormatRulesHierarchy:
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0]) rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
assert rules == [] assert rules == []
@pytest.mark.parametrize("level,setup_func,expected_preset", [
("cell", lambda dg: dg._state.cell_formats.__setitem__("tcell_test-datagrid-0-0",
[FormatRule(style=Style(preset="error"))]), "error"),
("row", lambda dg: setattr(dg._state.rows[0], "format",
[FormatRule(style=Style(preset="warning"))]), "warning"),
("column", lambda dg: setattr(dg._state.columns[0], "format",
[FormatRule(style=Style(preset="success"))]), "success"),
("table", lambda dg: setattr(dg._state, "table_format",
[FormatRule(style=Style(preset="info"))]), "info"),
])
def test_hierarchy_priority(self, datagrid, level, setup_func, expected_preset):
"""Test that each level has correct priority in the hierarchy."""
setup_func(datagrid)
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
assert rules is not None
assert len(rules) == 1
assert rules[0].style.preset == expected_preset
# ============================================================================= # =============================================================================
# DataGridFormattingEditor Integration Tests # DataGridFormattingEditor Integration Tests
@@ -194,7 +171,7 @@ table "wrong_name":
# Rules should not be applied (wrong table name) # Rules should not be applied (wrong table name)
assert len(datagrid._state.table_format) == 0 assert len(datagrid._state.table_format) == 0
def test_i_can_dispatch_tables_rules(self, manager, datagrid, editor): def test_i_can_dispatch_tables_rules(self, datagrids_manager, datagrid, editor):
"""Test that tables rules are dispatched to DataGridsManager.""" """Test that tables rules are dispatched to DataGridsManager."""
dsl = ''' dsl = '''
@@ -206,11 +183,11 @@ tables:
editor.on_content_changed() editor.on_content_changed()
# Check that manager.all_tables_formats is populated # Check that manager.all_tables_formats is populated
assert len(manager.all_tables_formats) == 2 assert len(datagrids_manager.get_state().all_tables_formats) == 2
assert manager.all_tables_formats[0].style.preset == "neutral" assert datagrids_manager.get_state().all_tables_formats[0].style.preset == "neutral"
assert manager.all_tables_formats[1].formatter.precision == 2 assert datagrids_manager.get_state().all_tables_formats[1].formatter.precision == 2
def test_i_can_combine_all_scope_types(self, manager, datagrid, editor): def test_i_can_combine_all_scope_types(self, datagrids_manager, datagrid, editor):
"""Test that all 5 scope types can be used together.""" """Test that all 5 scope types can be used together."""
dsl = ''' dsl = '''
@@ -233,7 +210,7 @@ cell (amount, 1):
editor.on_content_changed() editor.on_content_changed()
# Check all levels are populated # Check all levels are populated
assert len(manager.all_tables_formats) == 1 assert len(datagrids_manager.get_state().all_tables_formats) == 1
assert len(datagrid._state.table_format) == 1 assert len(datagrid._state.table_format) == 1
assert len(datagrid._state.columns[0].format) == 1 assert len(datagrid._state.columns[0].format) == 1
assert len(datagrid._state.rows[0].format) == 1 assert len(datagrid._state.rows[0].format) == 1

View File

@@ -1,4 +1,5 @@
from myfasthtml.controls.IconsHelper import IconsHelper from myfasthtml.controls.IconsHelper import IconsHelper
from myfasthtml.test.matcher import matches, TestIconNotStr
def test_existing_icon(): def test_existing_icon():
@@ -13,8 +14,7 @@ def test_dynamic_icon():
def test_unknown_icon(): def test_unknown_icon():
IconsHelper.reset() IconsHelper.reset()
assert IconsHelper.get("does_not_exist") is None assert matches(IconsHelper.get("does_not_exist"), TestIconNotStr("Question20Regular"))
def test_dynamic_icon_by_package(): def test_dynamic_icon_by_package():