"""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