from __future__ import annotations import pytest from textual.color import Color from textual.css.errors import UnresolvedVariableError from textual.css.parse import substitute_references from textual.css.scalar import Scalar, Unit from textual.css.stylesheet import Stylesheet, StylesheetParseError from textual.css.tokenize import tokenize from textual.css.tokenizer import ReferencedBy, Token, TokenError from textual.css.transition import Transition from textual.geometry import Spacing from textual.layouts.vertical import VerticalLayout class TestVariableReferenceSubstitution: def test_simple_reference(self): css = "$x: 1; #some-widget{border: $x;}" variables = substitute_references(tokenize(css, "")) assert list(variables) == [ Token( name="variable_name", value="$x:", path="", code=css, location=(0, 0), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(0, 3), referenced_by=None, ), Token( name="number", value="1", path="", code=css, location=(0, 4), referenced_by=None, ), Token( name="variable_value_end", value=";", path="", code=css, location=(0, 5), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(0, 6), referenced_by=None, ), Token( name="selector_start_id", value="#some-widget", path="", code=css, location=(0, 7), referenced_by=None, ), Token( name="declaration_set_start", value="{", path="", code=css, location=(0, 19), referenced_by=None, ), Token( name="declaration_name", value="border:", path="", code=css, location=(0, 20), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(0, 27), referenced_by=None, ), Token( name="number", value="1", path="", code=css, location=(0, 4), referenced_by=ReferencedBy( name="x", location=(0, 28), length=2, code=css ), ), Token( name="declaration_end", value=";", path="", code=css, location=(0, 30), referenced_by=None, ), Token( name="declaration_set_end", value="}", path="", code=css, location=(0, 31), referenced_by=None, ), ] def test_simple_reference_no_whitespace(self): css = "$x:1; #some-widget{border: $x;}" variables = substitute_references(tokenize(css, "")) assert list(variables) == [ Token( name="variable_name", value="$x:", path="", code=css, location=(0, 0), referenced_by=None, ), Token( name="number", value="1", path="", code=css, location=(0, 3), referenced_by=None, ), Token( name="variable_value_end", value=";", path="", code=css, location=(0, 4), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(0, 5), referenced_by=None, ), Token( name="selector_start_id", value="#some-widget", path="", code=css, location=(0, 6), referenced_by=None, ), Token( name="declaration_set_start", value="{", path="", code=css, location=(0, 18), referenced_by=None, ), Token( name="declaration_name", value="border:", path="", code=css, location=(0, 19), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(0, 26), referenced_by=None, ), Token( name="number", value="1", path="", code=css, location=(0, 3), referenced_by=ReferencedBy( name="x", location=(0, 27), length=2, code=css ), ), Token( name="declaration_end", value=";", path="", code=css, location=(0, 29), referenced_by=None, ), Token( name="declaration_set_end", value="}", path="", code=css, location=(0, 30), referenced_by=None, ), ] def test_undefined_variable(self): css = ".thing { border: $not-defined; }" with pytest.raises(UnresolvedVariableError): list(substitute_references(tokenize(css, ""))) def test_transitive_reference(self): css = "$x: 1\n$y: $x\n.thing { border: $y }" assert list(substitute_references(tokenize(css, ""))) == [ Token( name="variable_name", value="$x:", path="", code=css, location=(0, 0), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(0, 3), referenced_by=None, ), Token( name="number", value="1", path="", code=css, location=(0, 4), referenced_by=None, ), Token( name="variable_value_end", value="\n", path="", code=css, location=(0, 5), referenced_by=None, ), Token( name="variable_name", value="$y:", path="", code=css, location=(1, 0), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(1, 3), referenced_by=None, ), Token( name="number", value="1", path="", code=css, location=(0, 4), referenced_by=ReferencedBy( name="x", location=(1, 4), length=2, code=css ), ), Token( name="variable_value_end", value="\n", path="", code=css, location=(1, 6), referenced_by=None, ), Token( name="selector_start_class", value=".thing", path="", code=css, location=(2, 0), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(2, 6), referenced_by=None, ), Token( name="declaration_set_start", value="{", path="", code=css, location=(2, 7), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(2, 8), referenced_by=None, ), Token( name="declaration_name", value="border:", path="", code=css, location=(2, 9), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(2, 16), referenced_by=None, ), Token( name="number", value="1", path="", code=css, location=(0, 4), referenced_by=ReferencedBy( name="y", location=(2, 17), length=2, code=css ), ), Token( name="whitespace", value=" ", path="", code=css, location=(2, 19), referenced_by=None, ), Token( name="declaration_set_end", value="}", path="", code=css, location=(2, 20), referenced_by=None, ), ] def test_multi_value_variable(self): css = "$x: 2 4\n$y: 6 $x 2\n.thing { border: $y }" assert list(substitute_references(tokenize(css, ""))) == [ Token( name="variable_name", value="$x:", path="", code=css, location=(0, 0), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(0, 3), referenced_by=None, ), Token( name="number", value="2", path="", code=css, location=(0, 4), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(0, 5), referenced_by=None, ), Token( name="number", value="4", path="", code=css, location=(0, 6), referenced_by=None, ), Token( name="variable_value_end", value="\n", path="", code=css, location=(0, 7), referenced_by=None, ), Token( name="variable_name", value="$y:", path="", code=css, location=(1, 0), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(1, 3), referenced_by=None, ), Token( name="number", value="6", path="", code=css, location=(1, 4), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(1, 5), referenced_by=None, ), Token( name="number", value="2", path="", code=css, location=(0, 4), referenced_by=ReferencedBy( name="x", location=(1, 6), length=2, code=css ), ), Token( name="whitespace", value=" ", path="", code=css, location=(0, 5), referenced_by=ReferencedBy( name="x", location=(1, 6), length=2, code=css ), ), Token( name="number", value="4", path="", code=css, location=(0, 6), referenced_by=ReferencedBy( name="x", location=(1, 6), length=2, code=css ), ), Token( name="whitespace", value=" ", path="", code=css, location=(1, 8), referenced_by=None, ), Token( name="number", value="2", path="", code=css, location=(1, 9), referenced_by=None, ), Token( name="variable_value_end", value="\n", path="", code=css, location=(1, 10), referenced_by=None, ), Token( name="selector_start_class", value=".thing", path="", code=css, location=(2, 0), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(2, 6), referenced_by=None, ), Token( name="declaration_set_start", value="{", path="", code=css, location=(2, 7), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(2, 8), referenced_by=None, ), Token( name="declaration_name", value="border:", path="", code=css, location=(2, 9), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(2, 16), referenced_by=None, ), Token( name="number", value="6", path="", code=css, location=(1, 4), referenced_by=ReferencedBy( name="y", location=(2, 17), length=2, code=css ), ), Token( name="whitespace", value=" ", path="", code=css, location=(1, 5), referenced_by=ReferencedBy( name="y", location=(2, 17), length=2, code=css ), ), Token( name="number", value="2", path="", code=css, location=(0, 4), referenced_by=ReferencedBy( name="y", location=(2, 17), length=2, code=css ), ), Token( name="whitespace", value=" ", path="", code=css, location=(0, 5), referenced_by=ReferencedBy( name="y", location=(2, 17), length=2, code=css ), ), Token( name="number", value="4", path="", code=css, location=(0, 6), referenced_by=ReferencedBy( name="y", location=(2, 17), length=2, code=css ), ), Token( name="whitespace", value=" ", path="", code=css, location=(1, 8), referenced_by=ReferencedBy( name="y", location=(2, 17), length=2, code=css ), ), Token( name="number", value="2", path="", code=css, location=(1, 9), referenced_by=ReferencedBy( name="y", location=(2, 17), length=2, code=css ), ), Token( name="whitespace", value=" ", path="", code=css, location=(2, 19), referenced_by=None, ), Token( name="declaration_set_end", value="}", path="", code=css, location=(2, 20), referenced_by=None, ), ] def test_variable_used_inside_property_value(self): css = "$x: red\n.thing { border: on $x; }" assert list(substitute_references(tokenize(css, ""))) == [ Token( name="variable_name", value="$x:", path="", code=css, location=(0, 0), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(0, 3), referenced_by=None, ), Token( name="token", value="red", path="", code=css, location=(0, 4), referenced_by=None, ), Token( name="variable_value_end", value="\n", path="", code=css, location=(0, 7), referenced_by=None, ), Token( name="selector_start_class", value=".thing", path="", code=css, location=(1, 0), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(1, 6), referenced_by=None, ), Token( name="declaration_set_start", value="{", path="", code=css, location=(1, 7), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(1, 8), referenced_by=None, ), Token( name="declaration_name", value="border:", path="", code=css, location=(1, 9), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(1, 16), referenced_by=None, ), Token( name="token", value="on", path="", code=css, location=(1, 17), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(1, 19), referenced_by=None, ), Token( name="token", value="red", path="", code=css, location=(0, 4), referenced_by=ReferencedBy( name="x", location=(1, 20), length=2, code=css ), ), Token( name="declaration_end", value=";", path="", code=css, location=(1, 22), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(1, 23), referenced_by=None, ), Token( name="declaration_set_end", value="}", path="", code=css, location=(1, 24), referenced_by=None, ), ] def test_variable_definition_eof(self): css = "$x: 1" assert list(substitute_references(tokenize(css, ""))) == [ Token( name="variable_name", value="$x:", path="", code=css, location=(0, 0), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(0, 3), referenced_by=None, ), Token( name="number", value="1", path="", code=css, location=(0, 4), referenced_by=None, ), ] def test_variable_reference_whitespace_trimming(self): css = "$x: 123;.thing{border: $x}" assert list(substitute_references(tokenize(css, ""))) == [ Token( name="variable_name", value="$x:", path="", code=css, location=(0, 0), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(0, 3), referenced_by=None, ), Token( name="number", value="123", path="", code=css, location=(0, 7), referenced_by=None, ), Token( name="variable_value_end", value=";", path="", code=css, location=(0, 10), referenced_by=None, ), Token( name="selector_start_class", value=".thing", path="", code=css, location=(0, 11), referenced_by=None, ), Token( name="declaration_set_start", value="{", path="", code=css, location=(0, 17), referenced_by=None, ), Token( name="declaration_name", value="border:", path="", code=css, location=(0, 18), referenced_by=None, ), Token( name="whitespace", value=" ", path="", code=css, location=(0, 25), referenced_by=None, ), Token( name="number", value="123", path="", code=css, location=(0, 7), referenced_by=ReferencedBy( name="x", location=(0, 26), length=2, code=css ), ), Token( name="declaration_set_end", value="}", path="", code=css, location=(0, 28), referenced_by=None, ), ] class TestParseLayout: def test_valid_layout_name(self): css = "#some-widget { layout: vertical; }" stylesheet = Stylesheet() stylesheet.add_source(css) styles = stylesheet.rules[0].styles assert isinstance(styles.layout, VerticalLayout) def test_invalid_layout_name(self): css = "#some-widget { layout: invalidlayout; }" stylesheet = Stylesheet() with pytest.raises(StylesheetParseError) as ex: stylesheet.add_source(css) stylesheet.parse() assert ex.value.errors is not None class TestParseText: def test_foreground(self): css = """#some-widget { color: green; } """ stylesheet = Stylesheet() stylesheet.add_source(css) styles = stylesheet.rules[0].styles assert styles.color == Color.parse("green") def test_background(self): css = """#some-widget { background: red; } """ stylesheet = Stylesheet() stylesheet.add_source(css) styles = stylesheet.rules[0].styles assert styles.background == Color.parse("red") class TestParseColor: """More in-depth tests around parsing of CSS colors""" @pytest.mark.parametrize( "value,result", [ ("rgb(1,255,50)", Color(1, 255, 50)), ("rgb( 1, 255,50 )", Color(1, 255, 50)), ("rgba( 1, 255,50,0.3 )", Color(1, 255, 50, 0.3)), ("rgba( 1, 255,50, 1.3 )", Color(1, 255, 50, 1.0)), ("hsl( 180, 50%, 50% )", Color(64, 191, 191)), ("hsl(180,50%,50%)", Color(64, 191, 191)), ("hsla(180,50%,50%,0.25)", Color(64, 191, 191, 0.25)), ("hsla( 180, 50% ,50%,0.25 )", Color(64, 191, 191, 0.25)), ("hsla( 180, 50% , 50% , 1.5 )", Color(64, 191, 191)), ], ) def test_rgb_and_hsl(self, value, result): css = f""".box {{ color: {value}; }} """ stylesheet = Stylesheet() stylesheet.add_source(css) styles = stylesheet.rules[0].styles assert styles.color == result class TestParseOffset: @pytest.mark.parametrize( "offset_x, parsed_x, offset_y, parsed_y", [ [ "-5.5%", Scalar(-5.5, Unit.PERCENT, Unit.WIDTH), "-30%", Scalar(-30, Unit.PERCENT, Unit.HEIGHT), ], [ "5%", Scalar(5, Unit.PERCENT, Unit.WIDTH), "40%", Scalar(40, Unit.PERCENT, Unit.HEIGHT), ], [ "10", Scalar(10, Unit.CELLS, Unit.WIDTH), "40", Scalar(40, Unit.CELLS, Unit.HEIGHT), ], ], ) def test_composite_rule(self, offset_x, parsed_x, offset_y, parsed_y): css = f"""#some-widget {{ offset: {offset_x} {offset_y}; }} """ stylesheet = Stylesheet() stylesheet.add_source(css) styles = stylesheet.rules[0].styles assert len(stylesheet.rules) == 1 assert stylesheet.rules[0].errors == [] assert styles.offset.x == parsed_x assert styles.offset.y == parsed_y @pytest.mark.parametrize( "offset_x, parsed_x, offset_y, parsed_y", [ [ "-5.5%", Scalar(-5.5, Unit.PERCENT, Unit.WIDTH), "-30%", Scalar(-30, Unit.PERCENT, Unit.HEIGHT), ], [ "5%", Scalar(5, Unit.PERCENT, Unit.WIDTH), "40%", Scalar(40, Unit.PERCENT, Unit.HEIGHT), ], [ "-10", Scalar(-10, Unit.CELLS, Unit.WIDTH), "40", Scalar(40, Unit.CELLS, Unit.HEIGHT), ], ], ) def test_separate_rules(self, offset_x, parsed_x, offset_y, parsed_y): css = f"""#some-widget {{ offset-x: {offset_x}; offset-y: {offset_y}; }} """ stylesheet = Stylesheet() stylesheet.add_source(css) styles = stylesheet.rules[0].styles assert len(stylesheet.rules) == 1 assert stylesheet.rules[0].errors == [] assert styles.offset.x == parsed_x assert styles.offset.y == parsed_y class TestParseOverflow: def test_multiple_enum(self): css = "#some-widget { overflow: hidden auto; }" stylesheet = Stylesheet() stylesheet.add_source(css) styles = stylesheet.rules[0].styles assert len(stylesheet.rules) == 1 assert styles.overflow_x == "hidden" assert styles.overflow_y == "auto" class TestParseTransition: @pytest.mark.parametrize( "duration, parsed_duration", [ ["5.57s", 5.57], ["0.5s", 0.5], ["1200ms", 1.2], ["0.5ms", 0.0005], ["20", 20.0], ["0.1", 0.1], ], ) def test_various_duration_formats(self, duration, parsed_duration): easing = "in_out_cubic" transition_property = "offset" delay = duration css = f"""#some-widget {{ transition: {transition_property} {duration} {easing} {delay}; }} """ stylesheet = Stylesheet() stylesheet.add_source(css) styles = stylesheet.rules[0].styles assert len(stylesheet.rules) == 1 assert stylesheet.rules[0].errors == [] assert styles.transitions == { "offset": Transition( duration=parsed_duration, easing=easing, delay=parsed_duration ) } def test_no_delay_specified(self): css = f"#some-widget {{ transition: offset-x 1 in_out_cubic; }}" stylesheet = Stylesheet() stylesheet.add_source(css) styles = stylesheet.rules[0].styles assert stylesheet.rules[0].errors == [] assert styles.transitions == { "offset-x": Transition(duration=1, easing="in_out_cubic", delay=0) } def test_unknown_easing_function(self): invalid_func_name = "invalid_easing_function" css = f"#some-widget {{ transition: offset 1 {invalid_func_name} 1; }}" stylesheet = Stylesheet() with pytest.raises(StylesheetParseError) as ex: stylesheet.add_source(css) stylesheet.parse() rules = stylesheet._parse_rules(css, "foo") stylesheet_errors = rules[0].errors assert len(stylesheet_errors) == 1 assert stylesheet_errors[0][0].value == invalid_func_name assert ex.value.errors is not None class TestParseOpacity: @pytest.mark.parametrize( "css_value, styles_value", [ ["-0.2", 0.0], ["0.4", 0.4], ["1.3", 1.0], ["-20%", 0.0], ["25%", 0.25], ["128%", 1.0], ], ) def test_opacity_to_styles(self, css_value, styles_value): css = f"#some-widget {{ text-opacity: {css_value} }}" stylesheet = Stylesheet() stylesheet.add_source(css) assert stylesheet.rules[0].styles.text_opacity == styles_value assert not stylesheet.rules[0].errors def test_opacity_invalid_value(self): css = "#some-widget { text-opacity: 123x }" stylesheet = Stylesheet() with pytest.raises(StylesheetParseError): stylesheet.add_source(css) stylesheet.parse() rules = stylesheet._parse_rules(css, "foo") assert rules[0].errors class TestParseMargin: def test_margin_partial(self): css = "#foo {margin: 1; margin-top: 2; margin-right: 3; margin-bottom: -1;}" stylesheet = Stylesheet() stylesheet.add_source(css) assert stylesheet.rules[0].styles.margin == Spacing(2, 3, -1, 1) class TestParsePadding: def test_padding_partial(self): css = "#foo {padding: 1; padding-top: 2; padding-right: 3; padding-bottom: -1;}" stylesheet = Stylesheet() stylesheet.add_source(css) assert stylesheet.rules[0].styles.padding == Spacing(2, 3, -1, 1) class TestParseTextAlign: @pytest.mark.parametrize( "valid_align", ["left", "start", "center", "right", "end", "justify"] ) def test_text_align(self, valid_align): css = f"#foo {{ text-align: {valid_align} }}" stylesheet = Stylesheet() stylesheet.add_source(css) assert stylesheet.rules[0].styles.text_align == valid_align def test_text_align_invalid(self): css = "#foo { text-align: invalid-value; }" stylesheet = Stylesheet() with pytest.raises(StylesheetParseError): stylesheet.add_source(css) stylesheet.parse() rules = stylesheet._parse_rules(css, "foo") assert rules[0].errors def test_text_align_empty_uses_default(self): css = "#foo { text-align: ; }" stylesheet = Stylesheet() stylesheet.add_source(css) assert stylesheet.rules[0].styles.text_align == "start" class TestTypeNames: def test_type_no_number(self): stylesheet = Stylesheet() stylesheet.add_source("TestType {}") assert len(stylesheet.rules) == 1 def test_type_with_number(self): stylesheet = Stylesheet() stylesheet.add_source("TestType1 {}") assert len(stylesheet.rules) == 1 def test_type_starts_with_number(self): stylesheet = Stylesheet() stylesheet.add_source("1TestType {}") with pytest.raises(TokenError): stylesheet.parse() def test_combined_type_no_number(self): for separator in " >,": stylesheet = Stylesheet() stylesheet.add_source(f"StartType {separator} TestType {{}}") assert len(stylesheet.rules) == 1 def test_combined_type_with_number(self): for separator in " >,": stylesheet = Stylesheet() stylesheet.add_source(f"StartType {separator} TestType1 {{}}") assert len(stylesheet.rules) == 1 def test_combined_type_starts_with_number(self): for separator in " >,": stylesheet = Stylesheet() stylesheet.add_source(f"StartType {separator} 1TestType {{}}") with pytest.raises(TokenError): stylesheet.parse()