Files
textual/tests/css/test_styles.py
Olivier Philippon 159d52b0f3 [css] Fix a bug with our now-flexible scrollbars sizes
Also add test cases for it in order to prevent potential regressions on this
2022-05-25 16:33:19 +01:00

282 lines
9.3 KiB
Python

import sys
from decimal import Decimal
if sys.version_info >= (3, 10):
from typing import Literal
else: # pragma: no cover
from typing_extensions import Literal
import pytest
from rich.style import Style
from textual.app import ComposeResult
from textual.color import Color
from textual.css.errors import StyleValueError
from textual.css.scalar import Scalar, Unit
from textual.css.styles import Styles, RenderStyles
from textual.dom import DOMNode
from textual.widget import Widget
from tests.utilities.test_app import AppTest
def test_styles_reset():
styles = Styles()
styles.text_style = "not bold"
assert styles.text_style == Style(bold=False)
styles.reset()
assert styles.text_style is Style.null()
def test_has_rule():
styles = Styles()
assert not styles.has_rule("text_style")
styles.text_style = "bold"
assert styles.has_rule("text_style")
styles.text_style = None
assert not styles.has_rule("text_style")
def test_clear_rule():
styles = Styles()
styles.text_style = "bold"
assert styles.has_rule("text_style")
styles.clear_rule("text_style")
assert not styles.has_rule("text_style")
def test_get_rules():
styles = Styles()
# Empty rules at start
assert styles.get_rules() == {}
styles.text_style = "bold"
assert styles.get_rules() == {"text_style": Style.parse("bold")}
styles.display = "none"
assert styles.get_rules() == {
"text_style": Style.parse("bold"),
"display": "none",
}
def test_set_rule():
styles = Styles()
assert styles.get_rules() == {}
styles.set_rule("text_style", Style.parse("bold"))
assert styles.get_rules() == {"text_style": Style.parse("bold")}
def test_reset():
styles = Styles()
assert styles.get_rules() == {}
styles.set_rule("text_style", Style.parse("bold"))
assert styles.get_rules() == {"text_style": Style.parse("bold")}
styles.reset()
assert styles.get_rules() == {}
def test_merge():
styles = Styles()
styles.set_rule("text_style", Style.parse("bold"))
styles2 = Styles()
styles2.set_rule("display", "none")
styles.merge(styles2)
assert styles.get_rules() == {
"text_style": Style.parse("bold"),
"display": "none",
}
def test_merge_rules():
styles = Styles()
styles.set_rule("text_style", Style.parse("bold"))
styles.merge_rules({"display": "none"})
assert styles.get_rules() == {
"text_style": Style.parse("bold"),
"display": "none",
}
def test_render_styles_border():
base = Styles()
inline = Styles()
styles_view = RenderStyles(None, base, inline)
base.border_top = ("heavy", "red")
# Base has border-top: heavy red
assert styles_view.border_top == ("heavy", Color.parse("red"))
inline.border_left = ("rounded", "green")
# Base has border-top heavy red, inline has border-left: rounded green
assert styles_view.border_top == ("heavy", Color.parse("red"))
assert styles_view.border_left == ("rounded", Color.parse("green"))
assert styles_view.border == (
("heavy", Color.parse("red")),
("", Color(0, 255, 0)),
("", Color(0, 255, 0)),
("rounded", Color.parse("green")),
)
def test_get_opacity_default():
styles = RenderStyles(DOMNode(), Styles(), Styles())
assert styles.opacity == 1.0
@pytest.mark.parametrize(
"set_value, expected",
[
[0.2, 0.2],
[-0.4, 0.0],
[5.8, 1.0],
["25%", 0.25],
["-10%", 0.0],
["120%", 1.0],
],
)
def test_opacity_set_then_get(set_value, expected):
styles = RenderStyles(DOMNode(), Styles(), Styles())
styles.opacity = set_value
assert styles.opacity == expected
def test_opacity_set_invalid_type_error():
styles = RenderStyles(DOMNode(), Styles(), Styles())
with pytest.raises(StyleValueError):
styles.opacity = "invalid value"
@pytest.mark.parametrize(
"size_dimension_input,size_dimension_expected_output",
[
# fmt: off
[None, None],
[1, Scalar(1, Unit.CELLS, Unit.WIDTH)],
[1.0, Scalar(1.0, Unit.CELLS, Unit.WIDTH)],
[1.2, Scalar(1.2, Unit.CELLS, Unit.WIDTH)],
[1.2e3, Scalar(1200.0, Unit.CELLS, Unit.WIDTH)],
["20", Scalar(20, Unit.CELLS, Unit.WIDTH)],
["1.4", Scalar(1.4, Unit.CELLS, Unit.WIDTH)],
[Scalar(100, Unit.CELLS, Unit.WIDTH), Scalar(100, Unit.CELLS, Unit.WIDTH)],
[Scalar(10.3, Unit.CELLS, Unit.WIDTH), Scalar(10.3, Unit.CELLS, Unit.WIDTH)],
[Scalar(10.4, Unit.CELLS, Unit.HEIGHT), Scalar(10.4, Unit.CELLS, Unit.HEIGHT)],
[Scalar(10.5, Unit.PERCENT, Unit.WIDTH), Scalar(10.5, Unit.WIDTH, Unit.WIDTH)],
[Scalar(10.6, Unit.PERCENT, Unit.PERCENT), Scalar(10.6, Unit.WIDTH, Unit.WIDTH)],
[Scalar(10.7, Unit.HEIGHT, Unit.PERCENT), Scalar(10.7, Unit.HEIGHT, Unit.PERCENT)],
# percentage values are normalised to floats and get the WIDTH "percent_unit":
[Scalar(11, Unit.PERCENT, Unit.HEIGHT), Scalar(11.0, Unit.WIDTH, Unit.WIDTH)],
# fmt: on
],
)
def test_widget_style_size_can_accept_various_data_types_and_normalize_them(
size_dimension_input, size_dimension_expected_output
):
widget = Widget()
widget.styles.width = size_dimension_input
assert widget.styles.width == size_dimension_expected_output
@pytest.mark.parametrize(
"size_dimension_input",
[
"a",
"1.4e3",
3.14j,
Decimal("3.14"),
list(),
tuple(),
dict(),
],
)
def test_widget_style_size_fails_if_data_type_is_not_supported(size_dimension_input):
widget = Widget()
with pytest.raises(StyleValueError):
widget.styles.width = size_dimension_input
@pytest.mark.asyncio
@pytest.mark.parametrize(
"overflow_y,scrollbar_gutter,scrollbar_size,text_length,expected_text_widget_width,expects_vertical_scrollbar",
(
# ------------------------------------------------
# ----- Let's start with `overflow-y: auto`:
# short text: full width, no scrollbar
["auto", "auto", 1, "short_text", 80, False],
# long text: reduced width, scrollbar
["auto", "auto", 1, "long_text", 79, True],
# short text, `scrollbar-gutter: stable`: reduced width, no scrollbar
["auto", "stable", 1, "short_text", 79, False],
# long text, `scrollbar-gutter: stable`: reduced width, scrollbar
["auto", "stable", 1, "long_text", 79, True],
# ------------------------------------------------
# ----- And now let's see the behaviour with `overflow-y: scroll`:
# short text: reduced width, scrollbar
["scroll", "auto", 1, "short_text", 79, True],
# long text: reduced width, scrollbar
["scroll", "auto", 1, "long_text", 79, True],
# short text, `scrollbar-gutter: stable`: reduced width, scrollbar
["scroll", "stable", 1, "short_text", 79, True],
# long text, `scrollbar-gutter: stable`: reduced width, scrollbar
["scroll", "stable", 1, "long_text", 79, True],
# ------------------------------------------------
# ----- Finally, let's check the behaviour with `overflow-y: hidden`:
# short text: full width, no scrollbar
["hidden", "auto", 1, "short_text", 80, False],
# long text: full width, no scrollbar
["hidden", "auto", 1, "long_text", 80, False],
# short text, `scrollbar-gutter: stable`: reduced width, no scrollbar
["hidden", "stable", 1, "short_text", 79, False],
# long text, `scrollbar-gutter: stable`: reduced width, no scrollbar
["hidden", "stable", 1, "long_text", 79, False],
# ------------------------------------------------
# ----- Bonus round with a custom scrollbar size, now that we can set this:
["auto", "auto", 3, "short_text", 80, False],
["auto", "auto", 3, "long_text", 77, True],
["scroll", "auto", 3, "short_text", 77, True],
["scroll", "stable", 3, "short_text", 77, True],
["hidden", "auto", 3, "long_text", 80, False],
["hidden", "stable", 3, "short_text", 77, False],
),
)
async def test_scrollbar_gutter(
overflow_y: str,
scrollbar_gutter: str,
scrollbar_size: int,
text_length: Literal["short_text", "long_text"],
expected_text_widget_width: int,
expects_vertical_scrollbar: bool,
):
from rich.text import Text
from textual.geometry import Size
class TextWidget(Widget):
def render(self, styles) -> Text:
text_multiplier = 10 if text_length == "long_text" else 2
return Text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit liber a a a."
* text_multiplier
)
container = Widget()
container.styles.height = 3
container.styles.overflow_y = overflow_y
container.styles.scrollbar_gutter = scrollbar_gutter
if scrollbar_size > 1:
container.styles.scrollbar_size_vertical = scrollbar_size
text_widget = TextWidget()
text_widget.styles.height = "auto"
container.add_child(text_widget)
class MyTestApp(AppTest):
def compose(self) -> ComposeResult:
yield container
app = MyTestApp(test_name="scrollbar_gutter", size=Size(80, 10))
await app.boot_and_shutdown()
assert text_widget.size.width == expected_text_widget_width
assert container.scrollbars_enabled[0] is expects_vertical_scrollbar