"""Extensive tests for the custom SVG exporter.""" from __future__ import annotations from textual_webterm.svg_exporter import ( ANSI_COLORS, DEFAULT_BG, DEFAULT_FG, CharData, _color_to_hex, _escape_xml, render_terminal_svg, ) class TestColorToHex: """Tests for _color_to_hex function.""" def test_default_foreground(self) -> None: """Default color returns DEFAULT_FG for foreground.""" assert _color_to_hex("default", is_foreground=True) == DEFAULT_FG def test_default_background(self) -> None: """Default color returns DEFAULT_BG for background.""" assert _color_to_hex("default", is_foreground=False) == DEFAULT_BG def test_hex_color_passthrough(self) -> None: """Hex colors pass through unchanged.""" assert _color_to_hex("#ff0000") == "#ff0000" assert _color_to_hex("#123456") == "#123456" assert _color_to_hex("#AABBCC") == "#AABBCC" def test_hex_color_without_hash(self) -> None: """Hex colors without # prefix (pyte's 256-color/truecolor) get # added.""" assert _color_to_hex("ff0000") == "#ff0000" assert _color_to_hex("123456") == "#123456" assert _color_to_hex("AABBCC") == "#AABBCC" assert _color_to_hex("ff8700") == "#ff8700" # Common 256-color orange def test_named_colors(self) -> None: """Named ANSI colors map correctly.""" assert _color_to_hex("red") == ANSI_COLORS["red"] assert _color_to_hex("green") == ANSI_COLORS["green"] assert _color_to_hex("blue") == ANSI_COLORS["blue"] assert _color_to_hex("white") == ANSI_COLORS["white"] assert _color_to_hex("black") == ANSI_COLORS["black"] def test_bright_colors(self) -> None: """Bright color variants map correctly.""" assert _color_to_hex("brightred") == ANSI_COLORS["brightred"] assert _color_to_hex("brightgreen") == ANSI_COLORS["brightgreen"] assert _color_to_hex("brightblue") == ANSI_COLORS["brightblue"] def test_case_insensitive(self) -> None: """Color names are case-insensitive.""" assert _color_to_hex("RED") == ANSI_COLORS["red"] assert _color_to_hex("Green") == ANSI_COLORS["green"] assert _color_to_hex("BRIGHTBLUE") == ANSI_COLORS["brightblue"] def test_unknown_color_returns_default(self) -> None: """Unknown color names return default.""" assert _color_to_hex("unknowncolor", is_foreground=True) == DEFAULT_FG assert _color_to_hex("unknowncolor", is_foreground=False) == DEFAULT_BG def test_rgb_format_returns_default(self) -> None: """RGB format falls back to default (not commonly used in terminals).""" assert _color_to_hex("rgb(255,0,0)", is_foreground=True) == DEFAULT_FG assert _color_to_hex("rgb(0,255,0)", is_foreground=False) == DEFAULT_BG def test_gray_aliases(self) -> None: """Gray/grey aliases work.""" assert _color_to_hex("gray") == ANSI_COLORS["gray"] assert _color_to_hex("grey") == ANSI_COLORS["grey"] assert _color_to_hex("lightgray") == ANSI_COLORS["lightgray"] assert _color_to_hex("lightgrey") == ANSI_COLORS["lightgrey"] class TestEscapeXml: """Tests for XML escaping.""" def test_no_special_chars(self) -> None: """Plain text passes through unchanged.""" assert _escape_xml("hello world") == "hello world" def test_less_than(self) -> None: """Less than is escaped.""" assert _escape_xml("<") == "<" assert _escape_xml("a < b") == "a < b" def test_greater_than(self) -> None: """Greater than is escaped.""" assert _escape_xml(">") == ">" assert _escape_xml("a > b") == "a > b" def test_ampersand(self) -> None: """Ampersand is escaped.""" assert _escape_xml("&") == "&" assert _escape_xml("a & b") == "a & b" def test_quotes(self) -> None: """Quotes are escaped.""" assert _escape_xml('"') == """ assert _escape_xml("'") == "'" def test_mixed_special_chars(self) -> None: """Multiple special chars are all escaped.""" assert _escape_xml('') == ( "<script>"alert"</script>" ) def test_unicode_preserved(self) -> None: """Unicode characters are preserved.""" assert _escape_xml("你好世界") == "你好世界" assert _escape_xml("🎉🚀") == "🎉🚀" class TestRenderTerminalSvg: """Tests for render_terminal_svg function.""" def _char( self, data: str, fg: str = "default", bg: str = "default", bold: bool = False, italics: bool = False, underscore: bool = False, reverse: bool = False, ) -> CharData: """Helper to create CharData.""" return { "data": data, "fg": fg, "bg": bg, "bold": bold, "italics": italics, "underscore": underscore, "reverse": reverse, } def _make_buffer(self, rows: list[str]) -> list[list[CharData]]: """Create simple buffer from strings.""" return [[self._char(c) for c in row] for row in rows] def test_empty_buffer(self) -> None: """Empty buffer produces valid SVG.""" svg = render_terminal_svg([], width=80, height=24) assert svg.startswith("") assert 'xmlns="http://www.w3.org/2000/svg"' in svg def test_css_properties(self) -> None: """SVG includes essential CSS properties for proper rendering.""" svg = render_terminal_svg([], width=80, height=24) # Check for legibility optimization assert "text-rendering: optimizeLegibility" in svg # Check for monospace font assert "font-family:" in svg assert "monospace" in svg # Check for pre whitespace handling assert "white-space: pre" in svg def test_buffer_with_empty_rows(self) -> None: """Buffer with rows containing only empty cells produces valid SVG.""" # Row with only empty placeholder cells (no actual characters) buffer = [ [self._char("") for _ in range(10)], # Empty row [self._char("A")], # Normal row [self._char("") for _ in range(10)], # Another empty row ] svg = render_terminal_svg(buffer, width=10, height=3) assert svg.startswith(" None: """Buffer with truly empty row (empty list) is handled.""" buffer = [ [], # Truly empty row (no cells at all) [self._char("B")], # Normal row ] svg = render_terminal_svg(buffer, width=10, height=2) assert svg.startswith("B" in svg def test_basic_text_output(self) -> None: """Basic text is included in SVG (each char with explicit x position).""" buffer = self._make_buffer(["Hello, World!"]) svg = render_terminal_svg(buffer, width=80, height=24) # Each character is rendered individually with explicit x assert ">H" in svg assert ">e" in svg assert ">!" in svg def test_multiline_output(self) -> None: """Multiple lines render correctly.""" buffer = self._make_buffer(["Line 1", "Line 2", "Line 3"]) svg = render_terminal_svg(buffer, width=80, height=24) # Check for characters from each line assert ">L" in svg assert ">1" in svg assert ">2" in svg assert ">3" in svg # Should have 3 text elements assert svg.count("&test"]) svg = render_terminal_svg(buffer, width=80, height=24) assert "<" in svg # < escaped assert ">" in svg # > escaped assert "&" in svg # & escaped assert "