Files
textual/tests/test_content.py
Jasmijn Wellner ceac08493b Fix typo
2025-10-05 18:07:14 +02:00

376 lines
12 KiB
Python

from __future__ import annotations
import pytest
from rich.text import Text
from textual.content import Content, Span
from textual.style import Style
from textual.visual import RenderOptions
from textual.widget import Widget
def test_blank():
"""Check blank content."""
blank = Content("")
assert isinstance(blank, Content)
assert str(blank) == ""
assert blank.plain == ""
assert not blank
assert blank.markup == ""
assert len(blank) == 0
assert blank.spans == []
def test_simple():
"""Check content with simple unstyled text."""
simple = Content("foo")
assert isinstance(simple, Content)
assert str(simple) == "foo"
assert simple.plain == "foo"
assert simple # Not empty is truthy
assert simple.markup == "foo"
assert len(simple) == 3
assert simple.spans == []
def test_constructor():
content = Content("Hello, World")
assert content
assert len(content) == 12
assert content.cell_length == 12
assert content.plain == "Hello, World"
repr(content)
def test_bool():
assert bool(Content("foo")) is True
assert bool(Content("")) is False
def test_from_rich_text():
text = Text.from_markup("[red]Hello[/red] [blue]World[/blue]")
content = Content.from_rich_text(text)
assert len(content) == 11
assert content.plain == "Hello World"
assert [Span(start=0, end=5, style="red"), Span(start=6, end=11, style="blue")]
def test_styled():
text = Content.styled("Hello", "red")
assert text.plain == "Hello"
assert len(text) == 5
assert text.cell_length == 5
assert text._spans == [Span(0, 5, "red")]
def test_getitem():
content = Content("Hello, world").stylize("blue", 0, 5)
assert content[0].plain == "H"
assert content[0]._spans == [Span(0, 1, "blue")]
assert content[-1].plain == "d"
assert content[-1]._spans == []
assert content[:2].plain == "He"
assert content[:2]._spans == [Span(0, 2, "blue")]
def test_cell_length():
assert Content("").cell_length == 0
assert Content("foo").cell_length == 3
assert Content("💩").cell_length == 2
def test_stylize() -> None:
"""Test the stylize method."""
foo = Content("foo bar")
assert foo.spans == []
red_foo = foo.stylize("red")
# stylize create a new object
assert foo.spans == []
# With no parameters, full string is stylized
assert red_foo.spans == [Span(0, 7, "red")]
red_foo = red_foo.stylize("blue", 4, 7)
# Another span is appended
assert red_foo.spans == [
Span(0, 7, "red"),
Span(4, 7, "blue"),
]
def test_stylize_before() -> None:
"""Test the stylize_before method."""
foo = Content("foo bar")
assert foo.spans == []
red_foo = foo.stylize("red")
# stylize create a new object
assert foo.spans == []
# With no parameters, full string is stylized
assert red_foo.spans == [Span(0, 7, "red")]
red_foo = red_foo.stylize_before("blue", 4, 7)
# Another span is appended
assert red_foo.spans == [
Span(4, 7, "blue"),
Span(0, 7, "red"),
]
def test_eq() -> None:
"""Test equality."""
assert Content("foo") == Content("foo")
assert Content("foo") == "foo"
assert Content("foo") != Content("bar")
assert Content("foo") != "bar"
def test_add() -> None:
"""Test addition."""
# Simple cases
assert Content("") + Content("") == Content("")
assert Content("foo") + Content("") == Content("foo")
# Works with simple strings
assert Content("foo") + "" == Content("foo")
assert "" + Content("foo") == Content("foo")
# Test spans after addition
content = Content.styled("foo", "red") + " " + Content.styled("bar", "blue")
assert str(content) == "foo bar"
assert content.spans == [Span(0, 3, "red"), Span(4, 7, "blue")]
assert content.cell_length == 7
def test_radd() -> None:
"""Test reverse addition."""
assert "foo" + Content("bar") == Content("foobar")
# Test spans after addition
content = "foo " + Content.styled("bar", "blue")
assert str(content) == "foo bar"
assert content.spans == [Span(4, 7, "blue")]
def test_from_markup():
"""Test simple parsing of content markup."""
content = Content.from_markup("[red]Hello[/red] [blue]World[/blue]")
assert len(content) == 11
assert content.plain == "Hello World"
assert content.spans == [
Span(start=0, end=5, style="red"),
Span(start=6, end=11, style="blue"),
]
def test_markup():
"""Test markup round trip"""
content = Content.from_markup("[red]Hello[/red] [blue]World[/blue]")
assert content.plain == "Hello World"
assert content.markup == "[red]Hello[/red] [blue]World[/blue]"
def test_join():
"""Test the join method."""
# Edge cases
assert Content("").join([]) == ""
assert Content(".").join([]) == ""
assert Content("").join(["foo"]) == "foo"
assert Content(".").join(["foo"]) == "foo"
# Join strings and Content
pieces = [Content.styled("foo", "red"), "bar", Content.styled("baz", "blue")]
content = Content(".").join(pieces)
assert content.plain == "foo.bar.baz"
assert content.spans == [Span(0, 3, "red"), Span(8, 11, "blue")]
def test_sort():
"""Test content may be sorted."""
# functools.total_ordering doing most of the heavy lifting here.
contents = sorted([Content("foo"), Content("bar"), Content("baz")])
assert contents[0].plain == "bar"
assert contents[1].plain == "baz"
assert contents[2].plain == "foo"
def test_truncate():
"""Test truncated method."""
content = Content.from_markup("[red]Hello World[/red]")
# Edge case of 0
assert content.truncate(0).markup == ""
# Edge case of 0 wil ellipsis
assert content.truncate(0, ellipsis=True).markup == ""
# Edge case of 1
assert content.truncate(1, ellipsis=True).markup == "[red]…[/red]"
# Truncate smaller
assert content.truncate(3).markup == "[red]Hel[/red]"
# Truncate to same size
assert content.truncate(11).markup == "[red]Hello World[/red]"
# Truncate smaller will ellipsis
assert content.truncate(5, ellipsis=True).markup == "[red]Hell…[/red]"
# Truncate larger results unchanged
assert content.truncate(15).markup == "[red]Hello World[/red]"
# Truncate larger with padding increases size
assert content.truncate(15, pad=True).markup == "[red]Hello World[/red] "
def test_assemble():
"""Test Content.assemble constructor."""
content = Content.assemble(
"Assemble: ", # Simple text
Content.from_markup(
"pieces of [red]text[/red] or [blue]content[/blue] into "
), # Other Content
("a single Content instance", "underline"), # A tuple of text and a style
)
assert (
content.plain
== "Assemble: pieces of text or content into a single Content instance"
)
print(content.spans)
assert content.spans == [
Span(20, 24, style="red"),
Span(28, 35, style="blue"),
Span(41, 66, style="underline"),
]
@pytest.mark.parametrize(
"markup,plain",
[
("\\[", "["),
("\\[foo", "[foo"),
("\\[foo]", "[foo]"),
("\\[/foo", "[/foo"),
("\\[/foo]", "[/foo]"),
("\\[]", "[]"),
("\\[0]", "[0]"),
],
)
def test_escape(markup: str, plain: str) -> None:
"""Test that escaping the first square bracket."""
content = Content.from_markup(markup)
assert content.plain == plain
assert content.spans == []
def test_strip_control_codes():
"""Test that control codes are removed from content."""
assert Content("foo\r\nbar").plain == "foo\nbar"
assert Content.from_markup("foo\r\nbar").plain == "foo\nbar"
def test_content_from_text():
# Check markup
content1 = Content.from_text("[bold]Hello World")
assert isinstance(content1, Content)
assert content1.plain == "Hello World"
assert content1.spans == [Span(0, 11, "bold")]
# Check Content is unaltered
content2 = Content.from_text(content1)
assert content1 is content2
# Check Text instance is converted to Content
content3 = Content.from_text(Text.from_markup("[bold]Hello World"))
assert isinstance(content3, Content)
assert content3.plain == "Hello World"
assert content3.spans == [Span(0, 11, Style(bold=True))]
# Check markup disabled
content4 = Content.from_text("[bold]Hello World", markup=False)
assert isinstance(content4, Content)
assert content4.plain == "[bold]Hello World"
assert content4.spans == []
def test_first_line():
content = Content("foo\nbar").stylize("red")
first_line = content.first_line
assert first_line.plain == "foo"
assert first_line.spans == [Span(0, 3, "red")]
def test_split_and_tabs():
spans = [
Span(0, 49, style="$text"),
]
content = Content("--- hello.py\t2024-01-15 10:30:00.000000000 -0800", spans=spans)
widget = Widget()
content.render_strips(0, None, Style(), RenderOptions(widget._get_style, {}))
def test_simplify():
"""Test simplify joins spans."""
content = Content.from_markup("[bold]Foo[/][bold]Bar[/]")
assert content.spans == [Span(0, 3, "bold"), Span(3, 6, "bold")]
content.simplify()
assert content.spans == [Span(0, 6, "bold")]
@pytest.mark.parametrize(
["input", "tab_width", "expected"],
[
(Content(""), 8, Content("")),
(Content("H"), 8, Content("H")),
(Content("Hello"), 8, Content("Hello")),
(Content("\t"), 8, Content(" " * 8)),
(Content("A\t"), 8, Content("A" + " " * 7)),
(Content("ABCD\t"), 8, Content("ABCD" + " " * 4)),
(Content("ABCDEFG\t"), 8, Content("ABCDEFG ")),
(Content("ABCDEFGH\t"), 8, Content("ABCDEFGH" + " " * 8)),
(Content("Hel\tlo!"), 4, Content("Hel lo!")),
(Content("\t\t"), 4, Content(" " * 8)),
(Content("FO\t\t"), 4, Content("FO ")),
(Content("FO\tOB\t"), 4, Content("FO OB ")),
(
Content("FOO", spans=[Span(0, 3, "red")]),
4,
Content("FOO", spans=[Span(0, 3, "red")]),
),
(
Content("FOO\tBAR", spans=[Span(0, 3, "red")]),
8,
Content("FOO BAR", spans=[Span(0, 3, "red")]),
),
(
Content("FOO\tBAR", spans=[Span(0, 3, "red"), Span(4, 8, "blue")]),
8,
Content("FOO BAR", spans=[Span(0, 3, "red"), Span(8, 11, "blue")]),
),
(
Content("foo\tbar\nbaz", spans=[Span(0, 11, "red")]),
8,
Content("foo bar\nbaz", spans=[Span(0, 15, "red")]),
),
],
)
def test_expand_tabs(input: Content, tab_width: int, expected: Content):
output = input.expand_tabs(tab_width).simplify()
print(repr(output))
assert output.plain == expected.plain
assert output._spans == expected._spans
def test_add_spans() -> None:
content = Content.from_markup("[red]Hello[/red], World!")
content = content.add_spans([Span(0, 5, "green"), Span(7, 9, "blue")])
expected = [
Span(0, 5, style="red"),
Span(0, 5, style="green"),
Span(7, 9, style="blue"),
]
assert content.spans == expected
def test_wrap() -> None:
content = Content.from_markup("[green]Hello, [b]World, One two three[/b]")
wrapped = content.wrap(6)
print(wrapped)
expected = [
Content("Hello,", spans=[Span(0, 6, style="green")]),
Content("World,", spans=[Span(0, 6, style="green"), Span(0, 6, style="b")]),
Content("One", spans=[Span(0, 3, style="green"), Span(0, 3, style="b")]),
Content("two", spans=[Span(0, 3, style="green"), Span(0, 3, style="b")]),
Content("three", spans=[Span(0, 5, style="green"), Span(0, 5, style="b")]),
]
assert len(wrapped) == len(expected)
for line1, line2 in zip(wrapped, expected):
assert line1.is_same(line2)