283 lines
8.8 KiB
Python
283 lines
8.8 KiB
Python
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
|