import pytest from myfasthtml.core.formatting.dataclasses import ( Condition, Style, Formatter, NumberFormatter, FormatRule, ) from myfasthtml.core.formatting.engine import FormattingEngine from myfasthtml.core.formatting.style_resolver import StyleContainer 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 isinstance(css, StyleContainer) assert "background-color: red" in css.css assert "color: white" in css.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 isinstance(css, StyleContainer) assert "color: green" in css.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 isinstance(css, StyleContainer) assert "color: red" in css.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 isinstance(css, StyleContainer) assert "color: gray" in css.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.css assert "color: red" not in css.css assert "color: blue" not in css.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.css assert "color: gray" not in css.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.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.css assert "color: red" not in css.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.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.css def test_style_and_formatter_fusion(self): """ Test that style and formatter can come from different rules. Scenario: - Rule 1: format("EUR") - unconditional formatter - Rule 2: style("secondary") if value > col.X - conditional style When condition is met: - Style from Rule 2 (higher specificity for style) - Formatter from Rule 1 (only rule with formatter) - Both should be applied (fusion) """ engine = FormattingEngine() rules = [ FormatRule(formatter=NumberFormatter(precision=2, suffix=" €")), # unconditional FormatRule( condition=Condition(operator=">", value={"col": "budget"}), style=Style(preset="secondary") # conditional ), ] row_data = {"budget": 100} # Case 1: Condition met (value > budget) css, formatted = engine.apply_format(rules, cell_value=150, row_data=row_data) assert isinstance(css, StyleContainer) assert css.cls == "mf-formatting-secondary" # Style from Rule 2 assert formatted == "150.00 €" # Formatter from Rule 1 # Case 2: Condition not met (value <= budget) css, formatted = engine.apply_format(rules, cell_value=50, row_data=row_data) assert css is None # No style (Rule 2 doesn't match) assert formatted == "50.00 €" # Formatter from Rule 1 still applies def test_multiple_styles_and_formatters_highest_specificity_wins(self): """ Test that style and formatter are resolved independently with specificity. Rules: - Rule 1: style("neutral") - unconditional - Rule 2: format("EUR") - unconditional - Rule 3: style("error") if value < 0 - conditional style - Rule 4: format.number(precision=0) if value < 0 - conditional formatter When value < 0: - Style from Rule 3 (higher specificity) - Formatter from Rule 4 (higher specificity) """ engine = FormattingEngine() rules = [ FormatRule(style=Style(preset="neutral")), FormatRule(formatter=NumberFormatter(precision=2, suffix=" €")), FormatRule( condition=Condition(operator="<", value=0), style=Style(preset="error") ), FormatRule( condition=Condition(operator="<", value=0), formatter=NumberFormatter(precision=0, suffix=" €") ), ] css, formatted = engine.apply_format(rules, cell_value=-5.67) assert css.cls == "mf-formatting-error" # Rule 3 wins for style assert formatted == "-6 €" # Rule 4 wins for formatter (precision=0) 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.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 isinstance(css, StyleContainer) assert css.cls == "mf-formatting-error" 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 css.cls == "mf-formatting-success" 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.css assert "color: yellow" in css.css def test_i_can_expand_rule_preset_via_style(self): """A rule preset referenced via style() must be expanded like via format(). Why: style("traffic_light") should expand the traffic_light rule preset (which has conditional style rules) instead of looking up "traffic_light" as a style preset name (where it doesn't exist). """ engine = FormattingEngine() rules = [FormatRule(style=Style(preset="traffic_light"))] css, _ = engine.apply_format(rules, cell_value=-5) assert css is not None assert css.cls == "mf-formatting-error" def test_i_can_expand_rule_preset_via_style_with_no_match(self): """A rule preset via style() with a non-matching condition returns no style. Why: traffic_light has style("error") only if value < 0. A positive value should produce no style output. """ engine = FormattingEngine() rules = [FormatRule(style=Style(preset="traffic_light"))] css, _ = engine.apply_format(rules, cell_value=10) assert css is not None assert css.cls == "mf-formatting-success"