Added "table" and "tables" in the DSL
This commit is contained in:
279
tests/controls/test_datagrid_formatting.py
Normal file
279
tests/controls/test_datagrid_formatting.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
Tests for DataGrid formatting integration with table/tables scopes.
|
||||
|
||||
Tests the complete formatting flow: DSL → Storage → Application.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from myfasthtml.controls.DataGrid import DataGrid
|
||||
from myfasthtml.controls.DataGridFormattingEditor import DataGridFormattingEditor
|
||||
from myfasthtml.controls.DataGridsManager import DataGridsManager
|
||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState
|
||||
from myfasthtml.core.constants import ColumnType
|
||||
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()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def datagrid(manager):
|
||||
"""Create a DataGrid instance."""
|
||||
from myfasthtml.controls.DataGrid import DatagridConf
|
||||
conf = DatagridConf(namespace="app", name="products", id="test-grid")
|
||||
grid = DataGrid(manager, conf=conf, save_state=False, _id="test-datagrid")
|
||||
|
||||
# Add some columns
|
||||
grid._state.columns = [
|
||||
DataGridColumnState(col_id="amount", col_index=0, title="Amount", type=ColumnType.Number, visible=True),
|
||||
DataGridColumnState(col_id="status", col_index=1, title="Status", type=ColumnType.Text, visible=True),
|
||||
]
|
||||
|
||||
# Add some rows
|
||||
grid._state.rows = [
|
||||
DataGridRowState(0),
|
||||
DataGridRowState(1),
|
||||
]
|
||||
|
||||
yield grid
|
||||
InstancesManager.reset()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def editor(datagrid):
|
||||
return DataGridFormattingEditor(datagrid, FormattingDSL())
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# _get_format_rules() Hierarchy Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestFormatRulesHierarchy:
|
||||
"""Tests for format rules hierarchy (cell > row > column > table > tables)."""
|
||||
|
||||
def test_i_can_get_cell_level_rules(self, datagrid):
|
||||
"""Test that cell-level rules have highest priority."""
|
||||
# Setup rules at different levels
|
||||
cell_rules = [FormatRule(style=Style(preset="error"))]
|
||||
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.columns[0].format = column_rules
|
||||
datagrid._state.table_format = table_rules
|
||||
|
||||
# Get rules for cell (0, 0)
|
||||
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||
|
||||
# Should return cell rules (highest priority)
|
||||
assert rules == cell_rules
|
||||
|
||||
def test_i_can_get_row_level_rules(self, datagrid):
|
||||
"""Test that row-level rules have second priority."""
|
||||
# Setup rules at different levels
|
||||
row_rules = [FormatRule(style=Style(preset="warning"))]
|
||||
column_rules = [FormatRule(style=Style(preset="success"))]
|
||||
table_rules = [FormatRule(style=Style(preset="info"))]
|
||||
|
||||
datagrid._state.rows[0].format = row_rules
|
||||
datagrid._state.columns[0].format = column_rules
|
||||
datagrid._state.table_format = table_rules
|
||||
|
||||
# Get rules for row 0 (no cell-level rules)
|
||||
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||
|
||||
# Should return row rules
|
||||
assert rules == row_rules
|
||||
|
||||
def test_i_can_get_column_level_rules(self, datagrid):
|
||||
"""Test that column-level rules have third priority."""
|
||||
# Setup rules at different levels
|
||||
column_rules = [FormatRule(style=Style(preset="success"))]
|
||||
table_rules = [FormatRule(style=Style(preset="info"))]
|
||||
|
||||
datagrid._state.columns[0].format = column_rules
|
||||
datagrid._state.table_format = table_rules
|
||||
|
||||
# Get rules for column (no cell or row rules)
|
||||
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||
|
||||
# Should return column rules
|
||||
assert rules == column_rules
|
||||
|
||||
def test_i_can_get_table_level_rules(self, datagrid, 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
|
||||
|
||||
# Get rules for cell (no higher level rules)
|
||||
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||
|
||||
# Should return table rules
|
||||
assert rules == table_rules
|
||||
|
||||
def test_i_can_get_tables_level_rules(self, datagrid, 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
|
||||
|
||||
# Get rules for cell (no other rules)
|
||||
rules = datagrid._get_format_rules(0, 0, datagrid._state.columns[0])
|
||||
|
||||
# Should return global tables rules
|
||||
assert rules == tables_rules
|
||||
|
||||
def test_i_get_none_when_no_rules(self, datagrid):
|
||||
"""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
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DataGridFormattingEditor Integration Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestFormattingEditorIntegration:
|
||||
"""Tests for DataGridFormattingEditor with table/tables scopes."""
|
||||
|
||||
def test_i_can_dispatch_table_rules(self, datagrid, editor):
|
||||
"""Test that table rules are dispatched to DatagridState.table_format."""
|
||||
|
||||
dsl = '''
|
||||
table "products":
|
||||
style("info")
|
||||
'''
|
||||
editor.set_content(dsl)
|
||||
editor.on_content_changed()
|
||||
|
||||
# Check that table_format is populated
|
||||
assert len(datagrid._state.table_format) == 1
|
||||
assert datagrid._state.table_format[0].style.preset == "info"
|
||||
|
||||
def test_i_cannot_use_wrong_table_name(self, datagrid, editor):
|
||||
"""Test that wrong table name is rejected."""
|
||||
|
||||
dsl = '''
|
||||
table "wrong_name":
|
||||
style("error")
|
||||
'''
|
||||
editor.set_content(dsl)
|
||||
editor.on_content_changed()
|
||||
|
||||
# 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):
|
||||
"""Test that tables rules are dispatched to DataGridsManager."""
|
||||
|
||||
dsl = '''
|
||||
tables:
|
||||
style("neutral")
|
||||
format.number(precision=2)
|
||||
'''
|
||||
editor.set_content(dsl)
|
||||
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
|
||||
|
||||
def test_i_can_combine_all_scope_types(self, manager, datagrid, editor):
|
||||
"""Test that all 5 scope types can be used together."""
|
||||
|
||||
dsl = '''
|
||||
tables:
|
||||
style(font_size="14px")
|
||||
|
||||
table "products":
|
||||
format.number(precision=2)
|
||||
|
||||
column amount:
|
||||
style("success") if value > 0
|
||||
|
||||
row 0:
|
||||
style("neutral", bold=True)
|
||||
|
||||
cell (amount, 1):
|
||||
style("error")
|
||||
'''
|
||||
editor.set_content(dsl)
|
||||
editor.on_content_changed()
|
||||
|
||||
# Check all levels are populated
|
||||
assert len(manager.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
|
||||
assert len(datagrid._state.cell_formats) == 1
|
||||
|
||||
def test_i_can_clear_table_format(self, datagrid, editor):
|
||||
"""Test that table_format is cleared when DSL changes."""
|
||||
# First set table rules
|
||||
dsl = '''
|
||||
table "products":
|
||||
style("info")
|
||||
'''
|
||||
editor.set_content(dsl)
|
||||
editor.on_content_changed()
|
||||
assert len(datagrid._state.table_format) == 1
|
||||
|
||||
# Then remove them
|
||||
editor.set_content("")
|
||||
editor.on_content_changed()
|
||||
assert len(datagrid._state.table_format) == 0
|
||||
|
||||
@pytest.mark.parametrize("table_name,should_apply", [
|
||||
("products", True), # Correct name
|
||||
("PRODUCTS", False), # Case sensitive
|
||||
("product", False), # Partial match not allowed
|
||||
("app.products", False), # Namespace not included
|
||||
("other", False), # Completely wrong
|
||||
])
|
||||
def test_table_name_validation(self, datagrid, editor, table_name, should_apply):
|
||||
"""Test that table name validation is case-sensitive and exact."""
|
||||
|
||||
dsl = f'''
|
||||
table "{table_name}":
|
||||
style("info")
|
||||
'''
|
||||
editor.set_content(dsl)
|
||||
editor.on_content_changed()
|
||||
|
||||
if should_apply:
|
||||
assert len(datagrid._state.table_format) == 1
|
||||
else:
|
||||
assert len(datagrid._state.table_format) == 0
|
||||
@@ -142,6 +142,49 @@ def test_i_can_detect_scope_with_multiple_declarations():
|
||||
assert scope.column_name == "amount"
|
||||
|
||||
|
||||
def test_i_can_detect_table_scope():
|
||||
"""Test detection of table scope."""
|
||||
text = 'table "products":\n style()'
|
||||
scope = detect_scope(text, current_line=1)
|
||||
|
||||
assert scope.scope_type == "table"
|
||||
assert scope.table_name == "products"
|
||||
|
||||
|
||||
def test_i_can_detect_table_scope_with_spaces():
|
||||
"""Test detection of table scope with spaces in name."""
|
||||
text = 'table "financial report":\n format()'
|
||||
scope = detect_scope(text, current_line=1)
|
||||
|
||||
assert scope.scope_type == "table"
|
||||
assert scope.table_name == "financial report"
|
||||
|
||||
|
||||
def test_i_can_detect_tables_scope():
|
||||
"""Test detection of global tables scope."""
|
||||
text = "tables:\n style()"
|
||||
scope = detect_scope(text, current_line=1)
|
||||
|
||||
assert scope.scope_type == "tables"
|
||||
assert scope.table_name is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scope_def,expected_type,expected_attrs", [
|
||||
('column amount:\n style()', "column", {"column_name": "amount"}),
|
||||
('row 5:\n style()', "row", {"row_index": 5}),
|
||||
('cell (amount, 3):\n style()', "cell", {"column_name": "amount", "row_index": 3}),
|
||||
('table "products":\n style()', "table", {"table_name": "products"}),
|
||||
('tables:\n style()', "tables", {}),
|
||||
])
|
||||
def test_i_can_detect_all_scope_types(scope_def, expected_type, expected_attrs):
|
||||
"""Test detection of all 5 scope types."""
|
||||
scope = detect_scope(scope_def, current_line=1)
|
||||
|
||||
assert scope.scope_type == expected_type
|
||||
for attr, value in expected_attrs.items():
|
||||
assert getattr(scope, attr) == value
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Context Detection - Scope Contexts
|
||||
# =============================================================================
|
||||
@@ -227,6 +270,42 @@ def test_context_cell_row_after_comma_quoted():
|
||||
assert context == Context.CELL_ROW
|
||||
|
||||
|
||||
def test_context_table_name_after_table():
|
||||
"""Test TABLE_NAME context after 'table '."""
|
||||
text = "table "
|
||||
cursor = Position(line=0, ch=6)
|
||||
scope = DetectedScope()
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.TABLE_NAME
|
||||
|
||||
|
||||
def test_context_tables_scope_after_tables():
|
||||
"""Test TABLES_SCOPE context after 'tables'."""
|
||||
text = "tables"
|
||||
cursor = Position(line=0, ch=6)
|
||||
scope = DetectedScope()
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.TABLES_SCOPE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text,cursor_ch,expected_context", [
|
||||
("column ", 7, Context.COLUMN_NAME),
|
||||
("row ", 4, Context.ROW_INDEX),
|
||||
("cell ", 5, Context.CELL_START),
|
||||
("table ", 6, Context.TABLE_NAME),
|
||||
("tables", 6, Context.TABLES_SCOPE),
|
||||
])
|
||||
def test_i_can_detect_all_scope_contexts(text, cursor_ch, expected_context):
|
||||
"""Test detection of all scope-related contexts."""
|
||||
cursor = Position(line=0, ch=cursor_ch)
|
||||
scope = DetectedScope()
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == expected_context
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Context Detection - Rule Contexts
|
||||
# =============================================================================
|
||||
@@ -553,6 +632,32 @@ def test_suggestions_scope_keyword(provider):
|
||||
assert "column" in labels
|
||||
assert "row" in labels
|
||||
assert "cell" in labels
|
||||
assert "table" in labels
|
||||
assert "tables" in labels
|
||||
|
||||
|
||||
def test_suggestions_table_name(provider):
|
||||
"""Test suggestions for TABLE_NAME context."""
|
||||
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||
scope = DetectedScope()
|
||||
|
||||
suggestions = engine.get_suggestions(Context.TABLE_NAME, scope, "")
|
||||
labels = [s.label for s in suggestions]
|
||||
|
||||
# Should suggest the current table name in quotes
|
||||
assert '"app.orders"' in labels
|
||||
|
||||
|
||||
def test_suggestions_tables_scope(provider):
|
||||
"""Test suggestions for TABLES_SCOPE context."""
|
||||
engine = FormattingCompletionEngine(provider, "app.orders")
|
||||
scope = DetectedScope()
|
||||
|
||||
suggestions = engine.get_suggestions(Context.TABLES_SCOPE, scope, "")
|
||||
labels = [s.label for s in suggestions]
|
||||
|
||||
# Should suggest colon to complete the scope
|
||||
assert ":" in labels
|
||||
|
||||
|
||||
def test_suggestions_style_preset(provider):
|
||||
|
||||
@@ -17,6 +17,8 @@ from myfasthtml.core.formatting.dsl import (
|
||||
ColumnScope,
|
||||
RowScope,
|
||||
CellScope,
|
||||
TableScope,
|
||||
TablesScope,
|
||||
DSLSyntaxError,
|
||||
)
|
||||
|
||||
@@ -125,6 +127,62 @@ cell tcell_grid1-3-2:
|
||||
assert rules[0].scope.row is None
|
||||
|
||||
|
||||
class TestTableScope:
|
||||
"""Tests for table scope parsing."""
|
||||
|
||||
def test_i_can_parse_table_scope(self):
|
||||
"""Test parsing a table scope."""
|
||||
dsl = """
|
||||
table "products":
|
||||
style("neutral")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, TableScope)
|
||||
assert rules[0].scope.table == "products"
|
||||
|
||||
def test_i_can_parse_table_scope_with_spaces(self):
|
||||
"""Test parsing a table scope with spaces in name."""
|
||||
dsl = """
|
||||
table "financial report":
|
||||
style("info")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, TableScope)
|
||||
assert rules[0].scope.table == "financial report"
|
||||
|
||||
|
||||
class TestTablesScope:
|
||||
"""Tests for tables scope (global) parsing."""
|
||||
|
||||
def test_i_can_parse_tables_scope(self):
|
||||
"""Test parsing the global tables scope."""
|
||||
dsl = """
|
||||
tables:
|
||||
style("neutral")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, TablesScope)
|
||||
|
||||
def test_i_can_parse_tables_scope_with_multiple_rules(self):
|
||||
"""Test parsing tables scope with multiple rules."""
|
||||
dsl = """
|
||||
tables:
|
||||
style("neutral")
|
||||
format.number(precision=2)
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 2
|
||||
assert isinstance(rules[0].scope, TablesScope)
|
||||
assert isinstance(rules[1].scope, TablesScope)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Style Tests
|
||||
# =============================================================================
|
||||
@@ -496,7 +554,49 @@ row 0:
|
||||
# Third rule: row 0
|
||||
assert isinstance(rules[2].scope, RowScope)
|
||||
assert rules[2].scope.row == 0
|
||||
|
||||
|
||||
def test_i_can_parse_all_scope_types(self):
|
||||
"""Test parsing all 5 scope types together."""
|
||||
dsl = """
|
||||
tables:
|
||||
style(font_size="14px")
|
||||
|
||||
table "products":
|
||||
format.number(precision=2)
|
||||
|
||||
column amount:
|
||||
format("EUR")
|
||||
|
||||
row 0:
|
||||
style("neutral", bold=True)
|
||||
|
||||
cell (amount, 5):
|
||||
style("highlight")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 5
|
||||
|
||||
# First rule: tables (global)
|
||||
assert isinstance(rules[0].scope, TablesScope)
|
||||
|
||||
# Second rule: table "products"
|
||||
assert isinstance(rules[1].scope, TableScope)
|
||||
assert rules[1].scope.table == "products"
|
||||
|
||||
# Third rule: column amount
|
||||
assert isinstance(rules[2].scope, ColumnScope)
|
||||
assert rules[2].scope.column == "amount"
|
||||
|
||||
# Fourth rule: row 0
|
||||
assert isinstance(rules[3].scope, RowScope)
|
||||
assert rules[3].scope.row == 0
|
||||
|
||||
# Fifth rule: cell (amount, 5)
|
||||
assert isinstance(rules[4].scope, CellScope)
|
||||
assert rules[4].scope.column == "amount"
|
||||
assert rules[4].scope.row == 5
|
||||
|
||||
def test_i_can_parse_style_and_format_combined(self):
|
||||
"""Test parsing style and format on same line."""
|
||||
dsl = """
|
||||
|
||||
Reference in New Issue
Block a user