Fix Rich markup errors + add UI rendering tests

- Fix mismatched closing tags in menu markup
- Use Text() objects for safe string rendering
- Add comprehensive UI render tests that catch markup errors
- Tests cover: menu, game status, chess board, narrow terminals
- CI will now catch these before deployment
This commit is contained in:
Greg Hendrickson
2026-01-27 18:50:21 +00:00
parent 39afbadf91
commit e3915f1b33
2 changed files with 174 additions and 6 deletions

View File

@@ -173,14 +173,14 @@ async def run_simple_menu(process, session: TerminalSession, username: str, mode
# Menu items as a table for better alignment
menu_table = Table(show_header=False, box=None, padding=(0, 2))
menu_table.add_column(justify="center")
menu_table.add_row(f"[cyan]Welcome, [bold bright_white]{username}[/]![/cyan]")
menu_table.add_row(Text(f"Welcome, {username}!", style="cyan"))
menu_table.add_row("")
menu_table.add_row("[bright_white on blue] 1 [/] [white]Play vs AI[/] [dim]♔ vs ♚[/dim]")
menu_table.add_row("[bright_white on magenta] 2 [/] [white]Play vs Human[/] [dim]♔ vs ♔[/dim]")
menu_table.add_row("[bright_white on green] 3 [/] [white]Learn & Practice[/] [dim]📖[/dim]")
menu_table.add_row("[bright_white on red] q [/] [white]Quit[/] [dim]👋[/dim]")
menu_table.add_row("[bright_white on blue] 1 [/bright_white on blue] Play vs AI [dim]♔ vs ♚[/dim]")
menu_table.add_row("[bright_white on magenta] 2 [/bright_white on magenta] Play vs Human [dim]♔ vs ♔[/dim]")
menu_table.add_row("[bright_white on green] 3 [/bright_white on green] Learn & Practice [dim]📖[/dim]")
menu_table.add_row("[bright_white on red] q [/bright_white on red] Quit [dim]👋[/dim]")
menu_table.add_row("")
menu_table.add_row("[dim italic]Press a key to select...[/dim]")
menu_table.add_row(Text("Press a key to select...", style="dim italic"))
panel_width = min(45, session.width - 4)
panel = Panel(

168
tests/test_ui_render.py Normal file
View File

@@ -0,0 +1,168 @@
"""Tests for UI rendering - catches Rich markup errors before deployment."""
import pytest
from io import StringIO
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from rich.align import Align
from rich.box import ROUNDED
def test_menu_render_no_markup_errors():
"""Test that the main menu renders without Rich markup errors."""
# Simulate the menu rendering code
output = StringIO()
console = Console(file=output, width=80, height=24, force_terminal=True, color_system="truecolor")
username = "testuser"
width = 80
height = 24
pieces = "♔ ♕ ♖ ♗ ♘ ♙"
# Title section
console.print(Align.center(Text(pieces, style="dim white")))
console.print()
console.print(Align.center(Text("S H E L L M A T E", style="bold bright_green")))
console.print(Align.center(Text("" * 20, style="green")))
console.print(Align.center(Text("SSH into Chess Mastery", style="italic bright_black")))
console.print(Align.center(Text(pieces[::-1], style="dim white")))
console.print()
# Menu table - this is where markup errors often occur
menu_table = Table(show_header=False, box=None, padding=(0, 2))
menu_table.add_column(justify="center")
menu_table.add_row(Text(f"Welcome, {username}!", style="cyan"))
menu_table.add_row("")
menu_table.add_row("[bright_white on blue] 1 [/bright_white on blue] Play vs AI [dim]♔ vs ♚[/dim]")
menu_table.add_row("[bright_white on magenta] 2 [/bright_white on magenta] Play vs Human [dim]♔ vs ♔[/dim]")
menu_table.add_row("[bright_white on green] 3 [/bright_white on green] Learn & Practice [dim]📖[/dim]")
menu_table.add_row("[bright_white on red] q [/bright_white on red] Quit [dim]👋[/dim]")
menu_table.add_row("")
menu_table.add_row(Text("Press a key to select...", style="dim italic"))
panel_width = min(45, width - 4)
panel = Panel(
Align.center(menu_table),
box=ROUNDED,
border_style="bright_blue",
width=panel_width,
padding=(1, 2),
)
console.print(Align.center(panel))
# Footer
console.print()
console.print(Align.center(Text(f"Terminal: {width}×{height}", style="dim")))
# If we got here without exception, markup is valid
rendered = output.getvalue()
# Check key content is present (content may have ANSI codes)
assert "S H E L L M A T E" in rendered or "SHELLMATE" in rendered
assert "Welcome" in rendered
assert "Play vs AI" in rendered
def test_game_status_render():
"""Test that game status panel renders correctly."""
output = StringIO()
console = Console(file=output, width=80, height=24, force_terminal=True)
status_lines = []
status_lines.append("[bold white]White ♔ to move[/bold white]")
status_lines.append("[dim]Moves: e2e4 e7e5 g1f3[/dim]")
status_lines.append("")
status_lines.append("[dim]Enter move (e.g. e2e4) │ [q]uit │ [r]esign[/dim]")
panel = Panel(
"\n".join(status_lines),
box=ROUNDED,
border_style="blue",
width=50,
title="[bold]Game Status[/bold]"
)
console.print(panel)
rendered = output.getvalue()
assert "White" in rendered
assert "Game Status" in rendered
def test_chess_board_render():
"""Test that chess board renders without errors."""
output = StringIO()
console = Console(file=output, width=80, height=24, force_terminal=True, color_system="truecolor")
PIECES = {
'K': '', 'Q': '', 'R': '', 'B': '', 'N': '', 'P': '',
'k': '', 'q': '', 'r': '', 'b': '', 'n': '', 'p': '',
}
# Render a simple board section
console.print(" a b c d e f g h")
console.print(" ┌───┬───┬───┬───┬───┬───┬───┬───┐")
# Render one rank with colored squares
row = "8 │"
for file in range(8):
is_light = (7 + file) % 2 == 1
if is_light:
bg = "on #769656"
else:
bg = "on #4a7c3f"
# Starting position pieces
pieces_row = ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r']
char = PIECES.get(pieces_row[file], ' ')
row += f"[#1a1a1a bold {bg}] {char} [/]│"
row += " 8"
console.print(row)
rendered = output.getvalue()
assert "a b c" in rendered
def test_narrow_terminal_render():
"""Test that menu renders correctly on narrow terminals."""
output = StringIO()
console = Console(file=output, width=40, height=20, force_terminal=True)
# Small terminal fallback
console.print(Align.center(Text("♟ SHELLMATE ♟", style="bold green")))
console.print()
menu_table = Table(show_header=False, box=None)
menu_table.add_column(justify="center")
menu_table.add_row(Text("Welcome!", style="cyan"))
menu_table.add_row("[dim]1. Play[/dim]")
menu_table.add_row("[dim]q. Quit[/dim]")
console.print(Align.center(menu_table))
rendered = output.getvalue()
assert "SHELLMATE" in rendered
def test_markup_escape_special_chars():
"""Test that usernames with special chars don't break markup."""
output = StringIO()
console = Console(file=output, width=80, height=24, force_terminal=True)
# Usernames that could break markup
test_usernames = [
"normal_user",
"user[with]brackets",
"user<with>angles",
"[admin]",
"test/path",
]
for username in test_usernames:
# Using Text() object safely escapes special characters
console.print(Text(f"Welcome, {username}!", style="cyan"))
rendered = output.getvalue()
assert "normal_user" in rendered
# Text() should have escaped the brackets safely