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)