Added classes to support formatting
This commit is contained in:
282
tests/core/formatting/test_engine.py
Normal file
282
tests/core/formatting/test_engine.py
Normal file
@@ -0,0 +1,282 @@
|
||||
import pytest
|
||||
|
||||
from myfasthtml.core.formatting.dataclasses import (
|
||||
Condition,
|
||||
Style,
|
||||
Formatter,
|
||||
NumberFormatter,
|
||||
FormatRule,
|
||||
)
|
||||
from myfasthtml.core.formatting.engine import FormattingEngine
|
||||
|
||||
|
||||
class TestApplyFormat:
|
||||
def test_apply_format_with_style_only(self):
|
||||
"""Rule with style only returns CSS string."""
|
||||
engine = FormattingEngine()
|
||||
rules = [FormatRule(style=Style(background_color="red", color="white"))]
|
||||
|
||||
css, formatted = engine.apply_format(rules, cell_value=42)
|
||||
|
||||
assert css is not None
|
||||
assert "background-color: red" in css
|
||||
assert "color: white" in css
|
||||
assert formatted is None
|
||||
|
||||
def test_apply_format_with_formatter_only(self):
|
||||
"""Rule with formatter only returns formatted value."""
|
||||
engine = FormattingEngine()
|
||||
rules = [FormatRule(formatter=NumberFormatter(precision=2, suffix=" €"))]
|
||||
|
||||
css, formatted = engine.apply_format(rules, cell_value=1234.5)
|
||||
|
||||
assert css is None
|
||||
assert formatted == "1234.50 €"
|
||||
|
||||
def test_apply_format_with_style_and_formatter(self):
|
||||
"""Rule with both style and formatter returns both."""
|
||||
engine = FormattingEngine()
|
||||
rules = [
|
||||
FormatRule(
|
||||
style=Style(color="green"),
|
||||
formatter=NumberFormatter(precision=2)
|
||||
)
|
||||
]
|
||||
|
||||
css, formatted = engine.apply_format(rules, cell_value=42.567)
|
||||
|
||||
assert css is not None
|
||||
assert "color: green" in css
|
||||
assert formatted == "42.57"
|
||||
|
||||
def test_apply_format_condition_met(self):
|
||||
"""Conditional rule applies when condition is met."""
|
||||
engine = FormattingEngine()
|
||||
rules = [
|
||||
FormatRule(
|
||||
condition=Condition(operator="<", value=0),
|
||||
style=Style(color="red")
|
||||
)
|
||||
]
|
||||
|
||||
css, formatted = engine.apply_format(rules, cell_value=-5)
|
||||
|
||||
assert css is not None
|
||||
assert "color: red" in css
|
||||
|
||||
def test_apply_format_condition_not_met(self):
|
||||
"""Conditional rule does not apply when condition is not met."""
|
||||
engine = FormattingEngine()
|
||||
rules = [
|
||||
FormatRule(
|
||||
condition=Condition(operator="<", value=0),
|
||||
style=Style(color="red")
|
||||
)
|
||||
]
|
||||
|
||||
css, formatted = engine.apply_format(rules, cell_value=5)
|
||||
|
||||
assert css is None
|
||||
assert formatted is None
|
||||
|
||||
def test_empty_rules_returns_none(self):
|
||||
"""Empty rules list returns (None, None)."""
|
||||
engine = FormattingEngine()
|
||||
|
||||
css, formatted = engine.apply_format([], cell_value=42)
|
||||
|
||||
assert css is None
|
||||
assert formatted is None
|
||||
|
||||
|
||||
class TestConflictResolution:
|
||||
def test_unconditional_rule_always_applies(self):
|
||||
"""Unconditional rule (no condition) always applies."""
|
||||
engine = FormattingEngine()
|
||||
rules = [FormatRule(style=Style(color="gray"))]
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value="anything")
|
||||
|
||||
assert css is not None
|
||||
assert "color: gray" in css
|
||||
|
||||
def test_multiple_unconditional_rules_last_wins(self):
|
||||
"""Among unconditional rules, last one wins."""
|
||||
engine = FormattingEngine()
|
||||
rules = [
|
||||
FormatRule(style=Style(color="red")),
|
||||
FormatRule(style=Style(color="blue")),
|
||||
FormatRule(style=Style(color="green")),
|
||||
]
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=42)
|
||||
|
||||
assert "color: green" in css
|
||||
assert "color: red" not in css
|
||||
assert "color: blue" not in css
|
||||
|
||||
def test_conditional_beats_unconditional(self):
|
||||
"""Conditional rule (higher specificity) beats unconditional."""
|
||||
engine = FormattingEngine()
|
||||
rules = [
|
||||
FormatRule(style=Style(color="gray")), # unconditional, specificity=0
|
||||
FormatRule(
|
||||
condition=Condition(operator="<", value=0),
|
||||
style=Style(color="red") # conditional, specificity=1
|
||||
),
|
||||
]
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=-5)
|
||||
|
||||
assert "color: red" in css
|
||||
assert "color: gray" not in css
|
||||
|
||||
def test_conditional_not_met_falls_back_to_unconditional(self):
|
||||
"""When conditional doesn't match, unconditional applies."""
|
||||
engine = FormattingEngine()
|
||||
rules = [
|
||||
FormatRule(style=Style(color="gray")), # unconditional
|
||||
FormatRule(
|
||||
condition=Condition(operator="<", value=0),
|
||||
style=Style(color="red") # doesn't match
|
||||
),
|
||||
]
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=5) # positive, condition not met
|
||||
|
||||
assert "color: gray" in css
|
||||
|
||||
def test_multiple_conditional_last_wins(self):
|
||||
"""Among conditional rules with same specificity, last wins."""
|
||||
engine = FormattingEngine()
|
||||
rules = [
|
||||
FormatRule(
|
||||
condition=Condition(operator="<", value=10),
|
||||
style=Style(color="red")
|
||||
),
|
||||
FormatRule(
|
||||
condition=Condition(operator="<", value=10),
|
||||
style=Style(color="blue")
|
||||
),
|
||||
]
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=5)
|
||||
|
||||
assert "color: blue" in css
|
||||
assert "color: red" not in css
|
||||
|
||||
def test_spec_example_value_minus_5(self):
|
||||
"""
|
||||
Example from spec: value=-5
|
||||
Rule 1: unconditional gray
|
||||
Rule 2: <0 -> red
|
||||
Rule 3: ==-5 -> black
|
||||
|
||||
Both rule 2 and 3 match with same specificity (1).
|
||||
Rule 3 is last, so black wins.
|
||||
"""
|
||||
engine = FormattingEngine()
|
||||
rules = [
|
||||
FormatRule(style=Style(color="gray")),
|
||||
FormatRule(
|
||||
condition=Condition(operator="<", value=0),
|
||||
style=Style(color="red")
|
||||
),
|
||||
FormatRule(
|
||||
condition=Condition(operator="==", value=-5),
|
||||
style=Style(color="black")
|
||||
),
|
||||
]
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=-5)
|
||||
|
||||
assert "color: black" in css
|
||||
|
||||
def test_spec_example_value_minus_3(self):
|
||||
"""
|
||||
Same rules as above but value=-3.
|
||||
Rule 3 (==-5) doesn't match.
|
||||
Rule 2 (<0) matches and beats rule 1 (unconditional).
|
||||
"""
|
||||
engine = FormattingEngine()
|
||||
rules = [
|
||||
FormatRule(style=Style(color="gray")),
|
||||
FormatRule(
|
||||
condition=Condition(operator="<", value=0),
|
||||
style=Style(color="red")
|
||||
),
|
||||
FormatRule(
|
||||
condition=Condition(operator="==", value=-5),
|
||||
style=Style(color="black")
|
||||
),
|
||||
]
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=-3)
|
||||
|
||||
assert "color: red" in css
|
||||
|
||||
|
||||
class TestWithRowData:
|
||||
def test_condition_with_column_reference(self):
|
||||
"""Condition can reference another column."""
|
||||
engine = FormattingEngine()
|
||||
rules = [
|
||||
FormatRule(
|
||||
condition=Condition(operator=">", value={"col": "budget"}),
|
||||
style=Style(color="red")
|
||||
)
|
||||
]
|
||||
row_data = {"budget": 100, "actual": 150}
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=150, row_data=row_data)
|
||||
|
||||
assert "color: red" in css
|
||||
|
||||
def test_condition_with_col_parameter(self):
|
||||
"""Row-level condition using col parameter."""
|
||||
engine = FormattingEngine()
|
||||
rules = [
|
||||
FormatRule(
|
||||
condition=Condition(operator="==", value="error", col="status"),
|
||||
style=Style(preset="error")
|
||||
)
|
||||
]
|
||||
row_data = {"status": "error", "value": 42}
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=42, row_data=row_data)
|
||||
|
||||
assert css is not None
|
||||
assert "background-color" in css
|
||||
|
||||
|
||||
class TestPresets:
|
||||
def test_style_preset(self):
|
||||
"""Style preset is resolved correctly."""
|
||||
engine = FormattingEngine()
|
||||
rules = [FormatRule(style=Style(preset="success"))]
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=42)
|
||||
|
||||
assert "var(--color-success)" in css
|
||||
|
||||
def test_formatter_preset(self):
|
||||
"""Formatter preset is resolved correctly."""
|
||||
engine = FormattingEngine()
|
||||
rules = [FormatRule(formatter=Formatter(preset="EUR"))]
|
||||
|
||||
_, formatted = engine.apply_format(rules, cell_value=1234.56)
|
||||
|
||||
assert formatted == "1 234,56 €"
|
||||
|
||||
def test_custom_presets(self):
|
||||
"""Custom presets can be injected."""
|
||||
custom_style_presets = {
|
||||
"custom": {"background-color": "purple", "color": "yellow"}
|
||||
}
|
||||
engine = FormattingEngine(style_presets=custom_style_presets)
|
||||
rules = [FormatRule(style=Style(preset="custom"))]
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=42)
|
||||
|
||||
assert "background-color: purple" in css
|
||||
assert "color: yellow" in css
|
||||
Reference in New Issue
Block a user