mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
250 lines
8.3 KiB
Python
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
|