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

@@ -1,5 +1,10 @@
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
@@ -27,3 +32,27 @@ def session():
def root_instance(session):
InstancesManager.reset()
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 fasthtml.components import Div, Script
from controls.conftest import get_data_grid
from myfasthtml.controls.DataGrid import DataGrid, DatagridConf
from myfasthtml.controls.DataGridsManager import DataGridsManager
from myfasthtml.controls.TabsManager import TabsManager
@@ -357,6 +358,31 @@ class TestDataGridBehaviour:
dg._selection_mode_selector.cycle_state()
dg.change_selection_mode()
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:

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
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.DataGridsManager import DataGridsManager
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridColumnUiState
from myfasthtml.core.constants import ColumnType
from myfasthtml.core.data.ColumnDefinition import ColumnDefinition
from myfasthtml.controls.datagrid_objects import DataGridRowUiState
from myfasthtml.core.data.DataServicesManager import DataServicesManager
from myfasthtml.core.formatting.dataclasses import FormatRule, Style
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
from myfasthtml.core.instances import InstancesManager
@pytest.fixture
def manager(root_instance):
"""Create a DataGridsManager instance."""
mgr = DataGridsManager(root_instance, _id="test-manager")
yield mgr
InstancesManager.reset()
def datagrid(root_instance):
"""Create a DataGrid instance."""
df = DataFrame(
{"amount": [100, 200, 300],
"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
def datagrid(manager):
"""Create a DataGrid instance."""
from myfasthtml.controls.DataGrid import DatagridConf
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()
def datagrids_manager(datagrid):
"""Create a DataGridsManager instance."""
return datagrid.get_parent()
@pytest.fixture
@@ -65,7 +60,7 @@ class TestFormatRulesHierarchy:
column_rules = [FormatRule(style=Style(preset="success"))]
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.table_format = table_rules
@@ -82,7 +77,8 @@ class TestFormatRulesHierarchy:
column_rules = [FormatRule(style=Style(preset="success"))]
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.table_format = table_rules
@@ -107,14 +103,14 @@ class TestFormatRulesHierarchy:
# Should return 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."""
# Setup rules at different levels
table_rules = [FormatRule(style=Style(preset="info"))]
tables_rules = [FormatRule(style=Style(preset="neutral"))]
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)
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
@@ -122,11 +118,11 @@ class TestFormatRulesHierarchy:
# Should return 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."""
# Setup global rules
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)
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
@@ -138,25 +134,6 @@ class TestFormatRulesHierarchy:
"""Test that None is returned when no rules are defined."""
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
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
# =============================================================================
@@ -194,7 +171,7 @@ table "wrong_name":
# Rules should not be applied (wrong table name)
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."""
dsl = '''
@@ -206,11 +183,11 @@ tables:
editor.on_content_changed()
# Check that manager.all_tables_formats is populated
assert len(manager.all_tables_formats) == 2
assert manager.all_tables_formats[0].style.preset == "neutral"
assert manager.all_tables_formats[1].formatter.precision == 2
assert len(datagrids_manager.get_state().all_tables_formats) == 2
assert datagrids_manager.get_state().all_tables_formats[0].style.preset == "neutral"
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."""
dsl = '''
@@ -233,7 +210,7 @@ cell (amount, 1):
editor.on_content_changed()
# 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.columns[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.test.matcher import matches, TestIconNotStr
def test_existing_icon():
@@ -13,8 +14,7 @@ def test_dynamic_icon():
def test_unknown_icon():
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():