"""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 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, ) 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, ) 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, } assert mock_syntax.call_args_list[1].args == ("some content", "text") assert mock_syntax.call_args_list[1].kwargs == { "theme": "monokai", "line_numbers": 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, ) 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() 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