""" Unit tests for engine.display.renderer module. Tests ANSI parsing and PIL rendering utilities. """ import pytest try: from PIL import Image PIL_AVAILABLE = True except ImportError: PIL_AVAILABLE = False from engine.display.renderer import ANSI_COLORS, parse_ansi, render_to_pil class TestParseANSI: """Tests for parse_ansi function.""" def test_plain_text(self): """Plain text without ANSI codes returns single token.""" tokens = parse_ansi("Hello World") assert len(tokens) == 1 assert tokens[0][0] == "Hello World" # Check default colors assert tokens[0][1] == (204, 204, 204) # fg assert tokens[0][2] == (0, 0, 0) # bg assert tokens[0][3] is False # bold def test_empty_string(self): """Empty string returns single empty token.""" tokens = parse_ansi("") assert tokens == [("", (204, 204, 204), (0, 0, 0), False)] def test_reset_code(self): """Reset code (ESC[0m) restores defaults.""" tokens = parse_ansi("\x1b[31mRed\x1b[0mNormal") assert len(tokens) == 2 assert tokens[0][0] == "Red" # Red fg should be ANSI_COLORS[1] assert tokens[0][1] == ANSI_COLORS[1] assert tokens[1][0] == "Normal" assert tokens[1][1] == (204, 204, 204) # back to default def test_bold_code(self): """Bold code (ESC[1m) sets bold flag.""" tokens = parse_ansi("\x1b[1mBold") assert tokens[0][3] is True def test_bold_off_code(self): """Bold off (ESC[22m) clears bold.""" tokens = parse_ansi("\x1b[1mBold\x1b[22mNormal") assert tokens[0][3] is True assert tokens[1][3] is False def test_4bit_foreground_colors(self): """4-bit foreground colors (30-37, 90-97) work.""" # Test normal red (31) tokens = parse_ansi("\x1b[31mRed") assert tokens[0][1] == ANSI_COLORS[1] # color 1 = red # Test bright cyan (96) - maps to index 14 (bright cyan) tokens = parse_ansi("\x1b[96mCyan") assert tokens[0][1] == ANSI_COLORS[14] # bright cyan def test_4bit_background_colors(self): """4-bit background colors (40-47, 100-107) work.""" # Green bg = 42 tokens = parse_ansi("\x1b[42mText") assert tokens[0][2] == ANSI_COLORS[2] # color 2 = green # Bright magenta bg = 105 tokens = parse_ansi("\x1b[105mText") assert tokens[0][2] == ANSI_COLORS[13] # bright magenta (13) def test_multiple_ansi_codes_in_sequence(self): """Multiple codes in one escape sequence are parsed.""" tokens = parse_ansi("\x1b[1;31;42mBold Red on Green") assert tokens[0][0] == "Bold Red on Green" assert tokens[0][3] is True # bold assert tokens[0][1] == ANSI_COLORS[1] # red fg assert tokens[0][2] == ANSI_COLORS[2] # green bg def test_nested_ansi_sequences(self): """Multiple separate ANSI sequences are tokenized correctly.""" text = "\x1b[31mRed\x1b[32mGreen\x1b[0mNormal" tokens = parse_ansi(text) assert len(tokens) == 3 assert tokens[0][0] == "Red" assert tokens[1][0] == "Green" assert tokens[2][0] == "Normal" def test_interleaved_text_and_ansi(self): """Text before and after ANSI codes is tokenized.""" tokens = parse_ansi("Pre\x1b[31mRedPost") assert len(tokens) == 2 assert tokens[0][0] == "Pre" assert tokens[1][0] == "RedPost" assert tokens[1][1] == ANSI_COLORS[1] def test_all_standard_4bit_colors(self): """All 4-bit color indices (0-15) map to valid RGB.""" for i in range(16): tokens = parse_ansi(f"\x1b[{i}mX") # Should be a defined color or default fg fg = tokens[0][1] valid = fg in ANSI_COLORS.values() or fg == (204, 204, 204) assert valid, f"Color {i} produced invalid fg {fg}" def test_unknown_code_ignored(self): """Unknown numeric codes are ignored, keep current style.""" tokens = parse_ansi("\x1b[99mText") # 99 is not recognized, should keep previous state (defaults) assert tokens[0][1] == (204, 204, 204) @pytest.mark.skipif(not PIL_AVAILABLE, reason="PIL not available") class TestRenderToPIL: """Tests for render_to_pil function (requires PIL).""" def test_renders_plain_text(self): """Plain buffer renders as image.""" buffer = ["Hello"] img = render_to_pil(buffer, width=10, height=1) assert isinstance(img, Image.Image) assert img.mode == "RGBA" def test_renders_with_ansi_colors(self): """Buffer with ANSI colors renders correctly.""" buffer = ["\x1b[31mRed\x1b[0mNormal"] img = render_to_pil(buffer, width=20, height=1) assert isinstance(img, Image.Image) def test_multi_line_buffer(self): """Multiple lines render with correct height.""" buffer = ["Line1", "Line2", "Line3"] img = render_to_pil(buffer, width=10, height=3) # Height should be approximately 3 * cell_height (18-2 padding) assert img.height > 0 def test_clipping_to_height(self): """Buffer longer than height is clipped.""" buffer = ["Line1", "Line2", "Line3", "Line4"] img = render_to_pil(buffer, width=10, height=2) # Should only render 2 lines assert img.height < img.width * 2 # roughly 2 lines tall def test_cell_dimensions_respected(self): """Custom cell_width and cell_height are used.""" buffer = ["Test"] img = render_to_pil(buffer, width=5, height=1, cell_width=20, cell_height=25) assert img.width == 5 * 20 assert img.height == 25 def test_font_fallback_on_invalid(self): """Invalid font path falls back to default font.""" buffer = ["Test"] # Should not crash with invalid font path img = render_to_pil( buffer, width=5, height=1, font_path="/nonexistent/font.ttf" ) assert isinstance(img, Image.Image)