Files
textual/tests/css/test_stylesheet.py
Will McGugan 38140c564b imports
2023-02-07 10:54:29 +00:00

250 lines
8.3 KiB
Python

from contextlib import nullcontext as does_not_raise
from typing import Any
import pytest
from textual.color import Color
from textual.css.stylesheet import CssSource, Stylesheet, StylesheetParseError
from textual.css.tokenizer import TokenError
from textual.dom import DOMNode
from textual.geometry import Spacing
from textual.widget import Widget
def _make_user_stylesheet(css: str) -> Stylesheet:
stylesheet = Stylesheet()
stylesheet.source["test.css"] = CssSource(css, is_defaults=False)
stylesheet.parse()
return stylesheet
def test_stylesheet_apply_highest_specificity_wins():
"""#ids have higher specificity than .classes"""
css = "#id {color: red;} .class {color: blue;}"
stylesheet = _make_user_stylesheet(css)
node = DOMNode(classes="class", id="id")
stylesheet.apply(node)
assert node.styles.color == Color(255, 0, 0)
def test_stylesheet_apply_doesnt_override_defaults():
css = "#id {color: red;}"
stylesheet = _make_user_stylesheet(css)
node = DOMNode(id="id")
stylesheet.apply(node)
assert node.styles.margin == Spacing.all(0)
assert node.styles.box_sizing == "border-box"
def test_stylesheet_apply_highest_specificity_wins_multiple_classes():
"""When we use two selectors containing only classes, then the selector
`.b.c` has greater specificity than the selector `.a`"""
css = ".b.c {background: blue;} .a {background: red; color: lime;}"
stylesheet = _make_user_stylesheet(css)
node = DOMNode(classes="a b c")
stylesheet.apply(node)
assert node.styles.background == Color(0, 0, 255)
assert node.styles.color == Color(0, 255, 0)
def test_stylesheet_many_classes_dont_overrule_id():
"""#id is further to the left in the specificity tuple than class, and
a selector containing multiple classes cannot take priority over even a
single class."""
css = "#id {color: red;} .a.b.c.d {color: blue;}"
stylesheet = _make_user_stylesheet(css)
node = DOMNode(classes="a b c d", id="id")
stylesheet.apply(node)
assert node.styles.color == Color(255, 0, 0)
def test_stylesheet_last_rule_wins_when_same_rule_twice_in_one_ruleset():
css = "#id {color: red; color: blue;}"
stylesheet = _make_user_stylesheet(css)
node = DOMNode(id="id")
stylesheet.apply(node)
assert node.styles.color == Color(0, 0, 255)
def test_stylesheet_rulesets_merged_for_duplicate_selectors():
css = "#id {color: red; background: lime;} #id {color:blue;}"
stylesheet = _make_user_stylesheet(css)
node = DOMNode(id="id")
stylesheet.apply(node)
assert node.styles.color == Color(0, 0, 255)
assert node.styles.background == Color(0, 255, 0)
def test_stylesheet_apply_takes_final_rule_in_specificity_clash():
""".a and .b both contain background and have same specificity, so .b wins
since it was declared last - the background should be blue."""
css = ".a {background: red; color: lime;} .b {background: blue;}"
stylesheet = _make_user_stylesheet(css)
node = DOMNode(classes="a b", id="c")
stylesheet.apply(node)
assert node.styles.color == Color(0, 255, 0) # color: lime
assert node.styles.background == Color(0, 0, 255) # background: blue
def test_stylesheet_apply_empty_rulesets():
"""Ensure that we don't crash when working with empty rulesets"""
css = ".a {} .b {}"
stylesheet = _make_user_stylesheet(css)
node = DOMNode(classes="a b")
stylesheet.apply(node)
def test_stylesheet_apply_user_css_over_widget_css():
user_css = ".a {color: red; tint: yellow;}"
class MyWidget(Widget):
DEFAULT_CSS = ".a {color: blue !important; background: lime;}"
node = MyWidget()
node.add_class("a")
stylesheet = _make_user_stylesheet(user_css)
stylesheet.add_source(
MyWidget.DEFAULT_CSS, "widget.py:MyWidget", is_default_css=True
)
stylesheet.apply(node)
# The node is red because user CSS overrides Widget.DEFAULT_CSS
assert node.styles.color == Color(255, 0, 0)
# The background colour defined in the Widget still applies, since user CSS doesn't override it
assert node.styles.background == Color(0, 255, 0)
# As expected, the tint colour is yellow, since there's no competition between user or widget CSS
assert node.styles.tint == Color(255, 255, 0)
@pytest.mark.parametrize(
"css_value,expectation,expected_color",
[
# Valid values:
["transparent", does_not_raise(), Color(0, 0, 0, 0)],
["ansi_red", does_not_raise(), Color(128, 0, 0)],
["ansi_bright_magenta", does_not_raise(), Color(255, 0, 255)],
["red", does_not_raise(), Color(255, 0, 0)],
["lime", does_not_raise(), Color(0, 255, 0)],
["coral", does_not_raise(), Color(255, 127, 80)],
["aqua", does_not_raise(), Color(0, 255, 255)],
["deepskyblue", does_not_raise(), Color(0, 191, 255)],
["rebeccapurple", does_not_raise(), Color(102, 51, 153)],
["#ffcc00", does_not_raise(), Color(255, 204, 0)],
["#ffcc0033", does_not_raise(), Color(255, 204, 0, 0.2)],
["rgb(200,90,30)", does_not_raise(), Color(200, 90, 30)],
["rgba(200,90,30,0.3)", does_not_raise(), Color(200, 90, 30, 0.3)],
# Some invalid ones:
["coffee", pytest.raises(StylesheetParseError), None], # invalid color name
["ansi_dark_cyan", pytest.raises(StylesheetParseError), None],
["red 4", pytest.raises(StylesheetParseError), None], # space in it
["1", pytest.raises(StylesheetParseError), None], # invalid value
["()", pytest.raises(TokenError), None], # invalid tokens
],
)
def test_color_property_parsing(css_value, expectation, expected_color):
stylesheet = Stylesheet()
css = """
* {
background: ${COLOR};
}
""".replace(
"${COLOR}", css_value
)
with expectation:
stylesheet.add_source(css)
stylesheet.parse()
if expected_color:
css_rule = stylesheet.rules[0]
assert css_rule.styles.background == expected_color
@pytest.mark.parametrize(
"css_property_name,expected_property_name_suggestion",
[
["backgroundu", "background"],
["bckgroundu", "background"],
["ofset-x", "offset-x"],
["ofst_y", "offset-y"],
["colr", "color"],
["colour", "color"],
["wdth", "width"],
["wth", "width"],
["wh", None],
["xkcd", None],
],
)
def test_did_you_mean_for_css_property_names(
css_property_name: str, expected_property_name_suggestion
):
stylesheet = Stylesheet()
css = """
* {
border: blue;
${PROPERTY}: red;
}
""".replace(
"${PROPERTY}", css_property_name
)
stylesheet.add_source(css)
with pytest.raises(StylesheetParseError) as err:
stylesheet.parse()
_, help_text = err.value.errors.rules[0].errors[0] # type: Any, HelpText
displayed_css_property_name = css_property_name.replace("_", "-")
expected_summary = f"Invalid CSS property {displayed_css_property_name!r}"
if expected_property_name_suggestion:
expected_summary += f'. Did you mean "{expected_property_name_suggestion}"?'
assert help_text.summary == expected_summary
@pytest.mark.parametrize(
"css_property_name,css_property_value,expected_color_suggestion",
[
["color", "blu", "blue"],
["background", "chartruse", "chartreuse"],
["tint", "ansi_whi", "ansi_white"],
["scrollbar-color", "transprnt", "transparent"],
["color", "xkcd", None],
],
)
def test_did_you_mean_for_color_names(
css_property_name: str, css_property_value: str, expected_color_suggestion
):
stylesheet = Stylesheet()
css = """
* {
border: blue;
${PROPERTY}: ${VALUE};
}
""".replace(
"${PROPERTY}", css_property_name
).replace(
"${VALUE}", css_property_value
)
stylesheet.add_source(css)
with pytest.raises(StylesheetParseError) as err:
stylesheet.parse()
_, help_text = err.value.errors.rules[0].errors[0] # type: Any, HelpText
displayed_css_property_name = css_property_name.replace("_", "-")
expected_error_summary = (
f"Invalid value for the [i]{displayed_css_property_name}[/] property"
)
if expected_color_suggestion is not None:
expected_error_summary += f'. Did you mean "{expected_color_suggestion}"?'
assert help_text.summary == expected_error_summary