from unittest.mock import patch, MagicMock from src.markdown_helper import MarkdownRenderer def _mock_imgui(mock_imgui): mock_imgui.TableFlags_ = type("T", (), {"borders": 1, "row_bg": 2, "resizable": 4})() mock_imgui.TableColumnFlags_ = type("C", (), {"width_stretch": 8})() mock_imgui.Col_ = type("C", (), {"text": 0, "text_disabled": 1, "frame_bg": 2, "button": 3, "button_hovered": 4, "button_active": 5})() mock_imgui.begin_table = MagicMock(return_value=True) mock_imgui.end_table = MagicMock() mock_imgui.table_setup_column = MagicMock() mock_imgui.table_next_row = MagicMock() mock_imgui.table_next_column = MagicMock() mock_imgui.text = MagicMock() mock_imgui.text_wrapped = MagicMock() mock_imgui.bullet = MagicMock() mock_imgui.same_line = MagicMock() mock_imgui.new_line = MagicMock() mock_imgui.indent = MagicMock() mock_imgui.unindent = MagicMock() mock_imgui.spacing = MagicMock() mock_imgui.separator = MagicMock() mock_imgui.push_style_color = MagicMock() mock_imgui.pop_style_color = MagicMock() mock_imgui.push_font = MagicMock() mock_imgui.pop_font = MagicMock() mock_imgui.small_button = MagicMock() mock_imgui.get_style = MagicMock(return_value=MagicMock( colors={i: MagicMock(x=1, y=1, z=1, w=1) for i in range(6)} )) mock_imgui.get_io = MagicMock(return_value=MagicMock(fonts=MagicMock(fonts=[None]))) mock_imgui.calc_text_size = MagicMock(return_value=MagicMock(x=50, y=20)) mock_imgui.get_item_rect_min = MagicMock(return_value=MagicMock(x=0, y=0)) mock_imgui.get_window_draw_list = MagicMock(return_value=MagicMock( add_line=MagicMock() )) mock_imgui.is_item_hovered = MagicMock(return_value=False) mock_imgui.is_mouse_released = MagicMock(return_value=False) mock_imgui.get_text_line_height = MagicMock(return_value=20.0) mock_imgui.ImVec2 = lambda *a: MagicMock(x=a[0] if a else 0, y=a[1] if len(a) > 1 else 0) def test_render_passes_bullet_chunks_to_python_renderer(): md = "- one\n- two\n- three\n" with patch("src.md_renderer_py.imgui") as mock_imgui: _mock_imgui(mock_imgui) MarkdownRenderer().render(md, context_id="bullets") assert mock_imgui.bullet.call_count == 3, f"expected 3 bullets, got {mock_imgui.bullet.call_count}" assert mock_imgui.text_wrapped.called def test_render_passes_dash_and_asterisk_bullets_uniformly(): md = "- dash\n* asterisk\n+ plus\n" with patch("src.md_renderer_py.imgui") as mock_imgui: _mock_imgui(mock_imgui) MarkdownRenderer().render(md, context_id="bullets") assert mock_imgui.bullet.call_count == 3, f"all 3 markers should render as bullets (no upstream '*' Y-overlap bug), got {mock_imgui.bullet.call_count}" def test_render_passes_numbered_list_intact(): md = "1. First question\n2. Second question\n" with patch("src.md_renderer_py.imgui") as mock_imgui: _mock_imgui(mock_imgui) MarkdownRenderer().render(md, context_id="numbered") text_args = [str(c) for c in mock_imgui.text.call_args_list] assert any("1." in s for s in text_args), f"expected '1.' in numbered list, got {text_args!r}" assert any("2." in s for s in text_args), f"expected '2.' in numbered list, got {text_args!r}" def test_render_explicit_newline_between_list_paragraphs_fixes_overlap(): md = "- bullet text (long enough to wrap maybe)\n\n continuation paragraph after a blank line" with patch("src.md_renderer_py.imgui") as mock_imgui: _mock_imgui(mock_imgui) MarkdownRenderer().render(md, context_id="cont") assert mock_imgui.new_line.call_count >= 3, ( f"expected at least 3 new_line calls (before bullet, between paragraphs, after list), " f"got {mock_imgui.new_line.call_count}. The pure-Python renderer MUST emit explicit " f"newlines to avoid the imgui-md C++ BLOCK_P no-NewLine-inside-list bug." ) def test_render_handles_empty_input(): r = MarkdownRenderer() r.render("") def test_render_handles_nested_lists(): md = "- outer\n - inner1\n - inner2\n- outer2" with patch("src.md_renderer_py.imgui") as mock_imgui: _mock_imgui(mock_imgui) MarkdownRenderer().render(md, context_id="nested") assert mock_imgui.bullet.call_count == 4, f"expected 4 bullets (1 outer + 2 inner + 1 outer2), got {mock_imgui.bullet.call_count}" def test_render_handles_emphasis_inline(): md = "This is *emphasized* text." with patch("src.md_renderer_py.imgui") as mock_imgui: _mock_imgui(mock_imgui) MarkdownRenderer().render(md, context_id="em") assert mock_imgui.push_style_color.called, "em should push style color" assert mock_imgui.pop_style_color.called def test_render_handles_strong_inline(): md = "This is **strong** text." with patch("src.md_renderer_py.imgui") as mock_imgui: _mock_imgui(mock_imgui) MarkdownRenderer().render(md, context_id="strong") assert mock_imgui.push_style_color.called def test_render_handles_inline_code(): md = "Use `foo()` inline." with patch("src.md_renderer_py.imgui") as mock_imgui: _mock_imgui(mock_imgui) MarkdownRenderer().render(md, context_id="code") assert mock_imgui.small_button.called, "inline code should use small_button" def test_render_handles_table(): md = "| A | B |\n|---|---|\n| 1 | 2 |" with patch("src.md_renderer_py.imgui") as mock_imgui: _mock_imgui(mock_imgui) MarkdownRenderer().render(md, context_id="table") assert mock_imgui.begin_table.called assert mock_imgui.end_table.called assert mock_imgui.table_setup_column.call_count == 2 def test_render_handles_link(): md = "Click [here](https://example.com)." with patch("src.md_renderer_py.imgui") as mock_imgui: _mock_imgui(mock_imgui) MarkdownRenderer().render(md, context_id="link") assert mock_imgui.text_wrapped.called assert mock_imgui.push_style_color.called def test_render_unindented_strips_common_indent(): md = " - a\n - b\n - c" with patch("src.md_renderer_py.imgui") as mock_imgui: _mock_imgui(mock_imgui) MarkdownRenderer().render_unindented(md) assert mock_imgui.bullet.call_count == 3, f"expected 3 bullets after unindenting, got {mock_imgui.bullet.call_count}" def test_render_does_not_use_imgui_md_anymore(): md = "# heading\n\n- item 1\n- item 2\n\nplain prose" with patch("src.md_renderer_py.imgui") as mock_imgui, \ patch("src.markdown_helper.imgui_md") as mock_md: _mock_imgui(mock_imgui) MarkdownRenderer().render(md, context_id="all") assert mock_md.render.call_count == 0, ( "imgui_md.render must NOT be called — the new design uses src.md_renderer_py " "to avoid the C++ imgui-md BLOCK_P no-NewLine-inside-list bug and BLOCK_LI " "asteroid-Y-overlap bug" )