[css] Add a test for the impact of our border edge types on the layout

This commit is contained in:
Olivier Philippon
2022-05-18 11:40:53 +01:00
parent b135fa784b
commit af2f1580ce
6 changed files with 152 additions and 54 deletions

View File

@@ -17,7 +17,7 @@ class VerticalContainer(Widget):
VerticalContainer Placeholder {
margin: 1 0;
height: 5;
height: auto;
align: center top;
}
"""
@@ -37,26 +37,28 @@ class Introduction(Widget):
return Text("Here are the color edge types we support.", justify="center")
class BorderDemo(Widget):
def __init__(self, name: str):
super().__init__(name=name)
def render(self, style) -> RenderableType:
return Text(self.name, style="black on yellow", justify="center")
class MyTestApp(App):
def compose(self) -> ComposeResult:
placeholders = []
border_demo_widgets = []
for border_edge_type in EdgeType.__args__:
border_placeholder = Placeholder(
id=f"placeholder_{border_edge_type}",
title=(border_edge_type or " ").upper(),
name=f"border: {border_edge_type} white",
)
border_placeholder.styles.border = (border_edge_type, "white")
placeholders.append(border_placeholder)
border_demo = BorderDemo(f'"border: {border_edge_type} white"')
border_demo.styles.height = "auto"
border_demo.styles.margin = (1, 0)
border_demo.styles.border = (border_edge_type, "white")
border_demo_widgets.append(border_demo)
yield VerticalContainer(Introduction(), *placeholders, id="root")
yield VerticalContainer(Introduction(), *border_demo_widgets, id="root")
def on_mount(self):
self.bind("q", "quit")
self.bind("t", "tree")
def action_tree(self):
self.log(self.tree)
app = MyTestApp()

View File

@@ -57,7 +57,7 @@ BORDER_LOCATIONS: dict[
"wide": ((1, 1, 1), (0, 1, 0), (1, 1, 1)),
}
_INVISIBLE_EDGE_TYPES: tuple[EdgeType, ...] = ("none", "hidden")
INVISIBLE_EDGE_TYPES: tuple[EdgeType, ...] = ("", "none", "hidden")
@lru_cache(maxsize=1024)
@@ -170,10 +170,10 @@ class Border:
width (int): Desired width.
"""
top, right, bottom, left = self._sides
has_left = left not in _INVISIBLE_EDGE_TYPES
has_right = right not in _INVISIBLE_EDGE_TYPES
has_top = top not in _INVISIBLE_EDGE_TYPES
has_bottom = bottom not in _INVISIBLE_EDGE_TYPES
has_left = left not in INVISIBLE_EDGE_TYPES
has_right = right not in INVISIBLE_EDGE_TYPES
has_top = top not in INVISIBLE_EDGE_TYPES
has_bottom = bottom not in INVISIBLE_EDGE_TYPES
if has_top:
lines.pop(0)
@@ -199,10 +199,10 @@ class Border:
outer_style = console.get_style(self.outer_style)
top_style, right_style, bottom_style, left_style = self._styles
has_left = left not in _INVISIBLE_EDGE_TYPES
has_right = right not in _INVISIBLE_EDGE_TYPES
has_top = top not in _INVISIBLE_EDGE_TYPES
has_bottom = bottom not in _INVISIBLE_EDGE_TYPES
has_left = left not in INVISIBLE_EDGE_TYPES
has_right = right not in INVISIBLE_EDGE_TYPES
has_top = top not in INVISIBLE_EDGE_TYPES
has_bottom = bottom not in INVISIBLE_EDGE_TYPES
width = options.max_width - has_left - has_right

View File

