Working on Formating DSL completion
This commit is contained in:
0
tests/core/formatting/dsl/__init__.py
Normal file
0
tests/core/formatting/dsl/__init__.py
Normal file
770
tests/core/formatting/dsl/test_completion.py
Normal file
770
tests/core/formatting/dsl/test_completion.py
Normal file
@@ -0,0 +1,770 @@
|
||||
"""
|
||||
Tests for formatting DSL autocompletion.
|
||||
|
||||
Tests for scope detection, context detection, suggestions, and engine integration.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from typing import Any
|
||||
|
||||
from myfasthtml.core.dsl.types import Position, Suggestion, CompletionResult
|
||||
from myfasthtml.core.formatting.dsl.completion.contexts import (
|
||||
Context,
|
||||
DetectedScope,
|
||||
detect_scope,
|
||||
detect_context,
|
||||
)
|
||||
from myfasthtml.core.formatting.dsl.completion.suggestions import get_suggestions
|
||||
from myfasthtml.core.formatting.dsl.completion.engine import (
|
||||
FormattingCompletionEngine,
|
||||
get_completions,
|
||||
)
|
||||
from myfasthtml.core.formatting.dsl.completion import presets
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Mock Provider Fixture
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class MockProvider:
|
||||
"""
|
||||
Mock metadata provider for testing.
|
||||
|
||||
Provides predefined data for columns, values, and presets.
|
||||
"""
|
||||
|
||||
def get_tables(self) -> list[str]:
|
||||
return ["app.orders"]
|
||||
|
||||
def get_columns(self, table: str) -> list[str]:
|
||||
return ["id", "amount", "status"]
|
||||
|
||||
def get_column_values(self, column: str) -> list[Any]:
|
||||
if column == "status":
|
||||
return ["draft", "pending", "approved"]
|
||||
if column == "amount":
|
||||
return [100, 250, 500]
|
||||
return []
|
||||
|
||||
def get_row_count(self, table: str) -> int:
|
||||
return 150
|
||||
|
||||
def get_style_presets(self) -> list[str]:
|
||||
return ["custom_highlight"]
|
||||
|
||||
def get_format_presets(self) -> list[str]:
|
||||
return ["CHF"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def provider():
|
||||
"""Return a mock provider for tests."""
|
||||
return MockProvider()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Scope Detection Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def test_i_can_detect_column_scope():
|
||||
"""Test detection of column scope."""
|
||||
text = "column amount:\n style()"
|
||||
scope = detect_scope(text, current_line=1)
|
||||
|
||||
assert scope.scope_type == "column"
|
||||
assert scope.column_name == "amount"
|
||||
assert scope.row_index is None
|
||||
|
||||
def test_i_can_detect_column_scope_after_first_line():
|
||||
"""Test detection of column scope."""
|
||||
text = "column amount:\n style()"
|
||||
scope = detect_scope(text, current_line=2)
|
||||
|
||||
assert scope.scope_type == "column"
|
||||
assert scope.column_name == "amount"
|
||||
assert scope.row_index is None
|
||||
|
||||
|
||||
def test_i_can_detect_column_scope_quoted():
|
||||
"""Test detection of column scope with quoted column name."""
|
||||
text = 'column "total amount":\n style()'
|
||||
scope = detect_scope(text, current_line=1)
|
||||
|
||||
assert scope.scope_type == "column"
|
||||
assert scope.column_name == "total amount"
|
||||
|
||||
|
||||
def test_i_can_detect_row_scope():
|
||||
"""Test detection of row scope."""
|
||||
text = "row 5:\n style()"
|
||||
scope = detect_scope(text, current_line=1)
|
||||
|
||||
assert scope.scope_type == "row"
|
||||
assert scope.row_index == 5
|
||||
assert scope.column_name is None
|
||||
|
||||
|
||||
def test_i_can_detect_cell_scope():
|
||||
"""Test detection of cell scope."""
|
||||
text = "cell (amount, 3):\n style()"
|
||||
scope = detect_scope(text, current_line=1)
|
||||
|
||||
assert scope.scope_type == "cell"
|
||||
assert scope.column_name == "amount"
|
||||
assert scope.row_index == 3
|
||||
|
||||
|
||||
def test_i_can_detect_cell_scope_quoted():
|
||||
"""Test detection of cell scope with quoted column name."""
|
||||
text = 'cell ("total amount", 3):\n style()'
|
||||
scope = detect_scope(text, current_line=1)
|
||||
|
||||
assert scope.scope_type == "cell"
|
||||
assert scope.column_name == "total amount"
|
||||
assert scope.row_index == 3
|
||||
|
||||
|
||||
def test_i_cannot_detect_scope_without_declaration():
|
||||
"""Test that no scope is detected when there's no declaration."""
|
||||
text = " style()"
|
||||
scope = detect_scope(text, current_line=0)
|
||||
|
||||
assert scope.scope_type is None
|
||||
|
||||
|
||||
def test_i_can_detect_scope_with_multiple_declarations():
|
||||
"""Test that the most recent scope is detected."""
|
||||
text = "column id:\n style()\ncolumn amount:\n format()"
|
||||
scope = detect_scope(text, current_line=3)
|
||||
|
||||
assert scope.scope_type == "column"
|
||||
assert scope.column_name == "amount"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Context Detection - Scope Contexts
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def test_context_scope_keyword_at_line_start():
|
||||
"""Test SCOPE_KEYWORD context at start of non-indented line."""
|
||||
text = ""
|
||||
cursor = Position(line=0, ch=0)
|
||||
scope = DetectedScope()
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.SCOPE_KEYWORD
|
||||
|
||||
|
||||
def test_context_scope_keyword_partial():
|
||||
"""Test SCOPE_KEYWORD context with partial keyword."""
|
||||
text = "col"
|
||||
cursor = Position(line=0, ch=3)
|
||||
scope = DetectedScope()
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.SCOPE_KEYWORD
|
||||
|
||||
|
||||
def test_context_column_name_after_column():
|
||||
"""Test COLUMN_NAME context after 'column '."""
|
||||
text = "column "
|
||||
cursor = Position(line=0, ch=7)
|
||||
scope = DetectedScope()
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.COLUMN_NAME
|
||||
|
||||
|
||||
def test_context_row_index_after_row():
|
||||
"""Test ROW_INDEX context after 'row '."""
|
||||
text = "row "
|
||||
cursor = Position(line=0, ch=4)
|
||||
scope = DetectedScope()
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.ROW_INDEX
|
||||
|
||||
|
||||
def test_context_cell_start_after_cell():
|
||||
"""Test CELL_START context after 'cell '."""
|
||||
text = "cell "
|
||||
cursor = Position(line=0, ch=5)
|
||||
scope = DetectedScope()
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.CELL_START
|
||||
|
||||
|
||||
def test_context_cell_column_after_open_paren():
|
||||
"""Test CELL_COLUMN context after 'cell ('."""
|
||||
text = "cell ("
|
||||
cursor = Position(line=0, ch=6)
|
||||
scope = DetectedScope()
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.CELL_COLUMN
|
||||
|
||||
|
||||
def test_context_cell_row_after_comma():
|
||||
"""Test CELL_ROW context after 'cell (amount, '."""
|
||||
text = "cell (amount, "
|
||||
cursor = Position(line=0, ch=14)
|
||||
scope = DetectedScope()
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.CELL_ROW
|
||||
|
||||
|
||||
def test_context_cell_row_after_comma_quoted():
|
||||
"""Test CELL_ROW context after 'cell ("column", '."""
|
||||
text = 'cell ("amount", '
|
||||
cursor = Position(line=0, ch=16)
|
||||
scope = DetectedScope()
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.CELL_ROW
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Context Detection - Rule Contexts
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def test_context_rule_start_on_indented_empty_line():
|
||||
"""Test RULE_START context on empty indented line."""
|
||||
text = "column amount:\n "
|
||||
cursor = Position(line=1, ch=4)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.RULE_START
|
||||
|
||||
|
||||
def test_context_rule_start_partial_keyword():
|
||||
"""Test RULE_START context with partial keyword."""
|
||||
text = "column amount:\n sty"
|
||||
cursor = Position(line=1, ch=7)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.RULE_START
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Context Detection - Style Contexts
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def test_context_style_args_after_open_paren():
|
||||
"""Test STYLE_ARGS context after 'style('."""
|
||||
text = "column amount:\n style("
|
||||
cursor = Position(line=1, ch=10)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.STYLE_ARGS
|
||||
|
||||
|
||||
def test_context_style_preset_inside_quotes():
|
||||
"""Test STYLE_PRESET context inside style quotes."""
|
||||
text = 'column amount:\n style("err'
|
||||
cursor = Position(line=1, ch=14)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.STYLE_PRESET
|
||||
|
||||
|
||||
def test_context_style_param_after_comma():
|
||||
"""Test STYLE_PARAM context after comma in style()."""
|
||||
text = 'column amount:\n style("error", '
|
||||
cursor = Position(line=1, ch=21)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.STYLE_PARAM
|
||||
|
||||
|
||||
def test_context_boolean_value_after_bold():
|
||||
"""Test BOOLEAN_VALUE context after 'bold='."""
|
||||
text = "column amount:\n style(bold="
|
||||
cursor = Position(line=1, ch=15)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.BOOLEAN_VALUE
|
||||
|
||||
|
||||
def test_context_boolean_value_after_italic():
|
||||
"""Test BOOLEAN_VALUE context after 'italic='."""
|
||||
text = "column amount:\n style(italic="
|
||||
cursor = Position(line=1, ch=17)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.BOOLEAN_VALUE
|
||||
|
||||
|
||||
def test_context_color_value_after_color():
|
||||
"""Test COLOR_VALUE context after 'color='."""
|
||||
text = "column amount:\n style(color="
|
||||
cursor = Position(line=1, ch=16)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.COLOR_VALUE
|
||||
|
||||
|
||||
def test_context_color_value_after_background_color():
|
||||
"""Test COLOR_VALUE context after 'background_color='."""
|
||||
text = "column amount:\n style(background_color="
|
||||
cursor = Position(line=1, ch=27)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.COLOR_VALUE
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Context Detection - Format Contexts
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def test_context_format_preset_inside_quotes():
|
||||
"""Test FORMAT_PRESET context inside format quotes."""
|
||||
text = 'column amount:\n format("EU'
|
||||
cursor = Position(line=1, ch=15)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.FORMAT_PRESET
|
||||
|
||||
|
||||
def test_context_format_type_after_dot():
|
||||
"""Test FORMAT_TYPE context after 'format.'."""
|
||||
text = "column amount:\n format."
|
||||
cursor = Position(line=1, ch=11)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.FORMAT_TYPE
|
||||
|
||||
|
||||
def test_context_format_param_date():
|
||||
"""Test FORMAT_PARAM_DATE context inside format.date()."""
|
||||
text = "column amount:\n format.date("
|
||||
cursor = Position(line=1, ch=16)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.FORMAT_PARAM_DATE
|
||||
|
||||
|
||||
def test_context_format_param_text():
|
||||
"""Test FORMAT_PARAM_TEXT context inside format.text()."""
|
||||
text = "column amount:\n format.text("
|
||||
cursor = Position(line=1, ch=16)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.FORMAT_PARAM_TEXT
|
||||
|
||||
|
||||
def test_context_date_format_value():
|
||||
"""Test DATE_FORMAT_VALUE context after 'format=' in format.date."""
|
||||
text = "column amount:\n format.date(format="
|
||||
cursor = Position(line=1, ch=23)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.DATE_FORMAT_VALUE
|
||||
|
||||
|
||||
def test_context_transform_value():
|
||||
"""Test TRANSFORM_VALUE context after 'transform='."""
|
||||
text = "column amount:\n format.text(transform="
|
||||
cursor = Position(line=1, ch=26)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.TRANSFORM_VALUE
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Context Detection - After Style/Format
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def test_context_after_style_or_format():
|
||||
"""Test AFTER_STYLE_OR_FORMAT context after closing paren."""
|
||||
text = 'column amount:\n style("error")'
|
||||
cursor = Position(line=1, ch=19)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.AFTER_STYLE_OR_FORMAT
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Context Detection - Condition Contexts
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def test_context_condition_start_after_if():
|
||||
"""Test CONDITION_START context after 'if '."""
|
||||
text = 'column amount:\n style("error") if '
|
||||
cursor = Position(line=1, ch=23)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.CONDITION_START
|
||||
|
||||
|
||||
def test_context_condition_after_not():
|
||||
"""Test CONDITION_AFTER_NOT context after 'if not '."""
|
||||
text = 'column amount:\n style("error") if not '
|
||||
cursor = Position(line=1, ch=27)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.CONDITION_AFTER_NOT
|
||||
|
||||
|
||||
def test_context_column_ref_after_col_dot():
|
||||
"""Test COLUMN_REF context after 'col.'."""
|
||||
text = 'column amount:\n style("error") if col.'
|
||||
cursor = Position(line=1, ch=28)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.COLUMN_REF
|
||||
|
||||
|
||||
def test_context_column_ref_quoted():
|
||||
"""Test COLUMN_REF_QUOTED context after 'col."'."""
|
||||
text = 'column amount:\n style("error") if col."'
|
||||
cursor = Position(line=1, ch=29)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.COLUMN_REF_QUOTED
|
||||
|
||||
|
||||
def test_context_operator_after_value():
|
||||
"""Test OPERATOR context after 'value '."""
|
||||
text = 'column amount:\n style("error") if value '
|
||||
cursor = Position(line=1, ch=30)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.OPERATOR
|
||||
|
||||
|
||||
def test_context_operator_value_after_equals():
|
||||
"""Test OPERATOR_VALUE context after 'value == '."""
|
||||
text = 'column amount:\n style("error") if value == '
|
||||
cursor = Position(line=1, ch=33)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.OPERATOR_VALUE
|
||||
|
||||
|
||||
def test_context_between_and():
|
||||
"""Test BETWEEN_AND context after 'between 0 '."""
|
||||
text = 'column amount:\n style("error") if value between 0 '
|
||||
cursor = Position(line=1, ch=39)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.BETWEEN_AND
|
||||
|
||||
|
||||
def test_context_between_value():
|
||||
"""Test BETWEEN_VALUE context after 'between 0 and '."""
|
||||
text = 'column amount:\n style("error") if value between 0 and '
|
||||
cursor = Position(line=1, ch=43)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.BETWEEN_VALUE
|
||||
|
||||
|
||||
def test_context_in_list_start():
|
||||
"""Test IN_LIST_START context after 'in '."""
|
||||
text = 'column status:\n style("error") if value in '
|
||||
cursor = Position(line=1, ch=33)
|
||||
scope = DetectedScope(scope_type="column", column_name="status")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.IN_LIST_START
|
||||
|
||||
|
||||
def test_context_in_list_value():
|
||||
"""Test IN_LIST_VALUE context after 'in ['."""
|
||||
text = 'column status:\n style("error") if value in ['
|
||||
cursor = Position(line=1, ch=34)
|
||||
scope = DetectedScope(scope_type="column", column_name="status")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.IN_LIST_VALUE
|
||||
|
||||
|
||||
def test_context_in_list_value_after_comma():
|
||||
"""Test IN_LIST_VALUE context after comma in list."""
|
||||
text = 'column status:\n style("error") if value in ["a", '
|
||||
cursor = Position(line=1, ch=39)
|
||||
scope = DetectedScope(scope_type="column", column_name="status")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.IN_LIST_VALUE
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Context Detection - Special Cases
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def test_context_none_in_comment():
|
||||
"""Test NONE context when cursor is in comment."""
|
||||
text = "column amount:\n # comment"
|
||||
cursor = Position(line=1, ch=15)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = detect_context(text, cursor, scope)
|
||||
assert context == Context.NONE
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Suggestions Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def test_suggestions_scope_keyword(provider):
|
||||
"""Test suggestions for SCOPE_KEYWORD context."""
|
||||
scope = DetectedScope()
|
||||
|
||||
suggestions = get_suggestions(Context.SCOPE_KEYWORD, scope, provider)
|
||||
labels = [s.label for s in suggestions]
|
||||
|
||||
assert "column" in labels
|
||||
assert "row" in labels
|
||||
assert "cell" in labels
|
||||
|
||||
|
||||
def test_suggestions_style_preset(provider):
|
||||
"""Test suggestions for STYLE_PRESET context."""
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
suggestions = get_suggestions(Context.STYLE_PRESET, scope, provider)
|
||||
labels = [s.label for s in suggestions]
|
||||
|
||||
assert "primary" in labels
|
||||
assert "error" in labels
|
||||
assert "warning" in labels
|
||||
assert "custom_highlight" in labels # From provider
|
||||
|
||||
|
||||
def test_suggestions_format_type(provider):
|
||||
"""Test suggestions for FORMAT_TYPE context."""
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
suggestions = get_suggestions(Context.FORMAT_TYPE, scope, provider)
|
||||
labels = [s.label for s in suggestions]
|
||||
|
||||
assert "number" in labels
|
||||
assert "date" in labels
|
||||
assert "boolean" in labels
|
||||
assert "text" in labels
|
||||
assert "enum" in labels
|
||||
|
||||
|
||||
def test_suggestions_operators(provider):
|
||||
"""Test suggestions for OPERATOR context."""
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
suggestions = get_suggestions(Context.OPERATOR, scope, provider)
|
||||
labels = [s.label for s in suggestions]
|
||||
|
||||
assert "==" in labels
|
||||
assert "<" in labels
|
||||
assert "contains" in labels
|
||||
assert "in" in labels
|
||||
assert "between" in labels
|
||||
|
||||
|
||||
def test_suggestions_boolean_value(provider):
|
||||
"""Test suggestions for BOOLEAN_VALUE context."""
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
suggestions = get_suggestions(Context.BOOLEAN_VALUE, scope, provider)
|
||||
labels = [s.label for s in suggestions]
|
||||
|
||||
assert "True" in labels
|
||||
assert "False" in labels
|
||||
|
||||
|
||||
def test_suggestions_color_value(provider):
|
||||
"""Test suggestions for COLOR_VALUE context."""
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
suggestions = get_suggestions(Context.COLOR_VALUE, scope, provider)
|
||||
labels = [s.label for s in suggestions]
|
||||
|
||||
assert "red" in labels
|
||||
assert "blue" in labels
|
||||
assert "var(--color-primary)" in labels
|
||||
|
||||
|
||||
def test_suggestions_column_values(provider):
|
||||
"""Test suggestions for OPERATOR_VALUE context with column scope."""
|
||||
scope = DetectedScope(scope_type="column", column_name="status")
|
||||
|
||||
suggestions = get_suggestions(Context.OPERATOR_VALUE, scope, provider)
|
||||
labels = [s.label for s in suggestions]
|
||||
|
||||
# Base suggestions
|
||||
assert "col." in labels
|
||||
assert "True" in labels
|
||||
assert "False" in labels
|
||||
|
||||
# Column values from provider
|
||||
assert '"draft"' in labels
|
||||
assert '"pending"' in labels
|
||||
assert '"approved"' in labels
|
||||
|
||||
|
||||
def test_suggestions_rule_start(provider):
|
||||
"""Test suggestions for RULE_START context."""
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
suggestions = get_suggestions(Context.RULE_START, scope, provider)
|
||||
labels = [s.label for s in suggestions]
|
||||
|
||||
assert "style(" in labels
|
||||
assert "format(" in labels
|
||||
assert "format." in labels
|
||||
|
||||
|
||||
def test_suggestions_none_context(provider):
|
||||
"""Test that NONE context returns empty suggestions."""
|
||||
scope = DetectedScope()
|
||||
|
||||
suggestions = get_suggestions(Context.NONE, scope, provider)
|
||||
|
||||
assert suggestions == []
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Engine Integration Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def test_i_can_get_completions_for_style_preset(provider):
|
||||
"""Test complete flow for style preset completion."""
|
||||
text = 'column amount:\n style("'
|
||||
cursor = Position(line=1, ch=11)
|
||||
|
||||
result = get_completions(text, cursor, provider)
|
||||
|
||||
assert not result.is_empty
|
||||
labels = [s.label for s in result.suggestions]
|
||||
assert "primary" in labels
|
||||
assert "error" in labels
|
||||
|
||||
|
||||
def test_i_can_get_completions_filters_by_prefix(provider):
|
||||
"""Test that completions are filtered by prefix."""
|
||||
text = 'column amount:\n style("err'
|
||||
cursor = Position(line=1, ch=14)
|
||||
|
||||
result = get_completions(text, cursor, provider)
|
||||
|
||||
labels = [s.label for s in result.suggestions]
|
||||
assert "error" in labels
|
||||
assert "primary" not in labels
|
||||
|
||||
|
||||
def test_i_can_get_completions_returns_correct_positions(provider):
|
||||
"""Test that completion result has correct from/to positions."""
|
||||
text = 'column amount:\n style("err'
|
||||
cursor = Position(line=1, ch=14) # After "err"
|
||||
|
||||
result = get_completions(text, cursor, provider)
|
||||
|
||||
# from_pos should be at start of "err"
|
||||
assert result.from_pos.line == 1
|
||||
assert result.from_pos.ch == 11 # Start of "err"
|
||||
|
||||
# to_pos should be at end of "err"
|
||||
assert result.to_pos.line == 1
|
||||
assert result.to_pos.ch == 14 # End of "err"
|
||||
|
||||
|
||||
def test_i_can_get_completions_at_scope_start(provider):
|
||||
"""Test completions at the start of a new line (scope keywords)."""
|
||||
text = ""
|
||||
cursor = Position(line=0, ch=0)
|
||||
|
||||
result = get_completions(text, cursor, provider)
|
||||
|
||||
labels = [s.label for s in result.suggestions]
|
||||
assert "column" in labels
|
||||
assert "row" in labels
|
||||
assert "cell" in labels
|
||||
|
||||
|
||||
def test_i_can_get_completions_for_column_names(provider):
|
||||
"""Test completions for column names."""
|
||||
text = "column "
|
||||
cursor = Position(line=0, ch=7)
|
||||
|
||||
result = get_completions(text, cursor, provider)
|
||||
|
||||
labels = [s.label for s in result.suggestions]
|
||||
assert "id" in labels
|
||||
assert "amount" in labels
|
||||
assert "status" in labels
|
||||
|
||||
|
||||
def test_i_can_get_completions_in_comment_returns_empty(provider):
|
||||
"""Test that completions in comment are empty."""
|
||||
text = "column amount:\n # comment"
|
||||
cursor = Position(line=1, ch=15)
|
||||
|
||||
result = get_completions(text, cursor, provider)
|
||||
|
||||
assert result.is_empty
|
||||
|
||||
|
||||
def test_i_can_create_formatting_completion_engine(provider):
|
||||
"""Test that FormattingCompletionEngine can be instantiated."""
|
||||
engine = FormattingCompletionEngine(provider)
|
||||
|
||||
assert engine.provider == provider
|
||||
|
||||
|
||||
def test_i_can_use_engine_detect_scope(provider):
|
||||
"""Test engine's detect_scope method."""
|
||||
engine = FormattingCompletionEngine(provider)
|
||||
text = "column amount:\n style()"
|
||||
|
||||
scope = engine.detect_scope(text, current_line=1)
|
||||
|
||||
assert scope.scope_type == "column"
|
||||
assert scope.column_name == "amount"
|
||||
|
||||
|
||||
def test_i_can_use_engine_detect_context(provider):
|
||||
"""Test engine's detect_context method."""
|
||||
engine = FormattingCompletionEngine(provider)
|
||||
text = "column amount:\n style("
|
||||
cursor = Position(line=1, ch=10)
|
||||
scope = DetectedScope(scope_type="column", column_name="amount")
|
||||
|
||||
context = engine.detect_context(text, cursor, scope)
|
||||
|
||||
assert context == Context.STYLE_ARGS
|
||||
576
tests/core/formatting/test_dsl_parser.py
Normal file
576
tests/core/formatting/test_dsl_parser.py
Normal file
@@ -0,0 +1,576 @@
|
||||
"""
|
||||
Tests for the DataGrid Formatting DSL parser.
|
||||
|
||||
Tests the parsing of DSL text into ScopedRule objects.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from myfasthtml.core.formatting.dsl import (
|
||||
parse_dsl,
|
||||
ColumnScope,
|
||||
RowScope,
|
||||
CellScope,
|
||||
ScopedRule,
|
||||
DSLSyntaxError,
|
||||
)
|
||||
from myfasthtml.core.formatting.dataclasses import (
|
||||
Condition,
|
||||
Style,
|
||||
FormatRule,
|
||||
NumberFormatter,
|
||||
DateFormatter,
|
||||
BooleanFormatter,
|
||||
TextFormatter,
|
||||
EnumFormatter,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Scope Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestColumnScope:
|
||||
"""Tests for column scope parsing."""
|
||||
|
||||
def test_i_can_parse_column_scope(self):
|
||||
"""Test parsing a simple column scope."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style("error")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, ColumnScope)
|
||||
assert rules[0].scope.column == "amount"
|
||||
|
||||
def test_i_can_parse_column_scope_with_quoted_name(self):
|
||||
"""Test parsing a column scope with quoted name containing spaces."""
|
||||
dsl = """
|
||||
column "total amount":
|
||||
style("error")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, ColumnScope)
|
||||
assert rules[0].scope.column == "total amount"
|
||||
|
||||
|
||||
class TestRowScope:
|
||||
"""Tests for row scope parsing."""
|
||||
|
||||
def test_i_can_parse_row_scope(self):
|
||||
"""Test parsing a row scope."""
|
||||
dsl = """
|
||||
row 0:
|
||||
style("neutral")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, RowScope)
|
||||
assert rules[0].scope.row == 0
|
||||
|
||||
def test_i_can_parse_row_scope_with_large_index(self):
|
||||
"""Test parsing a row scope with a large index."""
|
||||
dsl = """
|
||||
row 999:
|
||||
style("highlight")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert rules[0].scope.row == 999
|
||||
|
||||
|
||||
class TestCellScope:
|
||||
"""Tests for cell scope parsing."""
|
||||
|
||||
def test_i_can_parse_cell_scope_with_coords(self):
|
||||
"""Test parsing a cell scope with coordinates."""
|
||||
dsl = """
|
||||
cell (amount, 3):
|
||||
style("highlight")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, CellScope)
|
||||
assert rules[0].scope.column == "amount"
|
||||
assert rules[0].scope.row == 3
|
||||
assert rules[0].scope.cell_id is None
|
||||
|
||||
def test_i_can_parse_cell_scope_with_quoted_column(self):
|
||||
"""Test parsing a cell scope with quoted column name."""
|
||||
dsl = """
|
||||
cell ("total amount", 5):
|
||||
style("highlight")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert rules[0].scope.column == "total amount"
|
||||
assert rules[0].scope.row == 5
|
||||
|
||||
def test_i_can_parse_cell_scope_with_id(self):
|
||||
"""Test parsing a cell scope with cell ID."""
|
||||
dsl = """
|
||||
cell tcell_grid1-3-2:
|
||||
style("highlight")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, CellScope)
|
||||
assert rules[0].scope.cell_id == "tcell_grid1-3-2"
|
||||
assert rules[0].scope.column is None
|
||||
assert rules[0].scope.row is None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Style Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestStyleParsing:
|
||||
"""Tests for style expression parsing."""
|
||||
|
||||
def test_i_can_parse_style_with_preset(self):
|
||||
"""Test parsing style with preset only."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style("error")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert rules[0].rule.style is not None
|
||||
assert rules[0].rule.style.preset == "error"
|
||||
|
||||
def test_i_can_parse_style_without_preset(self):
|
||||
"""Test parsing style without preset, with direct properties."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style(color="red", bold=True)
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
style = rules[0].rule.style
|
||||
assert style.preset is None
|
||||
assert style.color == "red"
|
||||
assert style.font_weight == "bold"
|
||||
|
||||
def test_i_can_parse_style_with_preset_and_options(self):
|
||||
"""Test parsing style with preset and additional options."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style("error", bold=True, italic=True)
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
style = rules[0].rule.style
|
||||
assert style.preset == "error"
|
||||
assert style.font_weight == "bold"
|
||||
assert style.font_style == "italic"
|
||||
|
||||
@pytest.mark.parametrize("option,attr_name,attr_value", [
|
||||
("bold=True", "font_weight", "bold"),
|
||||
("italic=True", "font_style", "italic"),
|
||||
("underline=True", "text_decoration", "underline"),
|
||||
("strikethrough=True", "text_decoration", "line-through"),
|
||||
])
|
||||
def test_i_can_parse_style_options(self, option, attr_name, attr_value):
|
||||
"""Test parsing individual style options."""
|
||||
dsl = f"""
|
||||
column amount:
|
||||
style({option})
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
style = rules[0].rule.style
|
||||
assert getattr(style, attr_name) == attr_value
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Format Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestFormatParsing:
|
||||
"""Tests for format expression parsing."""
|
||||
|
||||
def test_i_can_parse_format_preset(self):
|
||||
"""Test parsing format with preset."""
|
||||
dsl = """
|
||||
column amount:
|
||||
format("EUR")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert formatter is not None
|
||||
assert formatter.preset == "EUR"
|
||||
|
||||
def test_i_can_parse_format_preset_with_options(self):
|
||||
"""Test parsing format preset with options."""
|
||||
dsl = """
|
||||
column amount:
|
||||
format("EUR", precision=3)
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert formatter.preset == "EUR"
|
||||
assert formatter.precision == 3
|
||||
|
||||
@pytest.mark.parametrize("format_type,formatter_class", [
|
||||
("number", NumberFormatter),
|
||||
("date", DateFormatter),
|
||||
("boolean", BooleanFormatter),
|
||||
("text", TextFormatter),
|
||||
("enum", EnumFormatter),
|
||||
])
|
||||
def test_i_can_parse_format_types(self, format_type, formatter_class):
|
||||
"""Test parsing explicit format types."""
|
||||
dsl = f"""
|
||||
column amount:
|
||||
format.{format_type}()
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, formatter_class)
|
||||
|
||||
def test_i_can_parse_format_number_with_options(self):
|
||||
"""Test parsing format.number with all options."""
|
||||
dsl = """
|
||||
column amount:
|
||||
format.number(precision=2, suffix=" EUR", thousands_sep=" ")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, NumberFormatter)
|
||||
assert formatter.precision == 2
|
||||
assert formatter.suffix == " EUR"
|
||||
assert formatter.thousands_sep == " "
|
||||
|
||||
def test_i_can_parse_format_date_with_options(self):
|
||||
"""Test parsing format.date with format option."""
|
||||
dsl = """
|
||||
column created_at:
|
||||
format.date(format="%d/%m/%Y")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, DateFormatter)
|
||||
assert formatter.format == "%d/%m/%Y"
|
||||
|
||||
def test_i_can_parse_format_boolean_with_options(self):
|
||||
"""Test parsing format.boolean with all options."""
|
||||
dsl = """
|
||||
column active:
|
||||
format.boolean(true_value="Oui", false_value="Non")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, BooleanFormatter)
|
||||
assert formatter.true_value == "Oui"
|
||||
assert formatter.false_value == "Non"
|
||||
|
||||
def test_i_can_parse_format_enum_with_source(self):
|
||||
"""Test parsing format.enum with source mapping."""
|
||||
dsl = """
|
||||
column status:
|
||||
format.enum(source={"draft": "Brouillon", "published": "Publie"})
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, EnumFormatter)
|
||||
assert formatter.source == {"draft": "Brouillon", "published": "Publie"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Condition Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestConditionParsing:
|
||||
"""Tests for condition parsing."""
|
||||
|
||||
@pytest.mark.parametrize("operator,dsl_op", [
|
||||
("==", "=="),
|
||||
("!=", "!="),
|
||||
("<", "<"),
|
||||
("<=", "<="),
|
||||
(">", ">"),
|
||||
(">=", ">="),
|
||||
("contains", "contains"),
|
||||
("startswith", "startswith"),
|
||||
("endswith", "endswith"),
|
||||
])
|
||||
def test_i_can_parse_comparison_operators(self, operator, dsl_op):
|
||||
"""Test parsing all comparison operators."""
|
||||
dsl = f"""
|
||||
column amount:
|
||||
style("error") if value {dsl_op} 0
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition is not None
|
||||
assert condition.operator == operator
|
||||
|
||||
@pytest.mark.parametrize("unary_op", ["isempty", "isnotempty"])
|
||||
def test_i_can_parse_unary_conditions(self, unary_op):
|
||||
"""Test parsing unary conditions (isempty, isnotempty)."""
|
||||
dsl = f"""
|
||||
column name:
|
||||
style("neutral") if value {unary_op}
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.operator == unary_op
|
||||
|
||||
def test_i_can_parse_condition_in(self):
|
||||
"""Test parsing 'in' condition with list."""
|
||||
dsl = """
|
||||
column status:
|
||||
style("success") if value in ["approved", "validated"]
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.operator == "in"
|
||||
assert condition.value == ["approved", "validated"]
|
||||
|
||||
def test_i_can_parse_condition_between(self):
|
||||
"""Test parsing 'between' condition."""
|
||||
dsl = """
|
||||
column score:
|
||||
style("warning") if value between 30 and 70
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.operator == "between"
|
||||
assert condition.value == [30, 70]
|
||||
|
||||
def test_i_can_parse_condition_negation(self):
|
||||
"""Test parsing negated condition."""
|
||||
dsl = """
|
||||
column status:
|
||||
style("error") if not value == "approved"
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.negate is True
|
||||
assert condition.operator == "=="
|
||||
|
||||
def test_i_can_parse_condition_case_sensitive(self):
|
||||
"""Test parsing case-sensitive condition."""
|
||||
dsl = """
|
||||
column name:
|
||||
style("error") if value == "Error" (case)
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.case_sensitive is True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Literal Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestLiteralParsing:
|
||||
"""Tests for literal value parsing in conditions."""
|
||||
|
||||
@pytest.mark.parametrize("literal,expected_value,expected_type", [
|
||||
('"hello"', "hello", str),
|
||||
("'world'", "world", str),
|
||||
("42", 42, int),
|
||||
("-10", -10, int),
|
||||
("3.14", 3.14, float),
|
||||
("-2.5", -2.5, float),
|
||||
("True", True, bool),
|
||||
("False", False, bool),
|
||||
("true", True, bool),
|
||||
("false", False, bool),
|
||||
])
|
||||
def test_i_can_parse_literals(self, literal, expected_value, expected_type):
|
||||
"""Test parsing various literal types in conditions."""
|
||||
dsl = f"""
|
||||
column amount:
|
||||
style("error") if value == {literal}
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.value == expected_value
|
||||
assert isinstance(condition.value, expected_type)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Reference Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestReferenceParsing:
|
||||
"""Tests for cell reference parsing in conditions."""
|
||||
|
||||
def test_i_can_parse_column_reference(self):
|
||||
"""Test parsing column reference in condition."""
|
||||
dsl = """
|
||||
column actual:
|
||||
style("error") if value > col.budget
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.value == {"col": "budget"}
|
||||
|
||||
def test_i_can_parse_column_reference_with_quoted_name(self):
|
||||
"""Test parsing column reference with quoted name."""
|
||||
dsl = """
|
||||
column actual:
|
||||
style("error") if value > col."max budget"
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.value == {"col": "max budget"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Complex Structure Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestComplexStructures:
|
||||
"""Tests for complex DSL structures."""
|
||||
|
||||
def test_i_can_parse_multiple_rules_in_scope(self):
|
||||
"""Test parsing multiple rules under one scope."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style("error") if value < 0
|
||||
style("success") if value > 1000
|
||||
format("EUR")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 3
|
||||
# All rules share the same scope
|
||||
for rule in rules:
|
||||
assert isinstance(rule.scope, ColumnScope)
|
||||
assert rule.scope.column == "amount"
|
||||
|
||||
def test_i_can_parse_multiple_scopes(self):
|
||||
"""Test parsing multiple scopes."""
|
||||
dsl = """
|
||||
column amount:
|
||||
format("EUR")
|
||||
|
||||
column status:
|
||||
style("success") if value == "approved"
|
||||
|
||||
row 0:
|
||||
style("neutral", bold=True)
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 3
|
||||
|
||||
# First rule: column amount
|
||||
assert isinstance(rules[0].scope, ColumnScope)
|
||||
assert rules[0].scope.column == "amount"
|
||||
|
||||
# Second rule: column status
|
||||
assert isinstance(rules[1].scope, ColumnScope)
|
||||
assert rules[1].scope.column == "status"
|
||||
|
||||
# Third rule: row 0
|
||||
assert isinstance(rules[2].scope, RowScope)
|
||||
assert rules[2].scope.row == 0
|
||||
|
||||
def test_i_can_parse_style_and_format_combined(self):
|
||||
"""Test parsing style and format on same line."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style("error") format("EUR") if value < 0
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
rule = rules[0].rule
|
||||
assert rule.style is not None
|
||||
assert rule.style.preset == "error"
|
||||
assert rule.formatter is not None
|
||||
assert rule.formatter.preset == "EUR"
|
||||
assert rule.condition is not None
|
||||
assert rule.condition.operator == "<"
|
||||
|
||||
def test_i_can_parse_comments(self):
|
||||
"""Test that comments are ignored."""
|
||||
dsl = """
|
||||
# This is a comment
|
||||
column amount:
|
||||
# Another comment
|
||||
style("error") if value < 0
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert rules[0].rule.style.preset == "error"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Error Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestSyntaxErrors:
|
||||
"""Tests for syntax error handling."""
|
||||
|
||||
def test_i_cannot_parse_invalid_syntax(self):
|
||||
"""Test that invalid syntax raises DSLSyntaxError."""
|
||||
dsl = """
|
||||
column amount
|
||||
style("error")
|
||||
"""
|
||||
with pytest.raises(DSLSyntaxError):
|
||||
parse_dsl(dsl)
|
||||
|
||||
def test_i_cannot_parse_missing_indent(self):
|
||||
"""Test that missing indentation raises DSLSyntaxError."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style("error")
|
||||
"""
|
||||
with pytest.raises(DSLSyntaxError):
|
||||
parse_dsl(dsl)
|
||||
|
||||
def test_i_cannot_parse_empty_scope(self):
|
||||
"""Test that empty scope raises DSLSyntaxError."""
|
||||
dsl = """
|
||||
column amount:
|
||||
"""
|
||||
with pytest.raises(DSLSyntaxError):
|
||||
parse_dsl(dsl)
|
||||
|
||||
def test_i_cannot_parse_invalid_operator(self):
|
||||
"""Test that invalid operator raises DSLSyntaxError."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style("error") if value <> 0
|
||||
"""
|
||||
with pytest.raises(DSLSyntaxError):
|
||||
parse_dsl(dsl)
|
||||
105
tests/core/formatting/test_formatting_dsl_definition.py
Normal file
105
tests/core/formatting/test_formatting_dsl_definition.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""Tests for FormattingDSL definition."""
|
||||
|
||||
import pytest
|
||||
|
||||
from myfasthtml.core.formatting.dsl.definition import FormattingDSL
|
||||
from myfasthtml.core.formatting.dsl.grammar import GRAMMAR
|
||||
|
||||
|
||||
class TestFormattingDSL:
|
||||
"""Tests for FormattingDSL class."""
|
||||
|
||||
def test_i_can_create_formatting_dsl(self):
|
||||
"""Test that FormattingDSL can be instantiated."""
|
||||
dsl = FormattingDSL()
|
||||
|
||||
assert dsl is not None
|
||||
assert dsl.name == "Formatting DSL"
|
||||
|
||||
def test_i_can_get_formatting_dsl_grammar(self):
|
||||
"""Test that get_grammar() returns the GRAMMAR constant."""
|
||||
dsl = FormattingDSL()
|
||||
|
||||
grammar = dsl.get_grammar()
|
||||
|
||||
assert grammar == GRAMMAR
|
||||
assert "scope" in grammar
|
||||
assert "style_expr" in grammar
|
||||
assert "format_expr" in grammar
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"keyword",
|
||||
["column", "row", "cell", "if", "not", "value", "and"],
|
||||
)
|
||||
def test_i_can_get_formatting_dsl_keywords(self, keyword):
|
||||
"""Test that expected keywords are extracted from formatting DSL."""
|
||||
dsl = FormattingDSL()
|
||||
|
||||
completions = dsl.completions
|
||||
|
||||
assert keyword in completions["keywords"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"operator",
|
||||
["==", "!=", "<=", "<", ">=", ">", "contains", "startswith", "endswith"],
|
||||
)
|
||||
def test_i_can_get_formatting_dsl_operators(self, operator):
|
||||
"""Test that expected operators are extracted from formatting DSL."""
|
||||
dsl = FormattingDSL()
|
||||
|
||||
completions = dsl.completions
|
||||
|
||||
assert operator in completions["operators"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"function",
|
||||
["style", "format"],
|
||||
)
|
||||
def test_i_can_get_formatting_dsl_functions(self, function):
|
||||
"""Test that expected functions are extracted from formatting DSL."""
|
||||
dsl = FormattingDSL()
|
||||
|
||||
completions = dsl.completions
|
||||
|
||||
assert function in completions["functions"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"type_name",
|
||||
["number", "date", "boolean", "text", "enum"],
|
||||
)
|
||||
def test_i_can_get_formatting_dsl_types(self, type_name):
|
||||
"""Test that expected types are extracted from formatting DSL."""
|
||||
dsl = FormattingDSL()
|
||||
|
||||
completions = dsl.completions
|
||||
|
||||
assert type_name in completions["types"]
|
||||
|
||||
def test_i_can_get_completions_is_cached(self):
|
||||
"""Test that completions property is cached (same object returned)."""
|
||||
dsl = FormattingDSL()
|
||||
|
||||
completions1 = dsl.completions
|
||||
completions2 = dsl.completions
|
||||
|
||||
assert completions1 is completions2
|
||||
|
||||
def test_i_can_get_lezer_grammar_is_cached(self):
|
||||
"""Test that lezer_grammar property is cached (same object returned)."""
|
||||
dsl = FormattingDSL()
|
||||
|
||||
lezer1 = dsl.lezer_grammar
|
||||
lezer2 = dsl.lezer_grammar
|
||||
|
||||
assert lezer1 is lezer2
|
||||
|
||||
def test_i_can_get_editor_config(self):
|
||||
"""Test that get_editor_config() returns expected structure."""
|
||||
dsl = FormattingDSL()
|
||||
|
||||
config = dsl.get_editor_config()
|
||||
|
||||
assert "name" in config
|
||||
assert "lezerGrammar" in config
|
||||
assert "completions" in config
|
||||
assert config["name"] == "Formatting DSL"
|
||||
Reference in New Issue
Block a user