Files
rich-print/tests/test_render.py
Yunxiao Xu 7df3acde9b fix: enable word wrapping for syntax-highlighted code output
Long code lines were clipped at the terminal width, making them
unreadable. Pass word_wrap=True to all Syntax() calls so lines wrap
instead of being cut off.
2026-04-04 14:47:21 -07:00

328 lines
11 KiB
Python

"""Tests for the rendering module."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from rich.json import JSON as RichJSON
from rich.markdown import Markdown
from rich.padding import Padding
from rich.syntax import Syntax
from rp.pager import MousePager
from rp.render import RenderOptions, render
class TestRenderOptions:
"""RenderOptions dataclass defaults."""
def test_default_theme(self) -> None:
opts = RenderOptions()
assert opts.theme == "monokai"
def test_default_line_numbers_is_none(self) -> None:
opts = RenderOptions()
assert opts.line_numbers is None
def test_default_pager_is_none(self) -> None:
opts = RenderOptions()
assert opts.pager is None
def test_custom_values(self) -> None:
opts = RenderOptions(theme="github-dark", line_numbers=True, pager=False)
assert opts.theme == "github-dark"
assert opts.line_numbers is True
assert opts.pager is False
def test_default_debug_is_none(self) -> None:
opts = RenderOptions()
assert opts.debug is None
class TestRenderMarkdown:
"""Markdown rendering."""
@pytest.mark.parametrize("file_type", ["markdown", "md"])
def test_renders_markdown_types(
self,
capture_console,
sample_markdown: str,
file_type: str,
) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.Markdown", wraps=Markdown) as mock_markdown:
render(sample_markdown, file_type, capture_console, opts)
output = capture_console.file.getvalue()
assert len(output) > 0
mock_markdown.assert_called_once_with(sample_markdown)
class TestRenderJson:
"""JSON rendering."""
@pytest.mark.parametrize("content", ['{"a": 1}', "[1, 2, 3]"])
def test_renders_valid_json(
self,
capture_console,
content: str,
) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.RichJSON", wraps=RichJSON) as mock_json:
render(content, "json", capture_console, opts)
output = capture_console.file.getvalue()
assert len(output) > 0
mock_json.assert_called_once_with(content)
def test_invalid_json_falls_back_to_syntax(
self,
capture_console,
) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
render("{invalid json}", "json", capture_console, opts)
output = capture_console.file.getvalue()
assert len(output) > 0
mock_syntax.assert_called_once_with(
"{invalid json}",
"json",
theme="monokai",
line_numbers=False,
word_wrap=True,
)
class TestRenderCode:
"""Code rendering with Syntax."""
def test_renders_python(self, capture_console, sample_python: str) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
render(sample_python, "python", capture_console, opts)
output = capture_console.file.getvalue()
assert len(output) > 0
mock_syntax.assert_called_once_with(
sample_python,
"python",
theme="monokai",
line_numbers=True,
word_wrap=True,
)
def test_renders_unknown_type_as_text_fallback(self, capture_console) -> None:
opts = RenderOptions(pager=False)
def syntax_side_effect(*args, **kwargs):
lexer = args[1]
if lexer == "xyz_nonexistent_lexer":
raise Exception("unknown lexer")
return Syntax(*args, **kwargs)
with patch("rp.render.Syntax", side_effect=syntax_side_effect) as mock_syntax:
render("some content", "xyz_nonexistent_lexer", capture_console, opts)
output = capture_console.file.getvalue()
assert len(output) > 0
assert mock_syntax.call_args_list[0].args == (
"some content",
"xyz_nonexistent_lexer",
)
assert mock_syntax.call_args_list[0].kwargs == {
"theme": "monokai",
"line_numbers": True,
"word_wrap": True,
}
assert mock_syntax.call_args_list[1].args == ("some content", "text")
assert mock_syntax.call_args_list[1].kwargs == {
"theme": "monokai",
"line_numbers": True,
"word_wrap": True,
}
def test_renders_rust(self, capture_console) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
render("fn main() {}", "rust", capture_console, opts)
output = capture_console.file.getvalue()
assert len(output) > 0
mock_syntax.assert_called_once_with(
"fn main() {}",
"rust",
theme="monokai",
line_numbers=True,
word_wrap=True,
)
class TestCodeWrap:
"""Code wrapping (word_wrap) for syntax-highlighted output."""
def test_python_syntax_has_word_wrap(
self, capture_console, sample_python: str
) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
render(sample_python, "python", capture_console, opts)
mock_syntax.assert_called_once()
assert mock_syntax.call_args.kwargs["word_wrap"] is True
def test_long_line_wraps_in_output(self, capture_console) -> None:
long_line = "x = " + "'a' * " * 200 + "1"
opts = RenderOptions(pager=False, line_numbers=False)
render(long_line, "python", capture_console, opts)
output = capture_console.file.getvalue()
assert len(output) > 0
lines = output.split("\n")
non_empty = [line for line in lines if line.strip()]
assert len(non_empty) > 1, "Long line should wrap across multiple visual lines"
def test_text_fallback_has_word_wrap(self, capture_console) -> None:
opts = RenderOptions(pager=False)
def raise_on_bad_lexer(*args, **kwargs):
if args[1] == "unknown_xyz":
raise Exception("bad lexer")
return Syntax(*args, **kwargs)
with patch("rp.render.Syntax", side_effect=raise_on_bad_lexer) as mock_syntax:
render("some text", "unknown_xyz", capture_console, opts)
fallback_call = mock_syntax.call_args_list[-1]
assert fallback_call.args[1] == "text"
assert fallback_call.kwargs["word_wrap"] is True
def test_json_fallback_has_word_wrap(self, capture_console) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
render("{invalid}", "json", capture_console, opts)
mock_syntax.assert_called_once()
assert mock_syntax.call_args.kwargs["word_wrap"] is True
class TestLineNumbers:
"""Line number auto-detection logic."""
@pytest.mark.parametrize(
("content", "file_type", "expected"),
[
("x = 1", "python", True),
("# Heading", "markdown", False),
("{}", "json", False),
],
)
def test_default_line_number_behavior(
self,
capture_console,
content: str,
file_type: str,
expected: bool,
) -> None:
opts = RenderOptions(pager=False)
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
render(content, file_type, capture_console, opts)
if file_type == "python":
mock_syntax.assert_called_once()
assert mock_syntax.call_args.kwargs["line_numbers"] is expected
else:
assert len(capture_console.file.getvalue()) > 0
@pytest.mark.parametrize("line_numbers", [True, False])
def test_explicit_line_numbers(
self,
capture_console,
sample_python: str,
line_numbers: bool,
) -> None:
opts = RenderOptions(line_numbers=line_numbers, pager=False)
with patch("rp.render.Syntax", wraps=Syntax) as mock_syntax:
render(sample_python, "python", capture_console, opts)
mock_syntax.assert_called_once()
assert mock_syntax.call_args.kwargs["line_numbers"] is line_numbers
class TestPagerBehavior:
"""Pager auto-detection."""
def test_pager_disabled(self, capture_console, sample_python: str) -> None:
opts = RenderOptions(pager=False)
with patch.object(capture_console, "pager") as mock_pager:
render(sample_python, "python", capture_console, opts)
assert len(capture_console.file.getvalue()) > 0
mock_pager.assert_not_called()
def test_pager_auto_tty(self, capture_console, sample_python: str) -> None:
opts = RenderOptions(pager=None)
mock_context = MagicMock()
mock_context.__enter__.return_value = None
mock_context.__exit__.return_value = None
with patch("sys.stdout.isatty", return_value=True):
with patch.object(
capture_console, "pager", return_value=mock_context
) as mock_pager:
render(sample_python, "python", capture_console, opts)
mock_pager.assert_called_once()
assert mock_pager.call_args.kwargs["styles"] is True
assert isinstance(mock_pager.call_args.kwargs["pager"], MousePager)
def test_pager_auto_non_tty(self, capture_console, sample_python: str) -> None:
opts = RenderOptions(pager=None)
with patch("sys.stdout.isatty", return_value=False):
with patch.object(capture_console, "pager") as mock_pager:
render(sample_python, "python", capture_console, opts)
assert len(capture_console.file.getvalue()) > 0
mock_pager.assert_not_called()
def test_logs_auto_pager_decision(
self, capture_console, sample_python: str
) -> None:
messages: list[str] = []
opts = RenderOptions(pager=None, debug=messages.append)
with patch("sys.stdout.isatty", return_value=False):
render(sample_python, "python", capture_console, opts)
assert "pager: auto-detected disabled because stdout is not a TTY" in messages
assert "pager: writing directly to the console" in messages
class TestPadding:
"""Rendered output is wrapped in padding."""
def test_wraps_renderable_in_padding(
self,
capture_console,
sample_python: str,
) -> None:
opts = RenderOptions(pager=False)
with patch.object(capture_console, "print") as mock_print:
render(sample_python, "python", capture_console, opts)
printed = mock_print.call_args.args[0]
assert isinstance(printed, Padding)
assert printed.expand is True