@@ -27,6 +27,7 @@ from ._help_text import (
string_enum_help_text,
color_property_help_text,
)
from .._border import INVISIBLE_EDGE_TYPES
from ..color import Color, ColorPair, ColorParseError
from ._error_tools import friendly_list
from .constants import NULL_SPACING, VALID_STYLE_FLAGS
@@ -243,10 +244,10 @@ class Edges(NamedTuple):
"""
(top, _), (right, _), (bottom, _), (left, _) = self
return Spacing(
1 if top else 0,
1 if right else 0,
1 if bottom else 0,
1 if left else 0,
1 if top not in INVISIBLE_EDGE_TYPES else 0,
1 if right not in INVISIBLE_EDGE_TYPES else 0,
1 if bottom not in INVISIBLE_EDGE_TYPES else 0,
1 if left not in INVISIBLE_EDGE_TYPES else 0,
)

View File

@@ -42,7 +42,7 @@ from .scalar import Scalar, ScalarOffset, Unit, ScalarError, ScalarParseError
from .styles import DockGroup, Styles
from .tokenize import Token
from .transition import Transition
from .types import BoxSizing, Edge, Display, Overflow, Visibility
from .types import BoxSizing, Edge, Display, Overflow, Visibility, EdgeType
from ..color import Color, ColorParseError
from .._duration import _duration_as_seconds
from .._easing import EASING
@@ -418,7 +418,7 @@ class StylesBuilder:
process_padding_left = _process_space_partial
def _parse_border(self, name: str, tokens: list[Token]) -> tuple[str, Color]:
border_type = "solid"
border_type: EdgeType = "solid"
border_color = Color(0, 255, 0)
def border_value_error():

View File

@@ -3,9 +3,12 @@ import asyncio
from typing import cast, List
import pytest
from rich.console import RenderableType
from rich.text import Text
from tests.utilities.test_app import AppTest
from textual.app import ComposeResult
from textual.css.types import EdgeType
from textual.geometry import Size
from textual.widget import Widget
from textual.widgets import Placeholder
@@ -31,30 +34,20 @@ PLACEHOLDERS_DEFAULT_H = 3 # the default height for our Placeholder widgets
"expected_placeholders_offset_x",
),
(
[
SCREEN_SIZE,
1,
"border: ;", # #root has no border
"", # no specific placeholder style
# #root's virtual size=screen size
(SCREEN_W, SCREEN_H),
# placeholders width=same than screen :: height=default height
(SCREEN_W, PLACEHOLDERS_DEFAULT_H),
# placeholders should be at offset 0
0,
],
[
# "none" borders still allocate a space for the (invisible) border
SCREEN_SIZE,
1,
"border: none;", # #root has an invisible border
"", # no specific placeholder style
# #root's virtual size is smaller because of its borders
(SCREEN_W - 2, SCREEN_H - 2),
# placeholders width=same than screen, minus 2 borders :: height=default height minus 2 borders
(SCREEN_W - 2, PLACEHOLDERS_DEFAULT_H),
# placeholders should be at offset 1 because of #root's border
1,
*[
[
SCREEN_SIZE,
1,
f"border: {invisible_border_edge};", # #root has no visible border
"", # no specific placeholder style
# #root's virtual size=screen size
(SCREEN_W, SCREEN_H),
# placeholders width=same than screen :: height=default height
(SCREEN_W, PLACEHOLDERS_DEFAULT_H),
# placeholders should be at offset 0
0,
]
for invisible_border_edge in ("", "none", "hidden")
],
[
SCREEN_SIZE,
@@ -169,3 +162,75 @@ async def test_composition_of_vertical_container_with_children(
assert placeholder.size == expected_placeholders_size
assert placeholder.styles.offset.x.value == 0.0
assert app.screen.get_offset(placeholder).x == expected_placeholders_offset_x
@pytest.mark.asyncio
@pytest.mark.integration_test
@pytest.mark.parametrize(
"edge_type,expected_box_inner_size,expected_box_size,expected_top_left_edge_color,expects_visible_char_at_top_left_edge",
(
# These first 3 types of border edge types are synonyms, and display no borders:
["", Size(SCREEN_W, 1), Size(SCREEN_W, 1), "black", False],
["none", Size(SCREEN_W, 1), Size(SCREEN_W, 1), "black", False],
["hidden", Size(SCREEN_W, 1), Size(SCREEN_W, 1), "black", False],
# Let's transition to "blank": we still see no visible border, but the size is increased
# as the gutter space is reserved the same way it would be with a border:
["blank", Size(SCREEN_W - 2, 1), Size(SCREEN_W, 3), "#ffffff", False],
# And now for the "normally visible" border edge types:
# --> we see a visible border, and the size is increased:
*[
[edge_style, Size(SCREEN_W - 2, 1), Size(SCREEN_W, 3), "#ffffff", True]
for edge_style in [
"round",
"solid",
"double",
"dashed",
"heavy",
"inner",
"outer",
"hkey",
"vkey",
"tall",
"wide",
]
],
),
)
async def test_border_edge_types_impact_on_widget_size(
edge_type: EdgeType,
expected_box_inner_size: Size,
expected_box_size: Size,
expected_top_left_edge_color: str,
expects_visible_char_at_top_left_edge: bool,
):
class BorderTarget(Widget):
def render(self, style) -> RenderableType:
return Text("border target", style="black on yellow", justify="center")
border_target = BorderTarget()
border_target.styles.height = "auto"
border_target.styles.border = (edge_type, "white")
class MyTestApp(AppTest):
def compose(self) -> ComposeResult:
yield border_target
app = MyTestApp(size=SCREEN_SIZE, test_name="border_edge_types")
await app.boot_and_shutdown()
box_inner_size = Size(
border_target.content_region.width,
border_target.content_region.height,
)
assert box_inner_size == expected_box_inner_size
assert border_target.size == expected_box_size
top_left_edge_style = app.screen.get_style_at(0, 0)
top_left_edge_color = top_left_edge_style.color.name
assert top_left_edge_color == expected_top_left_edge_color
top_left_edge_char = app.get_char_at(0, 0)
top_left_edge_char_is_a_visible_one = top_left_edge_char != " "
assert top_left_edge_char_is_a_visible_one == expects_visible_char_at_top_left_edge

View File

@@ -8,7 +8,7 @@ from typing import AsyncContextManager, cast
from rich.console import Console
from textual import events
from textual import events, errors
from textual.app import App, ReturnType, ComposeResult
from textual.driver import Driver
from textual.geometry import Size
@@ -123,6 +123,36 @@ class AppTest(App):
last_display_start_index = total_capture.rindex(CLEAR_SCREEN_SEQUENCE)
return total_capture[last_display_start_index:]
def get_char_at(self, x: int, y: int) -> str:
"""Get the character at the given cell or empty string
Args:
x (int): X position within the Layout
y (int): Y position within the Layout
Returns:
str: The character at the cell (x, y) within the Layout
"""
# N.B. Basically a copy-paste-and-slightly-adapt of `Compositor.get_style_at()`
try:
widget, region = self.get_widget_at(x, y)
except errors.NoWidget:
return ""
if widget not in self.screen._compositor.regions:
return ""
x -= region.x
y -= region.y
lines = widget.get_render_lines(y, y + 1)
if not lines:
return ""
end = 0
for segment in lines[0]:
end += segment.cell_length
if x < end:
return segment.text[0]
return ""
@property
def console(self) -> ConsoleTest:
return self._console